passthru
and finalAttrsIn the Nix ecosystem, we emphasize the purity and isolation of derivations. But once software is compiled, you almost always need companion tooling to operate it: scripts for database migrations, infrastructure provisioning, deployment orchestration, or Software Bill of Materials (SBOM) generation.
The traditional pattern was to define these scripts as separate
packages or bundle them directly into a package’s passthru
attribute using a recursive let ... in block. While the
self-referential let solved the initial problem of scripts
referencing the package and each other, it introduced a subtle defect
when packages were overridden.
Understanding why the recursive let fails under
.overrideAttrs, and how modern Nix solves it with open
recursion, is key to writing robust, maintainable Nix expressions.
Imagine a derivation pkg that builds a web service. You
want to bundle operational scripts alongside it in
passthru: an init script for database setup, a
plan script for infrastructure dry-runs, and a
deploy script that executes the plan and uploads the built
artifact (pkg).
If you try to declare this inside stdenv.mkDerivation
using a naive rec { ... } attribute set, Nix will fail
because rec operates on raw input attributes rather than
evaluated outputs.
To work around this, developers historically defined the package in a
recursive let block, referencing pkg inside
its own passthru definition:
let
pkg = stdenv.mkDerivation {
pname = "my-service";
version = "1.0.0";
# ...
passthru = {
init = pkgs.writeShellScriptBin "init" ''
echo "Initializing database..."
'';
plan = pkgs.writeShellScriptBin "plan" ''
${pkg.init}/bin/init
echo "Planning deployment..."
'';
deploy = pkgs.writeShellScriptBin "deploy" ''
${pkg.plan}/bin/plan
echo "Deploying artifact from ${pkg}..."
'';
};
};
in
pkgThis works for basic use cases: passthru.deploy
correctly bakes in the exact store paths for pkg.plan,
pkg.init, and pkg itself.
The fatal flaw of the self-referential let appears when
a downstream user attempts to override the package using
.overrideAttrs:
myCustomService = myService.overrideAttrs (oldAttrs: {
version = "2.0.0";
src = ./new-source;
});Because let bindings are lexically
closed, the variable pkg inside the
passthru definitions remains permanently bound to the
original, un-overridden derivation.
When myCustomService is evaluated, Nix produces a new
derivation for version 2.0.0, but
myCustomService.passthru.deploy still points back to the
version 1.0.0 store path. The override silently fails to
propagate to the bundled scripts.
finalAttrsTo support open recursion and ensure overrides propagate correctly,
modern Nixpkgs introduced the finalAttrs pattern for
stdenv.mkDerivation. Instead of passing a plain attribute
set, you pass a function receiving finalAttrs:
stdenv.mkDerivation (finalAttrs: {
pname = "my-service";
version = "1.0.0";
# ...
passthru = {
init = pkgs.writeShellScriptBin "init" ''
echo "Initializing database..."
'';
plan = pkgs.writeShellScriptBin "plan" ''
${finalAttrs.passthru.init}/bin/init
echo "Planning deployment..."
'';
deploy = pkgs.writeShellScriptBin "deploy" ''
${finalAttrs.passthru.plan}/bin/plan
echo "Deploying artifact from ${finalAttrs.finalPackage}..."
'';
};
})finalAttrs relies on fixed-point evaluation
(lib.fix). Calling .overrideAttrs re-evaluates
the fixed point, dynamically re-binding
finalAttrs.finalPackage and
finalAttrs.passthru so all script paths remain
synchronized.
While finalAttrs supersedes let inside
stdenv.mkDerivation, the recursive let remains
essential outside derivation definitions, such as linking packages,
apps, and devShells in flake.nix, or structuring
non-derivation attribute sets and private helper functions.
When structuring operational scripts across a codebase:
finalAttrs.passthru for scripts
tightly bound to build outputs (like deployment hooks or automated SBOM
generation).apps.<system> for
clean CLI invocation (nix run .#deploy).devShells with
pkgs.writeShellApplication for workflows requiring secret
decryption, cloud provider credentials, or interactive multi-tool
orchestration.