#2983·devbox

Restore Lix ≥ 2.95 compatibility by replacing builtins.fetchClosure with builtins.appendContext

Author: mikeland73Created Sep 16, 2026Updated Sep 16, 2026

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-closure experimental feature removed, and builtins.fetchClosure with it. Since Lix removed CA derivations most of the inner workings of fetchClosure have 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-closure is 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:

nix
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:

nix
(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 flakes only, pure eval: nix print-dev-env --json returns buildInputs = /nix/store/zq74…-hello-2.12.1, hello on PATH. ✅
  • Substitution parity: appendContext on a cowsay path not in the local store pulled perl + cowsay from cache.nixos.org during eval. ✅
  • Lix 2.95.2 (nixpkgs-unstable#lixPackageSets.latest.lix), using Devbox's exact flags: the original flake fails with attribute 'fetchClosure' missing; the appendContext flake produces the identical dev env. ✅ (Lix also prints warning: unknown experimental feature 'ca-derivations' / 'fetch-closure' — non-fatal.)

mkShell's buildInputs accepts plain strings (checkDependencyList allows string), which the test confirms.

Plan

  1. Replace fetchClosure in internal/shellgen/tmpl/flake.nix.tmpl with builtins.appendContext, keeping the builtins.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.
  2. Delete dead fetchClosureExpr and the IsInBinaryCache branch in glibcPatchFlake.addOutput (internal/shellgen/flake_plan.go). Update the stale comments in internal/shellgen/flake_input.go and internal/devpkg/narinfo_cache.go.
  3. Drop fetch-closure from the experimental-features flags in internal/nix/command.go, internal/nix/nix.go (ExperimentalFlags), and .github/workflows/cli-tests.yaml. Nothing else uses it.
  4. Relax the Lix gate from #2949 in internal/nix/install.go: remove the SupportsFetchClosure check, LixVersionWithoutFetchClosure, and their tests. Keep Info.Implementation / IsLix() — the parsing is correct and useful for future diagnostics.
  5. Test: unit tests for the generated flake (assert appendContext appears, fetchClosure doesn't), plus manual runs with Nix and Lix 2.95.2 as above. CI's nix-installer job exercises the Nix path.
  6. Optional follow-up (separate PR): ca-derivations has 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.