Skip to content

Commit 71029a6

Browse files
committed
nix: make System, Version, SourceProfile public
As part of some cleanup for making the DetSys installer the default, move the code related to getting the Nix version and system to the non-internal go.jetpack.io/devbox/nix package. It also merges in some nearly-duplicate code from the search indexer's Nix code (which will eventually use this package instead). Some of the changes taken from the indexer are: - Calling any function or method automatically... - sources the Nix profile if necessary. - looks for Nix in some well-known places if it isn't in PATH. - `nix.Version` and `nix.System` are cached behind a `sync.Once` by default. All top-level functions map to a method on a default Nix struct, following the same pattern found in `flags`, `slog`, etc. const Version2_12 = "2.12.0" ... var Default = &Nix{} func AtLeast(version string) bool func SourceProfile() (sourced bool, err error) func System() string func Version() string type Info struct{ ... } func (i Info) AtLeast(version string) bool type Nix struct{ ... } func (n *Nix) Info() (Info, error) func (n *Nix) System() string func (n *Nix) Version() string
1 parent 892add7 commit 71029a6

File tree

18 files changed

+721
-580
lines changed

18 files changed

+721
-580
lines changed

internal/devbox/devbox.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ func Open(opts *devopt.Opts) (*Devbox, error) {
115115
cfg: cfg,
116116
env: opts.Env,
117117
environment: environment,
118-
nix: &nix.Nix{},
118+
nix: &nix.NixInstance{},
119119
projectDir: filepath.Dir(cfg.Root.AbsRootPath),
120120
pluginManager: plugin.NewManager(),
121121
stderr: opts.Stderr,

internal/devpkg/narinfo_cache.go

Lines changed: 3 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -73,14 +73,6 @@ func FillNarInfoCache(ctx context.Context, packages ...*Package) error {
7373
return nil
7474
}
7575

76-
// Pre-compute values read in fillNarInfoCache
77-
// so they can be read from multiple go-routines without locks
78-
_, err := nix.Version()
79-
if err != nil {
80-
return err
81-
}
82-
_ = nix.System()
83-
8476
group, _ := errgroup.WithContext(ctx)
8577
for _, p := range eligiblePackages {
8678
pkg := p // copy the loop variable since its used in a closure below
@@ -240,29 +232,22 @@ func (p *Package) sysInfoIfExists() (*lock.SystemInfo, error) {
240232
return nil, nil
241233
}
242234

243-
version, err := nix.Version()
244-
if err != nil {
245-
return nil, err
246-
}
247-
248235
// disable for nix < 2.17
249-
if !version.AtLeast(nix.Version2_17) {
250-
return nil, err
236+
if !nix.AtLeast(nix.Version2_17) {
237+
return nil, nil
251238
}
252239

253240
entry, err := p.lockfile.Resolve(p.Raw)
254241
if err != nil {
255242
return nil, err
256243
}
257244

258-
userSystem := nix.System()
259-
260245
if entry.Systems == nil {
261246
return nil, nil
262247
}
263248

264249
// Check if the user's system's info is present in the lockfile
265-
sysInfo, ok := entry.Systems[userSystem]
250+
sysInfo, ok := entry.Systems[nix.System()]
266251
if !ok {
267252
return nil, nil
268253
}

internal/devpkg/package.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -390,7 +390,7 @@ func (p *Package) normalizePackageAttributePath() (string, error) {
390390

391391
// We prefer nix.Search over just trying to parse the package's "URL" because
392392
// nix.Search will guarantee that the package exists for the current system.
393-
var infos map[string]*nix.Info
393+
var infos map[string]*nix.PkgInfo
394394
if p.IsDevboxPackage && !p.IsRunX() {
395395
// Perf optimization: For queries of the form nixpkgs/<commit>#foo, we can
396396
// use a nix.Search cache.

internal/nix/config.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import (
1616
"strings"
1717

1818
"go.jetpack.io/devbox/internal/redact"
19+
"go.jetpack.io/devbox/nix"
1920
)
2021

2122
// Config is a parsed Nix configuration.
@@ -106,7 +107,7 @@ func (c Config) IsUserTrusted(ctx context.Context, username string) (bool, error
106107
}
107108

108109
func IncludeDevboxConfig(ctx context.Context, username string) error {
109-
info, _ := versionInfo()
110+
info, _ := nix.Default.Info()
110111
path := cmp.Or(info.SystemConfig, "/etc/nix/nix.conf")
111112
includePath := filepath.Join(filepath.Dir(path), "devbox-nix.conf")
112113
b := fmt.Appendf(nil, "# This config was auto-generated by Devbox.\n\nextra-trusted-users = %s\n", username)

internal/nix/install.go

Lines changed: 5 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,8 @@ import (
2121
"go.jetpack.io/devbox/internal/build"
2222
"go.jetpack.io/devbox/internal/cmdutil"
2323
"go.jetpack.io/devbox/internal/fileutil"
24-
"go.jetpack.io/devbox/internal/redact"
2524
"go.jetpack.io/devbox/internal/ux"
25+
"go.jetpack.io/devbox/nix"
2626
)
2727

2828
const rootError = "warning: installing Nix as root is not supported by this script!"
@@ -121,33 +121,23 @@ func EnsureNixInstalled(writer io.Writer, withDaemonFunc func() *bool) (err erro
121121
return
122122
}
123123

124-
var version VersionInfo
125-
version, err = Version()
126-
if err != nil {
127-
err = redact.Errorf("nix: ensure install: get version: %w", err)
128-
return
129-
}
130-
131124
// ensure minimum nix version installed
132-
if !version.AtLeast(MinVersion) {
125+
if !nix.AtLeast(MinVersion) {
133126
err = usererr.New(
134127
"Devbox requires nix of version >= %s. Your version is %s. "+
135128
"Please upgrade nix and try again.\n",
136129
MinVersion,
137-
version,
130+
nix.Version(),
138131
)
139132
return
140133
}
141-
// call ComputeSystem to ensure its value is internally cached so other
142-
// callers can rely on just calling System
143-
err = ComputeSystem()
144134
}()
145135

146136
if BinaryInstalled() {
147137
return nil
148138
}
149139
if dirExists() {
150-
if err = SourceNixEnv(); err != nil {
140+
if _, err = SourceProfile(); err != nil {
151141
return err
152142
} else if BinaryInstalled() {
153143
return nil
@@ -174,7 +164,7 @@ func EnsureNixInstalled(writer io.Writer, withDaemonFunc func() *bool) (err erro
174164
}
175165

176166
// Source again
177-
if err = SourceNixEnv(); err != nil {
167+
if _, err = SourceProfile(); err != nil {
178168
return err
179169
}
180170

internal/nix/instance.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ package nix
66
import "context"
77

88
// These make it easier to stub out nix for testing
9-
type Nix struct{}
9+
type NixInstance struct{}
1010

1111
type Nixer interface {
1212
PrintDevEnv(ctx context.Context, args *PrintDevEnvArgs) (*PrintDevEnvOut, error)

internal/nix/nix.go

Lines changed: 1 addition & 219 deletions
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,13 @@ import (
1616
"runtime"
1717
"runtime/trace"
1818
"strings"
19-
"sync"
2019
"time"
2120

2221
"github.com/pkg/errors"
2322
"go.jetpack.io/devbox/internal/boxcli/featureflag"
2423
"go.jetpack.io/devbox/internal/boxcli/usererr"
2524
"go.jetpack.io/devbox/internal/redact"
2625
"go.jetpack.io/devbox/nix/flake"
27-
"golang.org/x/mod/semver"
2826

2927
"go.jetpack.io/devbox/internal/debug"
3028
)
@@ -51,7 +49,7 @@ type PrintDevEnvArgs struct {
5149

5250
// PrintDevEnv calls `nix print-dev-env -f <path>` and returns its output. The output contains
5351
// all the environment variables and bash functions required to create a nix shell.
54-
func (*Nix) PrintDevEnv(ctx context.Context, args *PrintDevEnvArgs) (*PrintDevEnvOut, error) {
52+
func (*NixInstance) PrintDevEnv(ctx context.Context, args *PrintDevEnvArgs) (*PrintDevEnvOut, error) {
5553
defer debug.FunctionTimer().End()
5654
defer trace.StartRegion(ctx, "nixPrintDevEnv").End()
5755

@@ -128,226 +126,10 @@ func ExperimentalFlags() []string {
128126
}
129127
}
130128

131-
func System() string {
132-
if cachedSystem == "" {
133-
// While this should have been initialized, we do a best-effort to avoid
134-
// a panic.
135-
if err := ComputeSystem(); err != nil {
136-
panic(fmt.Sprintf(
137-
"System called before being initialized by ComputeSystem: %v",
138-
err,
139-
))
140-
}
141-
}
142-
return cachedSystem
143-
}
144-
145-
var cachedSystem string
146-
147-
func ComputeSystem() error {
148-
// For Savil to debug "remove nixpkgs" feature. The Search api lacks x86-darwin info.
149-
// So, I need to fake that I am x86-linux and inspect the output in generated devbox.lock
150-
// and flake.nix files.
151-
// This is also used by unit tests.
152-
if cachedSystem != "" {
153-
return nil
154-
}
155-
override := os.Getenv("__DEVBOX_NIX_SYSTEM")
156-
if override != "" {
157-
cachedSystem = override
158-
} else {
159-
cmd := command("eval", "--impure", "--raw", "--expr", "builtins.currentSystem")
160-
out, err := cmd.Output(context.TODO())
161-
if err != nil {
162-
return err
163-
}
164-
cachedSystem = string(out)
165-
}
166-
return nil
167-
}
168-
169129
func SystemIsLinux() bool {
170130
return strings.Contains(System(), "linux")
171131
}
172132

173-
// All major Nix versions supported by Devbox.
174-
const (
175-
Version2_12 = "2.12.0"
176-
Version2_13 = "2.13.0"
177-
Version2_14 = "2.14.0"
178-
Version2_15 = "2.15.0"
179-
Version2_16 = "2.16.0"
180-
Version2_17 = "2.17.0"
181-
Version2_18 = "2.18.0"
182-
Version2_19 = "2.19.0"
183-
Version2_20 = "2.20.0"
184-
Version2_21 = "2.21.0"
185-
Version2_22 = "2.22.0"
186-
187-
MinVersion = Version2_12
188-
)
189-
190-
// versionRegexp matches the first line of "nix --version" output.
191-
//
192-
// The semantic component is sourced from <https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string>.
193-
// It's been modified to tolerate Nix prerelease versions, which don't have a
194-
// hyphen before the prerelease component and contain underscores.
195-
var versionRegexp = regexp.MustCompile(`^(.+) \(.+\) ((?P<major>0|[1-9]\d*)\.(?P<minor>0|[1-9]\d*)\.(?P<patch>0|[1-9]\d*)(?:(?:-|pre)(?P<prerelease>(?:0|[1-9]\d*|\d*[_a-zA-Z-][_0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[_a-zA-Z-][_0-9a-zA-Z-]*))*))?(?:\+(?P<buildmetadata>[0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?)$`)
196-
197-
// preReleaseRegexp matches Nix prerelease version strings, which are not valid
198-
// semvers.
199-
var preReleaseRegexp = regexp.MustCompile(`pre(?P<date>[0-9]+)_(?P<commit>[a-f0-9]{4,40})$`)
200-
201-
// VersionInfo contains information about a Nix installation.
202-
type VersionInfo struct {
203-
// Name is the executed program name (the first element of argv).
204-
Name string
205-
206-
// Version is the semantic Nix version string.
207-
Version string
208-
209-
// System is the current Nix system. It follows the pattern <arch>-<os>
210-
// and does not use the same values as GOOS or GOARCH.
211-
System string
212-
213-
// ExtraSystems are other systems that the current machine supports.
214-
// Usually set by the extra-platforms setting in nix.conf.
215-
ExtraSystems []string
216-
217-
// Features are the capabilities that the Nix binary was compiled with.
218-
Features []string
219-
220-
// SystemConfig is the path to the Nix system configuration file,
221-
// usually /etc/nix/nix.conf.
222-
SystemConfig string
223-
224-
// UserConfigs is a list of paths to the user's Nix configuration files.
225-
UserConfigs []string
226-
227-
// StoreDir is the path to the Nix store directory, usually /nix/store.
228-
StoreDir string
229-
230-
// StateDir is the path to the Nix state directory, usually
231-
// /nix/var/nix.
232-
StateDir string
233-
234-
// DataDir is the path to the Nix data directory, usually somewhere
235-
// within the Nix store. This field is empty for Nix versions <= 2.12.
236-
DataDir string
237-
}
238-
239-
func parseVersionInfo(data []byte) (VersionInfo, error) {
240-
// Example nix --version --debug output from Nix versions 2.12 to 2.21.
241-
// Version 2.12 omits the data directory, but they're otherwise
242-
// identical.
243-
//
244-
// See https://github.com/NixOS/nix/blob/5b9cb8b3722b85191ee8cce8f0993170e0fc234c/src/libmain/shared.cc#L284-L305
245-
//
246-
// nix (Nix) 2.21.2
247-
// System type: aarch64-darwin
248-
// Additional system types: x86_64-darwin
249-
// Features: gc, signed-caches
250-
// System configuration file: /etc/nix/nix.conf
251-
// User configuration files: /Users/nobody/.config/nix/nix.conf:/etc/xdg/nix/nix.conf
252-
// Store directory: /nix/store
253-
// State directory: /nix/var/nix
254-
// Data directory: /nix/store/m0ns07v8by0458yp6k30rfq1rs3kaz6g-nix-2.21.2/share
255-
256-
info := VersionInfo{}
257-
if len(data) == 0 {
258-
return info, redact.Errorf("empty nix --version output")
259-
}
260-
261-
lines := strings.Split(string(data), "\n")
262-
matches := versionRegexp.FindStringSubmatch(lines[0])
263-
if len(matches) < 3 {
264-
return info, redact.Errorf("parse nix version: %s", redact.Safe(lines[0]))
265-
}
266-
info.Name = matches[1]
267-
info.Version = matches[2]
268-
for _, line := range lines {
269-
name, value, found := strings.Cut(line, ": ")
270-
if !found {
271-
continue
272-
}
273-
274-
switch name {
275-
case "System type":
276-
info.System = value
277-
case "Additional system types":
278-
info.ExtraSystems = strings.Split(value, ", ")
279-
case "Features":
280-
info.Features = strings.Split(value, ", ")
281-
case "System configuration file":
282-
info.SystemConfig = value
283-
case "User configuration files":
284-
info.UserConfigs = strings.Split(value, ":")
285-
case "Store directory":
286-
info.StoreDir = value
287-
case "State directory":
288-
info.StateDir = value
289-
case "Data directory":
290-
info.DataDir = value
291-
}
292-
}
293-
return info, nil
294-
}
295-
296-
// AtLeast returns true if v.Version is >= version per semantic versioning. It
297-
// always returns false if v.Version is empty or invalid, such as when the
298-
// current Nix version cannot be parsed. It panics if version is an invalid
299-
// semver.
300-
func (v VersionInfo) AtLeast(version string) bool {
301-
if !strings.HasPrefix(version, "v") {
302-
version = "v" + version
303-
}
304-
if !semver.IsValid(version) {
305-
panic(fmt.Sprintf("nix.atLeast: invalid version %q", version[1:]))
306-
}
307-
if semver.IsValid("v" + v.Version) {
308-
return semver.Compare("v"+v.Version, version) >= 0
309-
}
310-
311-
// If the version isn't a valid semver, check to see if it's a
312-
// prerelease (e.g., 2.23.0pre20240526_7de033d6) and coerce it to a
313-
// valid version (2.23.0-pre.20240526+7de033d6) so we can compare it.
314-
prerelease := preReleaseRegexp.ReplaceAllString(v.Version, "-pre.$date+$commit")
315-
return semver.Compare("v"+prerelease, version) >= 0
316-
}
317-
318-
// version is the cached output of `nix --version --debug`.
319-
var versionInfo = sync.OnceValues(runNixVersion)
320-
321-
func runNixVersion() (VersionInfo, error) {
322-
// Arbitrary timeout to make sure we don't take too long or hang.
323-
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
324-
defer cancel()
325-
326-
// Intentionally don't use the nix.command function here. We use this to
327-
// perform Nix version checks and don't want to pass any extra-features
328-
// or flags that might be missing from old versions.
329-
cmd := exec.CommandContext(ctx, "nix", "--version", "--debug")
330-
out, err := cmd.Output()
331-
if err != nil {
332-
var exitErr *exec.ExitError
333-
if errors.As(err, &exitErr) && len(exitErr.Stderr) != 0 {
334-
return VersionInfo{}, redact.Errorf("nix command: %s: %q: %v", redact.Safe(cmd), exitErr.Stderr, err)
335-
}
336-
if errors.Is(ctx.Err(), context.DeadlineExceeded) {
337-
return VersionInfo{}, redact.Errorf("nix command: %s: timed out while reading output: %v", redact.Safe(cmd), err)
338-
}
339-
return VersionInfo{}, redact.Errorf("nix command: %s: %v", redact.Safe(cmd), err)
340-
}
341-
342-
slog.Debug("nix --version --debug output", "out", out)
343-
return parseVersionInfo(out)
344-
}
345-
346-
// Version returns the currently installed version of Nix.
347-
func Version() (VersionInfo, error) {
348-
return versionInfo()
349-
}
350-
351133
var nixPlatforms = []string{
352134
"aarch64-darwin",
353135
"aarch64-linux",

0 commit comments

Comments
 (0)