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
9 changes: 7 additions & 2 deletions internal/nix/install.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,18 @@ import (
"github.com/mattn/go-isatty"

"go.jetify.com/devbox/internal/boxcli/usererr"
"go.jetify.com/devbox/internal/cmdutil"
"go.jetify.com/devbox/internal/fileutil"
"go.jetify.com/devbox/nix"
)

func BinaryInstalled() bool {
return cmdutil.Exists("nix")
// Use the same resolver that Devbox uses to run nix commands (searching
// $PATH and well-known install locations), rather than a bare $PATH lookup.
// Otherwise Devbox can wrongly report nix as missing when it exists at a
// known location but isn't on $PATH — e.g. on NixOS, or when a non-POSIX
// login shell such as fish hasn't sourced the Nix profile (issue #2787).
_, err := nix.Default.LookPath()
return err == nil
}

func dirExistsAndIsNotEmpty(dir string) bool {
Expand Down
51 changes: 39 additions & 12 deletions nix/nix.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,11 @@ func System() string {
return Default.System()
}

// LookPath calls [Nix.LookPath] on the default Nix installation.
func LookPath() (string, error) {
return Default.LookPath()
}

// Version calls [Nix.Version] on the default Nix installation.
func Version() string {
return Default.Version()
Expand Down Expand Up @@ -88,24 +93,46 @@ func (n *Nix) resolvePath() (string, error) {
return path, nil
}

try := []string{
"/nix/var/nix/profiles/default/bin/nix",
"/run/current-system/sw/bin",
}
for _, path := range try {
for _, path := range nixBinaryFallbackPaths() {
stat, err := os.Stat(path)
if err == nil {
// Is it executable and not a directory?
m := stat.Mode()
if !m.IsDir() && m.Perm()&0o111 != 0 {
n.lookPath.Store(&path)
return path, nil
}
if err != nil {
continue
}
// Is it an executable file (and not a directory)?
m := stat.Mode()
if !m.IsDir() && m.Perm()&0o111 != 0 {
n.lookPath.Store(&path)
return path, nil
}
}
return "", pathErr
}

// LookPath returns the absolute path to the nix executable. It searches $PATH
// (after attempting to source the Nix profile) and, failing that, the
// well-known installation locations in [nixBinaryFallbackPaths]. It returns an
// error if nix cannot be found.
//
// Because Devbox invokes nix by absolute path, a non-error result here means
// nix commands will run even when nix is not on $PATH — which happens, for
// example, when the login shell has not sourced the Nix profile (common with
// non-POSIX shells such as fish).
func (n *Nix) LookPath() (string, error) {
return n.resolvePath()
}

// nixBinaryFallbackPaths returns well-known absolute paths to the nix
// executable, searched in order when nix is not found on $PATH. Each entry must
// point at the nix binary itself, not the directory that contains it.
func nixBinaryFallbackPaths() []string {
return []string{
"/nix/var/nix/profiles/default/bin/nix",
// On NixOS, nix is provided through the current system profile rather
// than /nix/var/nix/profiles/default.
"/run/current-system/sw/bin/nix",
}
}

func (n *Nix) logger() *slog.Logger {
if n.Logger == nil {
return slog.Default()
Expand Down
23 changes: 23 additions & 0 deletions nix/nix_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
package nix

import (
"path/filepath"
"slices"
"testing"
)
Expand Down Expand Up @@ -204,3 +205,25 @@ func TestVersionInfoAtLeast(t *testing.T) {
info.AtLeast(v)
})
}

func TestNixBinaryFallbackPaths(t *testing.T) {
paths := nixBinaryFallbackPaths()
if len(paths) == 0 {
t.Fatal("expected at least one fallback path")
}
// Every fallback must point at the nix binary itself, not a directory that
// contains it. resolvePath rejects directories, so a bare bin dir here
// would silently never match (regression guard for issue #2787).
for _, p := range paths {
if filepath.Base(p) != "nix" {
t.Errorf("fallback path %q must end in the nix binary, got base %q", p, filepath.Base(p))
}
if !filepath.IsAbs(p) {
t.Errorf("fallback path %q must be absolute", p)
}
}
// The NixOS system-profile location must be covered.
if !slices.Contains(paths, "/run/current-system/sw/bin/nix") {
t.Errorf("expected NixOS system-profile nix path in fallbacks, got %v", paths)
}
}
Loading