cmprss 0.4.0

A compression multi-tool for the command line.
{
  description = "A compression multi-tool for the command line.";

  inputs = {
    nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";

    crane = {
      url = "github:ipetkov/crane";
    };

    fenix = {
      # Needed because rust-overlay, normally used by crane, doesn't have llvm-tools for coverage
      url = "github:nix-community/fenix";
      inputs.nixpkgs.follows = "nixpkgs";
      inputs.rust-analyzer-src.follows = "";
    };

    # Flake helper for better organization with modules.
    flake-parts = {
      url = "github:hercules-ci/flake-parts";
      inputs.nixpkgs-lib.follows = "nixpkgs";
    };

    # For creating a universal `nix fmt`
    treefmt-nix = {
      url = "github:numtide/treefmt-nix";
      inputs.nixpkgs.follows = "nixpkgs";
    };
  };

  outputs = inputs @ {
    self,
    flake-parts,
    ...
  }:
    flake-parts.lib.mkFlake {inherit inputs;} {
      systems = [
        "aarch64-darwin"
        "aarch64-linux"
        "x86_64-darwin"
        "x86_64-linux"
      ];

      imports = [
        flake-parts.flakeModules.easyOverlay
        inputs.treefmt-nix.flakeModule
      ];

      perSystem = {
        config,
        system,
        pkgs,
        ...
      }: let
        # Use the stable rust tools from fenix
        fenixStable = inputs.fenix.packages.${system}.stable;
        rustSrc = fenixStable.rust-src;
        toolChain = fenixStable.completeToolchain;

        # Use the toolchain with the crane helper functions
        craneLib = (inputs.crane.mkLib pkgs).overrideToolchain toolChain;

        # Common arguments for mkCargoDerivation, a helper for the crane functions
        # Arguments can be included here even if they aren't used, but we only
        # place them here if they would otherwise show up in multiple places
        commonArgs = {
          inherit cargoArtifacts;
          # Clean the src to only have the Rust-relevant files
          src = craneLib.cleanCargoSource ./.;
          strictDeps = true;
          nativeBuildInputs = [
            pkgs.pkg-config
            pkgs.gcc
          ];
        };

        # Build only the cargo dependencies so we can cache them all when running in CI
        cargoArtifacts = craneLib.buildDepsOnly commonArgs;

        # Build the actual crate itself, reusing the cargoArtifacts
        cmprss = craneLib.buildPackage (commonArgs
          // {
            doCheck = false; # Tests are run as a separate build with nextest
            meta.mainProgram = "cmprss";
            nativeBuildInputs = commonArgs.nativeBuildInputs ++ [pkgs.installShellFiles];
            postInstall = pkgs.lib.optionalString (pkgs.stdenv.buildPlatform.canExecute pkgs.stdenv.hostPlatform) ''
              installShellCompletion --cmd cmprss \
                --bash <($out/bin/cmprss completions bash) \
                --fish <($out/bin/cmprss completions fish) \
                --zsh <($out/bin/cmprss completions zsh)
            '';
          });

        # Fully static musl build (Linux only)
        staticMusl = pkgs.lib.optionalAttrs pkgs.stdenv.isLinux (
          let
            muslTarget =
              if system == "x86_64-linux"
              then "x86_64-unknown-linux-musl"
              else "aarch64-unknown-linux-musl";

            muslToolchain = inputs.fenix.packages.${system}.combine [
              fenixStable.cargo
              fenixStable.rustc
              inputs.fenix.packages.${system}.targets.${muslTarget}.stable.rust-std
            ];

            craneLibMusl = (inputs.crane.mkLib pkgs).overrideToolchain muslToolchain;

            musl-cc = pkgs.pkgsStatic.stdenv.cc;

            staticArgs = {
              src = craneLibMusl.cleanCargoSource ./.;
              strictDeps = true;
              CARGO_BUILD_TARGET = muslTarget;
              CARGO_BUILD_RUSTFLAGS = "-C target-feature=+crt-static";
              TARGET_CC = "${musl-cc}/bin/${musl-cc.targetPrefix}cc";
              HOST_CC = "${pkgs.stdenv.cc}/bin/cc";
              nativeBuildInputs = [
                musl-cc
                pkgs.pkg-config
              ];
            };

            cargoArtifactsStatic = craneLibMusl.buildDepsOnly staticArgs;
          in {
            cmprss-static = craneLibMusl.buildPackage (staticArgs
              // {
                cargoArtifacts = cargoArtifactsStatic;
                doCheck = false;
                meta.mainProgram = "cmprss";
                nativeBuildInputs = staticArgs.nativeBuildInputs ++ [pkgs.installShellFiles];
                postInstall = pkgs.lib.optionalString (pkgs.stdenv.buildPlatform.canExecute pkgs.stdenv.hostPlatform) ''
                  installShellCompletion --cmd cmprss \
                    --bash <($out/bin/cmprss completions bash) \
                    --fish <($out/bin/cmprss completions fish) \
                    --zsh <($out/bin/cmprss completions zsh)
                '';
              });
          }
        );
        # Source filtered to specific file extensions for lightweight linter checks
        cleanSrc = pkgs.lib.cleanSource ./.;
        sourceWithExts = exts:
          pkgs.lib.cleanSourceWith {
            src = cleanSrc;
            filter = path: type:
              (type == "directory")
              || (pkgs.lib.any (ext: pkgs.lib.hasSuffix ".${ext}" path) exts);
          };

        mkSimpleLinter = {
          name,
          packages ? [],
          src ? cleanSrc,
          command,
        }:
          pkgs.runCommand "lint-${name}" {
            nativeBuildInputs = packages;
            inherit src;
          } ''
            cd $src
            ${command}
            mkdir -p $out
          '';

        linters = {
          statix = mkSimpleLinter {
            name = "statix";
            packages = [pkgs.statix];
            src = sourceWithExts ["nix"];
            command = "statix check .";
          };
          deadnix = mkSimpleLinter {
            name = "deadnix";
            packages = [pkgs.deadnix];
            src = sourceWithExts ["nix"];
            command = "deadnix --fail .";
          };
          shellcheck = mkSimpleLinter {
            name = "shellcheck";
            packages = [pkgs.shellcheck pkgs.findutils];
            src = sourceWithExts ["sh"];
            command = ''find . -name "*.sh" -type f -exec shellcheck {} +'';
          };
          actionlint = mkSimpleLinter {
            name = "actionlint";
            packages = [pkgs.actionlint pkgs.shellcheck pkgs.findutils];
            src = pkgs.lib.cleanSourceWith {
              src = cleanSrc;
              filter = path: type:
                (type == "directory")
                || (pkgs.lib.hasSuffix ".yml" path && pkgs.lib.hasInfix ".github" path);
            };
            command = ''find .github/workflows -name "*.yml" -exec actionlint {} +'';
          };
        };
      in {
        packages =
          {
            default = cmprss;
            inherit cmprss;

            # Check code coverage with tarpaulin
            coverage = craneLib.cargoTarpaulin (commonArgs
              // {
                # Use lcov output as thats far more widely supported
                cargoTarpaulinExtraArgs = "--skip-clean --include-tests --output-dir $out --out lcov";
              });

            # Run clippy (and deny all warnings) on the crate source
            clippy = craneLib.cargoClippy (commonArgs
              // {
                cargoClippyExtraArgs = "--all-targets -- --deny warnings";
              });

            # Check docs build successfully
            doc = craneLib.cargoDoc commonArgs;

            # Check formatting
            fmt = craneLib.cargoFmt commonArgs;

            # Run tests with cargo-nextest
            test = craneLib.cargoNextest commonArgs;

            # Audit dependencies, check licenses, and detect duplicate crates
            deny = craneLib.cargoDeny (commonArgs
              // {
                # advisories excluded: needs network access (blocked by nix sandbox)
                cargoDenyChecks = "bans licenses sources";
              });
          }
          // staticMusl;

        checks =
          {
            inherit cmprss;
            # Build almost every package in checks, with exceptions:
            # - coverage: It requires a full rebuild, and only needs to be run occasionally
            inherit (self.packages.${system}) clippy doc fmt test deny;
          }
          // linters;

        # This also sets up `nix fmt` to run all formatters
        treefmt = {
          projectRootFile = "./flake.nix";
          programs = {
            alejandra.enable = true;
            prettier.enable = true;
            rustfmt.enable = true;
            shfmt.enable = true;
            typos = {
              enable = true;
              configFile = "./.config/typos.toml";
            };
          };
        };

        apps = rec {
          default = cmprss;
          cmprss.program = self.packages.${system}.cmprss;
        };

        overlayAttrs = {
          inherit (config.packages) cmprss;
        };

        devShells.default = pkgs.mkShell {
          name = "cmprss";
          shellHook = ''
            echo ---------------------
            just --list
            echo ---------------------
          '';

          # Include the packages from the defined checks and packages
          # Installs the full cargo toolchain and the extra tools, e.g. cargo-tarpaulin.
          inputsFrom =
            (builtins.attrValues self.checks.${system})
            ++ (builtins.attrValues self.packages.${system});

          # Extra inputs can be added here
          packages = with pkgs; [
            act # For running Github Actions locally
            alejandra
            actionlint
            deadnix
            git-cliff
            gum # Pretty printing in scripts
            just
            nodePackages.prettier
            shellcheck
            statix
            typos

            # For running tests
            diffutils

            # Official tools
            brotli
            bzip2
            gnutar
            gzip
            lz4
            p7zip
            snzip
            unzip
            xz
            zip
            zstd
          ];

          # Many tools read this to find the sources for rust stdlib
          RUST_SRC_PATH = "${rustSrc}/lib/rustlib/src/rust/library";
        };
      };
    };
}