Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion internal/devpkg/narinfo_cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,10 @@ import (
)

// binaryCache is the store from which to fetch this package's binaries.
// It is used as FromStore in builtins.fetchClosure.
// It is used as the substituter when builtins.appendContext triggers
// store->ensurePath() during flake evaluation. Devbox pre-builds each package
// before generating the flake so the path is normally already in the local
// store; if it isn't (e.g. after GC), Nix will fetch it from this cache.
const binaryCache = "https://cache.nixos.org"

// useDefaultOutputs is a special value for the outputName parameter of
Expand Down
2 changes: 1 addition & 1 deletion internal/nix/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import "os"
func init() {
Default.ExtraArgs = Args{
"--extra-experimental-features", "ca-derivations",
"--option", "experimental-features", "nix-command flakes fetch-closure",
"--option", "experimental-features", "nix-command flakes",
}

// Retry commands that fail because of a flaky network, such as a
Expand Down
16 changes: 0 additions & 16 deletions internal/nix/install.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,22 +81,6 @@ func EnsureNixInstalled(ctx context.Context, writer io.Writer, withDaemonFunc fu
)
return
}

// Lix 2.95 removed builtins.fetchClosure, which Devbox relies on to
// install packages from a binary cache. Detect it early and print a
// clear compatibility hint instead of a cryptic
// "attribute 'fetchClosure' missing" error later on.
if info, infoErr := nix.Default.Info(); infoErr == nil && !info.SupportsFetchClosure() {
err = usererr.New(
"Devbox is not compatible with your Nix installation (Lix %s). "+
"Lix removed `builtins.fetchClosure` in version %s, which "+
"Devbox relies on to install packages. Please switch to Nix "+
"(https://nixos.org/download), or downgrade to Lix < %[2]s.\n",
info.Version,
nix.LixVersionWithoutFetchClosure,
)
return
}
}()

