rucola-notes 0.9.0

Terminal-based markdown note manager.
{
  description = "Rucola - terminal-based markdown note manager";

  inputs = {
    nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
    flake-parts.url = "github:hercules-ci/flake-parts";
    systems.url = "github:nix-systems/default";

    rust-overlay = {
      url = "github:oxalica/rust-overlay";
      inputs.nixpkgs.follows = "nixpkgs";
    };

    # Modern Rust build framework with split dependency / workspace caching.
    crane.url = "github:ipetkov/crane";

    # Unified formatter integration (runs on Nix files only here - the Rust
    # toolchain handles Rust formatting through `cargo fmt` in the dev shell).
    treefmt-nix = {
      url = "github:numtide/treefmt-nix";
      inputs.nixpkgs.follows = "nixpkgs";
    };

    # Security advisory database consumed by the cargo-audit check.
    advisory-db = {
      url = "github:rustsec/advisory-db";
      flake = false;
    };
  };

  outputs = inputs@{ self, flake-parts, ... }:
    flake-parts.lib.mkFlake { inherit inputs; } {
      systems = import inputs.systems;

      imports = [
        inputs.treefmt-nix.flakeModule
      ];

      # Flake-wide outputs. Exposing an overlay lets downstream flakes pull
      # rucola in via `overlays = [ rucola.overlays.default ]`.
      flake = {
        overlays.default = _final: prev: {
          rucola = self.packages.${prev.stdenv.hostPlatform.system}.rucola;
        };
      };

      perSystem = { config, system, lib, ... }:
        let
          pkgs = import inputs.nixpkgs {
            inherit system;
            overlays = [ inputs.rust-overlay.overlays.default ];
          };

          # Toolchain exactly pinned. `nix flake update` won't silently bump
          # the Rust compiler - only an explicit edit of this version does.
          rustToolchain = pkgs.rust-bin.stable."1.94.1".default.override {
            extensions = [ "rust-src" "rust-analyzer" "clippy" "rustfmt" ];
          };

          # Crane bound to the pinned toolchain. All crane-produced
          # derivations (buildPackage, cargoClippy, cargoNextest, ...) share
          # this single rustc/cargo.
          craneLib = (inputs.crane.mkLib pkgs).overrideToolchain rustToolchain;

          cargoToml = lib.importTOML ./Cargo.toml;

          # Minimal source: only the inputs cargo actually needs. Editing
          # README.md, CI configs or the flake itself no longer busts
          # rucola's build cache.
          src = lib.fileset.toSource {
            root = ./.;
            fileset = lib.fileset.unions [
              ./Cargo.toml
              ./Cargo.lock
              ./build.rs
              ./src
              ./tests
              ./default-config
            ];
          };

          # Native tools + C libraries rucola's dependency graph links
          # against. git2 is the driver; the rest of the tree is pure Rust.
          nativeBuildInputs = [ pkgs.pkg-config ];

          buildInputs = with pkgs;
            [ openssl zlib libgit2 ]
            ++ lib.optionals stdenv.hostPlatform.isDarwin (
              [ libiconv ]
              ++ (with pkgs.darwin.apple_sdk.frameworks; [
                CoreFoundation
                Security
                SystemConfiguration
              ])
            );

          # Shared args for every crane invocation in this flake. Using the
          # same set for buildDepsOnly, buildPackage and every check means
          # the fetched-and-built dependency graph is cached exactly once
          # and reused for linting, testing and the final binary.
          commonArgs = {
            inherit src nativeBuildInputs buildInputs;
            pname = cargoToml.package.name;
            version = cargoToml.package.version;

            strictDeps = true;

            # Prefer system libraries over the copies vendored by the -sys
            # crates. Keeps the closure small and inherits upstream patches.
            LIBGIT2_NO_VENDOR = "1";
            OPENSSL_NO_VENDOR = "1";
          };

          # Build only the dependency graph. This is the expensive step
          # (~30s of compile time) and is cached independently from the
          # workspace itself.
          cargoArtifacts = craneLib.buildDepsOnly commonArgs;

          rucola = craneLib.buildPackage (commonArgs // {
            inherit cargoArtifacts;

            # Tests run in a dedicated nextest check below. Skipping them
            # here keeps `nix build` focused on producing the binary.
            doCheck = false;

            meta = {
              description = lib.strings.removeSuffix "." cargoToml.package.description;
              homepage = cargoToml.package.homepage;
              changelog = "${cargoToml.package.homepage}/blob/main/CHANGELOG.md";
              license = lib.licenses.gpl3Only;
              mainProgram = "rucola";
              platforms = lib.platforms.unix;
            };
          });
        in
        {
          # Share the overlay-augmented pkgs with sibling flake-parts
          # modules (treefmt-nix) so there is exactly one nixpkgs instance.
          _module.args.pkgs = pkgs;

          packages = {
            default = rucola;
            rucola = rucola;
          };

          apps.default = {
            type = "app";
            program = lib.getExe rucola;
          };

          devShells.default = pkgs.mkShell {
            inherit nativeBuildInputs buildInputs;

            packages = with pkgs; [
              rustToolchain
              # Cargo ergonomics
              cargo-edit
              cargo-watch
              cargo-outdated
              cargo-audit
              cargo-nextest
              # Standalone TOML formatter referenced in the comments below
              # (treefmt deliberately doesn't wire it in as a flake check).
              taplo
              # Nix ergonomics
              nil
              config.treefmt.build.wrapper
            ];

            env = {
              LIBGIT2_NO_VENDOR = "1";
              OPENSSL_NO_VENDOR = "1";
              RUST_BACKTRACE = "1";
              RUST_SRC_PATH = "${rustToolchain}/lib/rustlib/src/rust/library";
            };

            shellHook = ''
              echo "rucola dev shell - $(rustc --version)"
            '';
          };

          # `nix flake check` runs the full matrix: package build, strict
          # clippy, the full test suite via nextest, and a RUSTSEC audit
          # against the pinned advisory-db snapshot.
          checks = {
            inherit rucola;

            clippy = craneLib.cargoClippy (commonArgs // {
              inherit cargoArtifacts;
              cargoClippyExtraArgs = "--all-targets --all-features -- -D warnings";
            });

            nextest = craneLib.cargoNextest (commonArgs // {
              inherit cargoArtifacts;
              # test_viewing canonicalises paths that don't exist inside
              # the sandbox; everything else runs in the sandbox cleanly.
              cargoNextestExtraArgs = "--no-fail-fast -E 'not test(=test_viewing)'";
              partitions = 1;
              partitionType = "count";
            });

            audit = craneLib.cargoAudit {
              inherit src;
              inherit (inputs) advisory-db;
            };
          };

          # `nix fmt` formats the Nix files in this repo. Rust and TOML
          # formatting is deliberately not wired in here because it would
          # rewrite upstream-owned source outside this flake's scope -
          # run `cargo fmt` / `taplo fmt` from the dev shell if desired.
          treefmt = {
            projectRootFile = "flake.nix";
            programs.nixpkgs-fmt.enable = true;
          };
        };
    };
}