Skip to content

Commit 6d759b4

Browse files
authored
nix: make System, Version, SourceProfile public (#2443)
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 6d759b4

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)