A flake framework for a tree published as one or many repos
README.md

Nix Workspace Rust #

The Rust build for nix-workspace: a plain-nixpkgs build of a crate, a devshell carrying the pre-commit hooks the monorepo holds it to, and a formatter.

Its own flake, because the framework knows no language, but a directory of the same repo rather than a repo of its own: one history, and a module that cannot drift from the framework it extends. Declaring this input is the whole of taking it - nix-workspace imports the workspaceModule any input exports - and a tree that does not declare it carries nothing Rust-shaped.

{
  description = "A GNU sed-compatible stream editor written in Rust";

  inputs = {
    workspace.url = "git+https://tangled.org/overby.me/nix-workspace";
    rust = {
      url = "git+https://tangled.org/overby.me/nix-workspace?dir=modules/rust";
      inputs.workspace.follows = "workspace";
    };
  };

  outputs = inputs:
    inputs.workspace {
      inherit inputs;
    };
}

That is the whole file. The crate is read from its own Cargo.toml, so nothing here restates a name, a version or a binary, and the result is packages.default, a checks.packages-default, an overlay entry and a devshell.

workspace follows through, so the framework and this are one flake in the lock rather than two copies of it. git-hooks arrives the same way and is never declared here.

Options #

All under rust.

option default for
pname the crate's own name, else the first workspace member's what to call the derivation; cosmetic, since the output is packages.default
description the flake's own the package's meta.description
root the flake's directory where the Cargo.toml is read from
subdir "" the crate is one level down
nativeBuildInputs [] nixpkgs attribute names of build-time tools
buildInputs [] nixpkgs attribute names of libraries linked against
env _: {} build-time environment, as a function of pkgs
toolchain a rust-toolchain.toml is present take rustc from that file
aliases {} extra names for a binary the crate builds
setupHook null a file installed as nix-support/setup-hook
hooks seven the pre-commit hooks nix develop installs

Build inputs are nixpkgs attribute names rather than packages, because the generator that writes these flakes reads them out of a project list and has no package set to resolve them against.

subdir exists because a crate whose Cargo.toml has a path dependency on a sibling is published as several directories, so that path = "../pcre2" still resolves. Its own crate is then one level down, and the build needs both cargoRoot and buildAndTestSubdir, which do different jobs.

aliases are the names a drop-in replacement also answers to - gawk beside awk, and all thirty-four of PipeWire's tool names for one multicall binary. Cargo builds only what Cargo.toml declares as a [[bin]], so without them a clone ships a binary under none of the names it dispatches on. The target must be a binary cargo built, not another alias, and a target that is not there is an error rather than a dangling symlink.

setupHook is the same gap one step further along: a tool replacing part of stdenv is not usable by being on PATH. pkg-config earns its place by a hook that exports PKG_CONFIG_PATH, and without one a build that lists it gets a binary that finds nothing.

toolchain exists for compiler plugins: they link against rustc's internals, whose API differs between releases, so they need the exact nightly named in their own rust-toolchain.toml rather than whatever rustc nixpkgs ships. Having that file is the request, so it is read rather than declared. What cannot be derived is the rust-overlay overlay that provides rust-bin, which the consuming flake supplies:

outputs = inputs:
  inputs.workspace {
    inherit inputs;
    withOverlays = [inputs.rust-overlay.overlays.default];
  };

An overlay is its own flake, so carrying it here would make every published repo fetch it for the one project that pins a toolchain.

The fine-grained build #

Declaring nix-lib as an input switches the build to that flake's buildCargoProject: one derivation per crate against a committed index, rather than one rustPlatform derivation for the whole tree. Declaring the input is the whole of opting in.

It runs tests as passthru derivations rather than inside the package build, so this adds a checks.tests that builds them - otherwise nix flake check goes green having built none.

aliases and setupHook are not supported there and fail loudly rather than being dropped.

Contributing #

See nix-workspace, whose repo this is a directory of.

It was split out of the framework, which shipped this as its workspaceModules.default and imported it for any call carrying a name. That made the framework demand a package name from every tree that took it, and made it the one input its own module-finding had to skip.