Restore Lix ≥ 2.95 compatibility by replacing builtins.fetchClosure with builtins.appendContext
Summary
Lix 2.95 removed builtins.fetchClosure (see #2946). #2949 added an up-front check that refuses to run on Lix ≥ 2.95 with a clear error. This issue tracks actually restoring compatibility by replacing fetchClosure in the generated flake with builtins.appendContext, which needs no experimental feature and works on both Nix and Lix.
Background
Why Lix dropped it
From the Lix 2.95 "Kakigōri" release notes:
fetch-closureexperimental feature removed, andbuiltins.fetchClosurewith it. Since Lix removed CA derivations most of the inner workings offetchClosurehave become inaccessible, and the remainder was largely not an improvement over existing features (such as importing store derivation). We've removed it to lighten the maintenance burden.
It's a knock-on effect of Lix removing content-addressed derivations. The input-addressed mode (inputAddressed = true, the only mode Devbox uses) was considered redundant. No migration path or transition period was provided.
Does this affect upstream Nix?
Not currently, and there's no indication it will:
fetch-closureis still an experimental feature in the Nix 2.34 manual; upstream (2.35.x) hasn't removed it.- Upstream still has
ca-derivations, so Lix's rationale doesn't apply. - NixOS/nix#8963 (stabilize
fetchClosure) is open with maintainers leaning toward stabilization; nobody is proposing removal. Determinate Nix tracks upstream.
It is still an experimental feature though, so upstream is free to change it — one more reason to move to a stable primitive.
How Devbox uses fetchClosure
internal/shellgen/tmpl/flake.nix.tmpl — for every package whose store path is in cache.nixos.org, Devbox pre-builds the path (nix build /nix/store/...), then the generated flake references it via:
builtins.fetchClosure {
fromStore = "https://cache.nixos.org";
fromPath = "/nix/store/...";
inputAddressed = true;
}The template comment already calls this a hack. All we actually need is "a string with store-path context that Nix will ensurePath", so the flake doesn't have to download/evaluate that package's nixpkgs revision.
fetchClosureExpr in internal/shellgen/flake_plan.go is dead code: addOutput only runs for Patch packages, and IsInBinaryCache is always false for those. (And patchGlibc accesses pkg.name / pkg.buildInputs, so it would break if it were ever reached.)
Alternatives considered
| Option | Verdict |
|---|---|
builtins.storePath |
❌ Not allowed in pure eval on either Nix or Lix (Lix tracking issue #402, NixOS/nix#5868, both unresolved). Would require --impure on print-dev-env, which Devbox deliberately keeps off (featureflag.ImpurePrintDevEnv) because it disables eval caching and leaks env. |
Path literal / path: flake input |
❌ Copies the path into a new store path, losing the closure's references. |
Reference nixpkgs-<rev>.legacyPackages... (the non-cached path) |
❌ Works, but throws away the binary-cache optimization: every cached package forces a nixpkgs tarball download + eval. |
builtins.appendContext |
✅ builtins.appendContext "/nix/store/x" { "/nix/store/x" = { path = true; }; } yields a string with opaque path context, has no pure-eval restriction, calls store->ensurePath() (substitutes from configured substituters if missing — same behavior as fetchClosure), needs no experimental feature, and has existed since Nix 2.0. |
Verification
Built Devbox from main, devbox add [email protected], took the generated flake, and swapped the fetchClosure block for:
(builtins.trace "downloading [email protected]" (builtins.appendContext "/nix/store/zq74har4ak9dxi7z5k7id9v6sgm2kb8w-hello-2.12.1" {
"/nix/store/zq74har4ak9dxi7z5k7id9v6sgm2kb8w-hello-2.12.1" = { path = true; };
}))- Nix 2.30.2, experimental features
nix-command flakesonly, pure eval:nix print-dev-env --jsonreturnsbuildInputs = /nix/store/zq74…-hello-2.12.1, hello onPATH. ✅ - Substitution parity:
appendContexton acowsaypath not in the local store pulledperl+cowsayfrom cache.nixos.org during eval. ✅ - Lix 2.95.2 (
nixpkgs-unstable#lixPackageSets.latest.lix), using Devbox's exact flags: the original flake fails withattribute 'fetchClosure' missing; theappendContextflake produces the identical dev env. ✅ (Lix also printswarning: unknown experimental feature 'ca-derivations'/'fetch-closure'— non-fatal.)
mkShell's buildInputs accepts plain strings (checkDependencyList allows string), which the test confirms.
Plan
- Replace
fetchClosureininternal/shellgen/tmpl/flake.nix.tmplwithbuiltins.appendContext, keeping thebuiltins.trace "downloading …"wrapper and replacing the HACK comment with one explaining the context trick and that the path is pre-built by Devbox (and substituted on demand otherwise). This is the whole fix. - Delete dead
fetchClosureExprand theIsInBinaryCachebranch inglibcPatchFlake.addOutput(internal/shellgen/flake_plan.go). Update the stale comments ininternal/shellgen/flake_input.goandinternal/devpkg/narinfo_cache.go. - Drop
fetch-closurefrom the experimental-features flags ininternal/nix/command.go,internal/nix/nix.go(ExperimentalFlags), and.github/workflows/cli-tests.yaml. Nothing else uses it. - Relax the Lix gate from #2949 in
internal/nix/install.go: remove theSupportsFetchClosurecheck,LixVersionWithoutFetchClosure, and their tests. KeepInfo.Implementation/IsLix()— the parsing is correct and useful for future diagnostics. - Test: unit tests for the generated flake (assert
appendContextappears,fetchClosuredoesn't), plus manual runs with Nix and Lix 2.95.2 as above. CI's nix-installer job exercises the Nix path. - Optional follow-up (separate PR):
ca-derivationshas been passed since 2023 (#456) with no in-repo consumer; on Lix it emits a warning on every command. Worth auditing whether it can be dropped.
Net effect: Devbox works on Lix ≥ 2.95, relies on one fewer experimental feature on upstream Nix, and keeps the binary-cache fast path.
Source: jetify-com/devbox