I'm getting rebuilds all of the time, especially when I change flake.nix

Nix will rebuild a derivation if any of its inputs change, which includes any file contained by the source that is passed in. For example, if the build expression specifies src = ./.; then the crate will be rebuilt when any file changes (including "unrelated" changes to flake.nix)!

There are two main ways to avoid unnecessary builds:

  1. Use a source cleaning function which can omit any files know to not be needed while building the crate (for example, all *.nix sources, flake.lock, and so on). For example cleanCargoSource (see [API docs] for details) implements some good defaults for ignoring irrelevant files which are not needed by cargo.
  2. Another option is to put the crate's source files into its own subdirectory (e.g. ./mycrate) and then set the build expression's source to that subdirectory (e.g. src = ./mycrate;). Then, changes to files outside of that directory will be ignored and will not cause a rebuild

In certain (especially non-trivial) crane-based workflows, it's possible that a change to a given file might trigger rebuilds of certain seemingly unrelated derivations. This is most often caused by a subtle bug introducing undesired derivation inputs.

Debugging with nix-diff

An efficient way to debug such problems is to use nix-diff to compare the derivation build plans:

# nix-diff does not support direct flake-urls so we'll need
# to get the actual derivation name
nix show-derivation .#affectedOutput | nix run nixpkgs#jq -- -r 'keys[0]' > before_drv
echo >> ./file/triggering/rebuild # cause a rebuild
nix show-derivation .#affectedOutput | nix run nixpkgs#jq -- -r 'keys[0]' > after_drv
nix run nixpkgs#nix-diff "$(cat before_drv)" "$(cat after_drv)"

Debugging with just nix

Another way to debug such problems is to use nix derivation show -r to compare the derivation build plans:

nix derivation show -r .#affectedOutput > before
echo >> ./file/triggering/rebuild # cause a rebuild
nix derivation show -r .#affectedOutput > after
diff -u before after

The difference in the highest-level derivation should point to a direct cause of the rebuild (possibly a different lower-level input derivation which can be compared recursively).

I've used a source filter but cargo is still rebuilding all dependencies from scratch!

Another source of artifact invalidation is if

  • A different set of dependency crates are being built between derivations
let
  src = ...;
in
craneLib.buildPackage {
  inherit src;

  cargoArtifacts = craneLib.buildDepsOnly {
    inherit src;
    cargoExtraArgs = "-p foo"; # Only build the `foo` crate
  };

  # Oops, we're only building the `bar` crate now
  # any dependency crates used by `bar` but not by `foo`
  # will get built from scratch!
  cargoExtraArgs = "-p bar";
}
  • Another reason could be using different feature flags between derivations, which result in setting different feature flags for dependency crates themselves and causing a rebuild
let
  src = ...;
in
craneLib.buildPackage {
  inherit src;

  cargoArtifacts = craneLib.buildDepsOnly {
    inherit src;
    cargoExtraArgs = "--no-default-features"; # Don't use any workspace features
  };

  # Oops, we're now building with an additional downstream feature flag which
  # needs to build more crates which we do not have cached!
  cargoExtraArgs = "--features feature-which-enables-downstream-feature";
}

If in doubt, double check that the same set of -p/--package and --features/--no-default-features/--all-features flags are used between all buildDepsOnly/cargoBuild/cargoClippy/buildPackage derivations.

Mixing [package] and [workspace] definitions in the top-level Cargo.toml

Another potential pitfall is defining both [package] and [workspace] in the project's top-level Cargo.toml file. Although cargo allows both to be defined, doing so results in cargo only operating on that package by default (unless the --workspace flag is passed in).

Any subsequent derivations which attempt to build with -p another-crate might not have their dependencies fully cached. Our recommendation is to only define [package] in the top-level Cargo.toml if the workspace contains a single crate; otherwise only [workspace] should be defined.