if BinaryInstalled() {
Expand Down
2 changes: 1 addition & 1 deletion internal/nix/nix.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ func FlakeNixpkgs(commit string) string {
}

func ExperimentalFlags() []string {
options := []string{"nix-command", "flakes", "fetch-closure"}
options := []string{"nix-command", "flakes"}
return []string{
"--extra-experimental-features", "ca-derivations",
"--option", "experimental-features", strings.Join(options, " "),
Expand Down
4 changes: 3 additions & 1 deletion internal/shellgen/flake_input.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,9 @@ func (f *flakeInput) BuildInputsForSymlinkJoin() ([]*SymlinkJoin, error) {
}

// Skip packages that are already in the binary cache. These will be directly
// included in the buildInputs using `builtins.fetchClosure` of their store paths.
// included in the buildInputs using `builtins.appendContext` of their store
// paths, which causes Nix to substitute them from the binary cache during
// eval if they aren't present locally.
inCache, err := pkg.IsInBinaryCache()
if err != nil {
return nil, err
Expand Down
22 changes: 0 additions & 22 deletions internal/shellgen/flake_plan.go
Original file line number Diff line number Diff line change
Expand Up @@ -171,12 +171,6 @@ func (g *glibcPatchFlake) addOutput(pkg *devpkg.Package) error {
if g.Outputs.Packages == nil {
g.Outputs.Packages = map[string]map[string]string{nix.System(): {}}
}
if cached, err := pkg.IsInBinaryCache(); err == nil && cached {
if expr, err := g.fetchClosureExpr(pkg); err == nil {
g.Outputs.Packages[nix.System()][relAttrPath] = expr
return nil
}
}

inputAttrPath, err := g.inputRelativeAttrPath(pkg)
if err != nil {
Expand Down Expand Up @@ -240,22 +234,6 @@ func (g *glibcPatchFlake) inputRelativeAttrPath(pkg *devpkg.Package) (string, er
return atrrPath, nil
}

// TODO: this only handles the first store path, but we should handle all of them
func (g *glibcPatchFlake) fetchClosureExpr(pkg *devpkg.Package) (string, error) {
storePaths, err := pkg.InputAddressedPaths()
if err != nil {
return "", err
}
if len(storePaths) == 0 {
return "", fmt.Errorf("no store path for package %s", pkg.Raw)
}
return fmt.Sprintf(`builtins.fetchClosure {
fromStore = "%s";
fromPath = "%s";
inputAddressed = true;
}`, "devpkg.BinaryCache", storePaths[0]), nil
}

// copySystemCUDALib searches for the system's libcuda.so shared library and
// copies it in the flake's lib directory.
func (g *glibcPatchFlake) copySystemCUDALib(flakeDir string) error {
Expand Down
22 changes: 12 additions & 10 deletions internal/shellgen/tmpl/flake.nix.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -40,17 +40,19 @@
{{- range $_, $pkg := .Packages }}
{{- range $_, $output := $pkg.GetOutputsWithCache }}
{{ if $output.CacheURI -}}
(builtins.trace "downloading {{ $pkg.Versioned }}" (builtins.fetchClosure {
{{/*
HACK HACK HACK! fetchClosure only supports http(s) caches and not
s3 caches. Until we implement that, we put a fake store here.
Since we pre-build everything, fetchClosure will not actually
fetch anything and just use the local version. This may break
if user somehow removes the local store path.
(builtins.trace "downloading {{ $pkg.Versioned }}" (builtins.appendContext "{{ $pkg.InputAddressedPathForOutput $output.Name }}" {
{{- /*
appendContext attaches a store-path context to the string so
that Nix calls store->ensurePath() during eval, substituting
the path from configured substituters (cache.nixos.org) if it
isn't already present locally. Devbox pre-builds each package
before generating this flake, so the path should usually be
in the local store at this point; if it isn't (e.g. after a
GC), Nix will fetch it automatically. This replaces the prior
builtins.fetchClosure hack, which required the experimental
"fetch-closure" feature and broke on Lix >= 2.95.
*/}}
fromStore = "https://cache.nixos.org";
fromPath = "{{ $pkg.InputAddressedPathForOutput $output.Name }}";
inputAddressed = true;
"{{ $pkg.InputAddressedPathForOutput $output.Name }}" = { path = true; };
}))
{{- end }}
{{- end }}
Expand Down
34 changes: 5 additions & 29 deletions nix/nix.go
Original file line number Diff line number Diff line change
Expand Up @@ -206,12 +206,11 @@ const (
MinVersion = Version2_18
)

// LixVersionWithoutFetchClosure is the first Lix version that removed
// builtins.fetchClosure, which Devbox relies on to install packages from a
// binary cache. Devbox is not compatible with Lix at or above this version.
//
// See https://lix.systems/blog/2026-03-25-lix-2.95-release/.
const LixVersionWithoutFetchClosure = "2.95.0"
// IsLix reports whether the Nix installation is the Lix fork, which identifies
// itself as "Lix, like Nix" in the parenthetical of its "nix --version" output.
func (i Info) IsLix() bool {
return strings.Contains(strings.ToLower(i.Implementation), "lix")
}

// versionRegexp matches the first line of "nix --version" output.
//
Expand Down Expand Up @@ -369,29 +368,6 @@ func (i Info) AtLeast(version string) bool {
return semver.Compare("v"+prerelease, version) >= 0
}

// IsLix reports whether the Nix installation is the Lix fork, which identifies
// itself as "Lix, like Nix" in the parenthetical of its "nix --version" output.
func (i Info) IsLix() bool {
return strings.Contains(strings.ToLower(i.Implementation), "lix")
}

// SupportsFetchClosure reports whether the Nix installation provides
// builtins.fetchClosure, which Devbox relies on to install packages from a
// binary cache. The Lix fork removed fetchClosure in version 2.95, so Devbox is
// not compatible with it (see LixVersionWithoutFetchClosure). When the version
// cannot be determined, this returns true to avoid blocking on a false
// positive.
func (i Info) SupportsFetchClosure() bool {
if !i.IsLix() {
return true
}
// Compare against the lowest possible prerelease of the removal version
// (e.g. "2.95.0-0") so that Lix 2.95 prereleases, which have also dropped
// fetchClosure, are treated as unsupported. A plain "2.95.0" boundary would
// let them through, since semver sorts a prerelease below its release.
return !i.AtLeast(LixVersionWithoutFetchClosure + "-0")
}

// sourceProfileMutex guards against multiple goroutines attempting to source
// the Nix profile scripts concurrently.
var sourceProfileMutex sync.Mutex
Expand Down
54 changes: 0 additions & 54 deletions nix/nix_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -251,60 +251,6 @@ func TestInfoIsLix(t *testing.T) {
}
}

func TestInfoSupportsFetchClosure(t *testing.T) {
cases := []struct {
name string
info Info
want bool
}{
{
name: "nix",
info: Info{Implementation: "Nix", Version: "2.34.6"},
want: true,
},
{
name: "lix before 2.95",
info: Info{Implementation: "Lix, like Nix", Version: "2.94.0"},
want: true,
},
{
name: "lix prerelease before 2.95",
info: Info{Implementation: "Lix, like Nix", Version: "2.90.0-beta.1"},
want: true,
},
{
name: "lix 2.95",
info: Info{Implementation: "Lix, like Nix", Version: "2.95.0"},
want: false,
},
{
// A 2.95 prerelease has also dropped fetchClosure and must be
// treated as unsupported, even though semver sorts it below 2.95.0.
name: "lix 2.95 prerelease",
info: Info{Implementation: "Lix, like Nix", Version: "2.95.0-beta.1"},
want: false,
},
{
name: "lix after 2.95",
info: Info{Implementation: "Lix, like Nix", Version: "2.95.2"},
want: false,
},
{
// Unknown version: assume support to avoid a false positive.
name: "lix unknown version",
info: Info{Implementation: "Lix, like Nix"},
want: true,
},
}
for _, tt := range cases {
t.Run(tt.name, func(t *testing.T) {
if got := tt.info.SupportsFetchClosure(); got != tt.want {
t.Errorf("SupportsFetchClosure() = %v, want %v", got, tt.want)
}
})
}
}

func TestNixBinaryFallbackPaths(t *testing.T) {
paths := nixBinaryFallbackPaths()
if len(paths) == 0 {
Expand Down
Loading