diff --git a/.envrc b/.envrc new file mode 100644 index 0000000..0d0f1d9 --- /dev/null +++ b/.envrc @@ -0,0 +1,5 @@ +# SPDX-FileCopyrightText: 2026 Mohamed Hammad +# SPDX-License-Identifier: GPL-3.0-or-later +# +# direnv: enter the flake's development shell on cd. Run `direnv allow` once. +use flake diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ca0c82d..d80ed75 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -31,6 +31,26 @@ jobs: - name: Test run: cargo test --workspace + nix: + name: nix flake check + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: DeterminateSystems/nix-installer-action@main + + # Builds packages.default (which runs the test suite in its check phase) + # and evaluates every devShell. This is also the gate on the flake staying + # in step with packaging/default.nix, since both come from one definition. + - name: Flake check + run: nix flake check --print-build-logs + + - name: Smoke-test the built binary + run: | + nix build --print-build-logs + ./result/bin/vacuum --version + ./result/bin/vacuum list --json --category temp-files > /dev/null + reuse: name: REUSE compliance runs-on: ubuntu-latest diff --git a/.gitignore b/.gitignore index 6e8b53a..47abcf4 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,13 @@ doc/*.info doc/*.html +# Nix build symlinks (`nix build` writes ./result) +/result +/result-* + +# direnv's per-directory cache; .envrc itself is tracked +/.direnv + # Editor / OS noise .DS_Store *.swp diff --git a/README.md b/README.md index 1fa3530..96bdbe7 100644 --- a/README.md +++ b/README.md @@ -91,12 +91,81 @@ vacuum list --json # machine-readable output for scripts/agents ## Install -Requires Rust (edition 2024, MSRV 1.85+). From a clone: +> Do **not** run `cargo install vacuum`. An unrelated crates.io crate holds that +> name (a spaceflight CLI). Install from this repository, as below. + +### Nix and NixOS + +The flake needs no Rust toolchain on the host — it supplies one. + +```sh +nix run github:Spacecraft-Software/Vacuum # run it once, install nothing +nix profile install github:Spacecraft-Software/Vacuum # install for this user +nix build github:Spacecraft-Software/Vacuum # build only; ./result/bin/vacuum +``` + +To install it system-wide, add the flake as an input and enable the module: + +```nix +{ + inputs.vacuum.url = "github:Spacecraft-Software/Vacuum"; + + # in your NixOS configuration: + # imports = [ inputs.vacuum.nixosModules.default ]; + # programs.vacuum.enable = true; +} +``` + +Or drop the package straight into `environment.systemPackages`: + +```nix +environment.systemPackages = [ inputs.vacuum.packages.${pkgs.system}.default ]; +``` + +For hacking on Vacuum itself, `nix develop` gives you rustc, cargo, clippy, +rustfmt, rust-analyzer, mold, reuse, and texinfo. `nix flake check` builds the +package and runs the test suite. + +### GNU Guix System + +`manifest.scm` supplies the toolchain; nothing is installed on the host until you +ask for it: ```sh -cargo install --path crates/vacuum-cli +git clone https://github.com/Spacecraft-Software/Vacuum +cd Vacuum +guix shell -m manifest.scm -- cargo install --path crates/vacuum-cli ``` +That puts `vacuum` in `~/.cargo/bin`. To work on the source instead: + +```sh +guix shell -m manifest.scm # toolchain only, host untouched +guix shell --pure -m manifest.scm # ignore the ambient environment +``` + +A package definition lives in `packaging/guix.scm`, but it is **not buildable +yet**: `cargo-build-system` resolves dependencies through `#:cargo-inputs` rather +than from `Cargo.lock`, and this workspace has 191 locked crates. That list is +generated with `guix import crate -r vacuum` at release time. Until then, use the +`guix shell` route above. + +### Arch Linux + +```sh +cd packaging && makepkg -si +``` + +### From source + +Requires Rust (edition 2024, MSRV 1.85+): + +```sh +cargo install --path crates/vacuum-cli --force +``` + +`--force` replaces any previous install of the `vacuum` binary. + ## Project Posture Vacuum is a **Personal / Hobby** project (Steelbore Standard §5). Provided **AS IS**, with no diff --git a/doc/vacuum.texi b/doc/vacuum.texi index 9c020c0..1607bbe 100644 --- a/doc/vacuum.texi +++ b/doc/vacuum.texi @@ -56,6 +56,7 @@ under @file{LICENSES/}. @menu * Overview:: What Vacuum does. +* Installation:: Nix, NixOS, GNU Guix System, Arch, and from source. * Invocation:: Commands and global options. * Interactive TUI:: Keys and the runtime directory picker. * Output Modes:: Human text, JSON envelope, JSONL, and field selection. @@ -85,6 +86,96 @@ Vacuum is conservative by default: it never removes anything without an explicit @option{--apply}, it routes deletions to the trash unless you ask to purge, and it refuses to touch protected system paths. @xref{Safety Model}. +@node Installation +@chapter Installation + +@quotation Warning +Do not run @samp{cargo install vacuum}. An unrelated crate on @uref{https://crates.io, +crates.io} holds that name (a spaceflight command-line tool). Install from the +Vacuum repository, as described below. +@end quotation + +@section Nix and NixOS + +The flake supplies its own Rust toolchain, so nothing is needed on the host +beyond Nix itself with flakes enabled. + +@example +nix run github:Spacecraft-Software/Vacuum +nix profile install github:Spacecraft-Software/Vacuum +nix build github:Spacecraft-Software/Vacuum +@end example + +For a system-wide install, add the flake as an input and enable the NixOS +module it exports: + +@example +@{ + inputs.vacuum.url = "github:Spacecraft-Software/Vacuum"; + + # in your NixOS configuration: + # imports = [ inputs.vacuum.nixosModules.default ]; + # programs.vacuum.enable = true; +@} +@end example + +Equivalently, place the package in @code{environment.systemPackages} +directly: + +@example +environment.systemPackages = + [ inputs.vacuum.packages.$@{pkgs.system@}.default ]; +@end example + +To work on Vacuum itself, @command{nix develop} provides rustc, cargo, clippy, +rustfmt, rust-analyzer, mold, @command{reuse}, and Texinfo. +@command{nix flake check} builds the package and runs the test suite. + +@section GNU Guix System + +The repository ships @file{manifest.scm}, which provides the toolchain without +installing anything on the host. + +@example +git clone https://github.com/Spacecraft-Software/Vacuum +cd Vacuum +guix shell -m manifest.scm -- cargo install --path crates/vacuum-cli +@end example + +This installs the @command{vacuum} binary into @file{~/.cargo/bin}. To obtain +only the development environment: + +@example +guix shell -m manifest.scm +guix shell --pure -m manifest.scm +@end example + +A Guix package definition exists at @file{packaging/guix.scm}, but it is not +yet buildable. Guix's @code{cargo-build-system} resolves dependencies through +@code{#:cargo-inputs} rather than from @file{Cargo.lock}, and this workspace +locks 191 crates; that list is generated with @samp{guix import crate -r +vacuum} at release time. Until then, use the @command{guix shell} route above. + +@section Arch Linux + +A @file{PKGBUILD} is provided: + +@example +cd packaging && makepkg -si +@end example + +@section From source + +Vacuum requires Rust edition 2024 with a minimum supported Rust version of +1.85. + +@example +cargo install --path crates/vacuum-cli --force +@end example + +The @option{--force} flag replaces any previously installed @command{vacuum} +binary. + @node Invocation @chapter Invocation diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..5825cc2 --- /dev/null +++ b/flake.lock @@ -0,0 +1,61 @@ +{ + "nodes": { + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1731533236, + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1785967620, + "narHash": "sha256-IItrdb7Puk05RqOBWZYFC5X6Wl1sJmCfh5MWVHw5iMM=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "b7c2ada94fe99c15b0dbcf4d11fd7850b957a436", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "flake-utils": "flake-utils", + "nixpkgs": "nixpkgs" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..fe1704f --- /dev/null +++ b/flake.nix @@ -0,0 +1,127 @@ +# SPDX-FileCopyrightText: 2026 Mohamed Hammad +# SPDX-License-Identifier: GPL-3.0-or-later +# +# Nix flake for Vacuum --- fast, safe disk-space recovery for the terminal. +# +# Usage: +# nix run . # run vacuum without installing it +# nix build # build; result/bin/vacuum +# nix profile install . # install for the current user +# nix develop # development shell +# nix develop -c cargo test --workspace +# nix flake check # build + test suite +# +# The package is defined once, in packaging/default.nix (Standard section 5.5); +# this flake passes `src = self` so it builds the checkout rather than a tagged +# tarball. That matters today: no release tag exists yet, so the fetchFromGitHub +# path in packaging/default.nix cannot resolve. +{ + description = "Vacuum --- fast, safe disk-space recovery for the terminal (CLI + TUI)"; + + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + flake-utils.url = "github:numtide/flake-utils"; + }; + + outputs = { self, nixpkgs, flake-utils }: + let + # System-independent: the NixOS module resolves the package from the + # host's own pkgs, so it works regardless of the flake's eval system. + nixosModules.default = { config, lib, pkgs, ... }: + let cfg = config.programs.vacuum; + in { + options.programs.vacuum = { + enable = lib.mkEnableOption "Vacuum, a disk-space recovery CLI and TUI"; + package = lib.mkOption { + type = lib.types.package; + default = pkgs.callPackage ./packaging/default.nix { + src = self; + version = "0.1.0-git"; + }; + defaultText = lib.literalMD "the `vacuum` package from this flake"; + description = "The Vacuum package to install."; + }; + }; + + config = lib.mkIf cfg.enable { + environment.systemPackages = [ cfg.package ]; + }; + }; + in + { + inherit nixosModules; + } + // flake-utils.lib.eachDefaultSystem (system: + let + pkgs = nixpkgs.legacyPackages.${system}; + + vacuum = pkgs.callPackage ./packaging/default.nix { + src = self; + # Not a release: mark it so `vacuum --version` never claims to be the + # tagged 0.1.0 when it is really whatever is checked out. + version = "0.1.0-git"; + }; + + common = with pkgs; [ + # The Rust toolchain from nixpkgs rather than through rustup. + # + # rustup would honour rust-toolchain.toml, but it downloads and stores + # a second complete toolchain (~1.5 GB) beside the one nixpkgs already + # has. nixpkgs' rustc is well past this workspace's 1.85 minimum, so + # the pin buys nothing here and costs real disk. rust-toolchain.toml + # stays in the tree for contributors who build outside Nix; inside + # this shell it is simply not consulted. + rustc + cargo + clippy + rustfmt + rust-analyzer + + gcc # mimalloc's -sys crate compiles C. + pkg-config + mold # Standard section 3.2.1: the linker LTO wants on NixOS. + + reuse # section 4.3 --- `reuse lint` must pass. + texinfo # section 8 --- make info, make html. + + git + gnumake + ]; + in + { + packages.default = vacuum; + packages.vacuum = vacuum; + + apps.default = { + type = "app"; + program = "${vacuum}/bin/vacuum"; + }; + + # `nix flake check` builds the package, which runs `cargo test` as part + # of buildRustPackage's check phase. + checks.default = vacuum; + + devShells.default = pkgs.mkShell { + name = "vacuum-dev"; + nativeBuildInputs = common; + shellHook = '' + echo "vacuum dev shell." + echo " cargo test --workspace tests" + echo " cargo clippy --workspace --all-targets -- -D warnings" + echo " cargo fmt --all -- --check formatting" + echo " reuse lint section 4.3 gate" + echo " make info Texinfo manual" + echo " nix develop .#docs + texi2pdf" + ''; + }; + + # `make pdf` only. Standard section 8 wants all three formats, but only + # at release time --- TeX Live is hundreds of megabytes that someone + # editing a cleaner never touches. + devShells.docs = pkgs.mkShell { + name = "vacuum-docs"; + nativeBuildInputs = common ++ [ pkgs.texliveSmall ]; + }; + } + ); +} diff --git a/manifest.scm b/manifest.scm new file mode 100644 index 0000000..998eea0 --- /dev/null +++ b/manifest.scm @@ -0,0 +1,31 @@ +;;; SPDX-FileCopyrightText: 2026 Mohamed Hammad +;;; SPDX-License-Identifier: GPL-3.0-or-later +;;; +;;; Vacuum --- GNU Guix development environment. +;;; +;;; The Guix counterpart of the flake's devShell: everything needed to build, +;;; test, and lint the workspace, without installing anything on the host. +;;; +;;; guix shell -m manifest.scm # enter the environment +;;; guix shell -m manifest.scm -- cargo test --workspace +;;; guix shell --pure -m manifest.scm # ignore the ambient environment +;;; +;;; Packages are named by specification rather than imported from their defining +;;; modules, so this file does not have to track which `gnu/packages/*.scm' each +;;; one currently lives in. `rust:cargo' selects the "cargo" output of the rust +;;; package, which is where Guix puts the cargo binary. +;;; +;;; The installable package definition lives in packaging/guix.scm +;;; (Standard section 5.5); this file is only about having a toolchain to hand. + +(use-modules (gnu packages)) + +(specifications->manifest + (list "rust" ; rustc + "rust:cargo" ; cargo lives in the rust package's "cargo" output + "gcc-toolchain" ; mimalloc's -sys crate compiles C + "mold" ; Standard section 3.2.1: the linker LTO wants + "texinfo" ; section 8 --- make info, make html + "reuse" ; section 4.3 --- `reuse lint` must pass + "make" + "git")) diff --git a/packaging/default.nix b/packaging/default.nix index 4e5c77c..5ef4a83 100644 --- a/packaging/default.nix +++ b/packaging/default.nix @@ -1,48 +1,75 @@ # SPDX-FileCopyrightText: 2026 Mohamed Hammad # SPDX-License-Identifier: GPL-3.0-or-later # -# Nix derivation for Vacuum. Build with: -# nix-build packaging/default.nix +# Nix derivation for Vacuum (Standard section 5.5). +# +# Two ways in: +# +# nix build # via flake.nix, builds this checkout +# nix-build packaging/default.nix # release build from the tagged tarball +# +# The flake passes `src = self`, which skips the fetchFromGitHub path entirely. +# That is what makes a from-checkout build work before a release is tagged. +# +# Dependencies are taken from the committed Cargo.lock rather than a vendor +# hash, so there is no `cargoHash` to regenerate on every dependency bump. { lib , rustPlatform , fetchFromGitHub , texinfo -, installShellFiles ? null + # Override to build a working tree (the flake does). When null, the tagged + # release tarball is fetched instead. +, src ? null +, version ? "0.1.0" }: -rustPlatform.buildRustPackage rec { +rustPlatform.buildRustPackage { pname = "vacuum"; - version = "0.1.0"; - - src = fetchFromGitHub { - owner = "Spacecraft-Software"; - repo = "Vacuum"; - rev = "v${version}"; - # TODO(release): replace with the real hash before tagging v${version}. - hash = lib.fakeHash; - }; + inherit version; + + src = + if src != null then + src + else + fetchFromGitHub { + owner = "Spacecraft-Software"; + repo = "Vacuum"; + rev = "v${version}"; + # TODO(release): replace with the real hash before tagging v0.1.0. + # Until that tag exists this path cannot build; use the flake + # (`nix build`), which builds the checkout directly. + hash = lib.fakeHash; + }; - # TODO(release): set to the committed Cargo.lock hash before tagging. - cargoHash = lib.fakeHash; + # The lockfile is committed, so Nix can vendor from it directly. `outputHashes` + # stays empty because every dependency comes from crates.io — no git deps. + cargoLock = { + lockFile = ../Cargo.lock; + }; nativeBuildInputs = [ texinfo ]; + # Only the CLI crate produces a binary; the rest are libraries it links. cargoBuildFlags = [ "-p" "vacuum-cli" ]; + # Standard section 8: the Texinfo manual ships with the package. It is + # generated, not committed, so it is built here. postBuild = '' make info ''; postInstall = '' install -Dm644 doc/vacuum.info "$out/share/info/vacuum.info" + install -Dm644 README.md "$out/share/doc/vacuum/README.md" + install -Dm644 NOTICE.md "$out/share/doc/vacuum/NOTICE.md" ''; - meta = with lib; { + meta = { description = "Fast, safe disk-space recovery for the terminal (CLI + TUI)"; homepage = "https://Vacuum.SpacecraftSoftware.org/"; - license = licenses.gpl3Plus; - maintainers = [ "Mohamed Hammad " ]; + license = lib.licenses.gpl3Plus; mainProgram = "vacuum"; + platforms = lib.platforms.unix; }; } diff --git a/packaging/guix.scm b/packaging/guix.scm index c553579..e1f7e35 100644 --- a/packaging/guix.scm +++ b/packaging/guix.scm @@ -1,13 +1,33 @@ ;;; SPDX-FileCopyrightText: 2026 Mohamed Hammad ;;; SPDX-License-Identifier: GPL-3.0-or-later ;;; -;;; GNU Guix package definition for Vacuum. +;;; Vacuum --- GNU Guix package definition (Standard section 5.5). +;;; ;;; Build with: guix build -f packaging/guix.scm +;;; +;;; RELEASE NOTE: before pushing a release tag, bump `version', replace the +;;; base32 placeholder in the `origin' with the real tarball hash, and populate +;;; #:cargo-inputs from the committed Cargo.lock. A placeholder hash must never +;;; reach a tagged release. +;;; +;;; STATUS: cargo-build-system resolves every dependency through #:cargo-inputs +;;; rather than from Cargo.lock, and this workspace has 191 locked crates. That +;;; list is generated, not hand-written: +;;; +;;; guix import crate -r vacuum +;;; +;;; Until it is generated and pasted in below, this definition will not build +;;; offline. For a working install from a checkout today, see the "GNU Guix +;;; System" section of README.md, which uses `guix shell' plus `cargo install'. -(use-modules (guix packages) - (guix download) - (guix build-system cargo) - ((guix licenses) #:prefix license:)) +(define-module (spacecraft vacuum) + #:use-module (guix packages) + #:use-module (guix download) + #:use-module (guix build-system cargo) + #:use-module (guix utils) + #:use-module ((guix licenses) #:prefix license:) + #:use-module (gnu packages crates-io) + #:use-module (gnu packages texinfo)) (define-public vacuum (package @@ -25,15 +45,48 @@ (base32 "0000000000000000000000000000000000000000000000000000")))) (build-system cargo-build-system) (arguments - (list #:install-source? #f)) - (native-inputs (list)) ; texinfo added when the manual is installed + (list + #:install-source? #f + ;; Only the CLI crate produces a binary; the others are its libraries. + #:cargo-build-flags '("--release" "-p" "vacuum-cli") + ;; TODO(release): populate from `guix import crate -r vacuum'. + #:cargo-inputs '() + #:cargo-development-inputs '() + #:phases + #~(modify-phases %standard-phases + ;; Standard section 8.6: the Texinfo manual is generated, not + ;; committed, so it is built here and registered with install-info. + (add-after 'build 'build-info-manual + (lambda _ + (invoke "makeinfo" "--no-split" + "doc/vacuum.texi" "-o" "doc/vacuum.info"))) + (add-after 'install 'install-info-manual + (lambda* (#:key outputs #:allow-other-keys) + (let* ((out (assoc-ref outputs "out")) + (info (string-append out "/share/info"))) + (mkdir-p info) + (install-file "doc/vacuum.info" info)))) + (add-after 'install 'install-docs + (lambda* (#:key outputs #:allow-other-keys) + (let* ((out (assoc-ref outputs "out")) + (doc (string-append out "/share/doc/vacuum"))) + (mkdir-p doc) + (install-file "README.md" doc) + (install-file "NOTICE.md" doc) + (install-file "LICENSE" doc))))))) + ;; No linked system libraries: the dependency tree needs only a C compiler + ;; (for mimalloc's -sys crate), which cargo-build-system already provides. + (native-inputs (list texinfo)) (home-page "https://Vacuum.SpacecraftSoftware.org/") (synopsis "Fast, safe disk-space recovery for the terminal (CLI + TUI)") (description "Vacuum scans disk usage in parallel, groups reclaimable space into clear categories (regenerable build artifacts, package-manager garbage, application -caches, and large files), and removes what you choose. It is dry-run by -default and sends deletions to the trash unless permanent removal is requested.") +caches, stale temporary files, and large files), and removes what you choose. +It is dry-run by default and sends deletions to the trash unless permanent +removal is requested. Operations needing root, such as a system-wide Nix +garbage collection, are printed as @command{sudo} lines for you to run rather +than escalated automatically.") (license license:gpl3+))) vacuum