@@ -22,6 +22,7 @@ import (
22
22
"go.jetpack.io/devbox/internal/boxcli/featureflag"
23
23
"go.jetpack.io/devbox/internal/boxcli/usererr"
24
24
"go.jetpack.io/devbox/internal/redact"
25
+ "golang.org/x/mod/semver"
25
26
26
27
"go.jetpack.io/devbox/internal/debug"
27
28
)
@@ -174,6 +175,22 @@ func SystemIsLinux() bool {
174
175
return strings .Contains (System (), "linux" )
175
176
}
176
177
178
+ // All major Nix versions supported by Devbox.
179
+ const (
180
+ Version2_12 = "2.12.0"
181
+ Version2_13 = "2.13.0"
182
+ Version2_14 = "2.14.0"
183
+ Version2_15 = "2.15.0"
184
+ Version2_16 = "2.16.0"
185
+ Version2_17 = "2.17.0"
186
+ Version2_18 = "2.18.0"
187
+ Version2_19 = "2.19.0"
188
+ Version2_20 = "2.20.0"
189
+ Version2_21 = "2.21.0"
190
+
191
+ MinVersion = Version2_12
192
+ )
193
+
177
194
// VersionInfo contains information about a Nix installation.
178
195
type VersionInfo struct {
179
196
// Name is the executed program name (the first element of argv).
@@ -210,12 +227,9 @@ type VersionInfo struct {
210
227
// DataDir is the path to the Nix data directory, usually somewhere
211
228
// within the Nix store. This field is empty for Nix versions <= 2.12.
212
229
DataDir string
213
-
214
- // raw is the raw nix --version --debug output.
215
- raw string
216
230
}
217
231
218
- func parseVersionInfo (data []byte ) VersionInfo {
232
+ func parseVersionInfo (data []byte ) ( VersionInfo , error ) {
219
233
// Example nix --version --debug output from Nix versions 2.12 to 2.21.
220
234
// Version 2.12 omits the data directory, but they're otherwise
221
235
// identical.
@@ -232,13 +246,17 @@ func parseVersionInfo(data []byte) VersionInfo {
232
246
// State directory: /nix/var/nix
233
247
// Data directory: /nix/store/m0ns07v8by0458yp6k30rfq1rs3kaz6g-nix-2.21.2/share
234
248
235
- info := VersionInfo {raw : string ( data ) }
236
- if len (info . raw ) == 0 {
237
- return info
249
+ info := VersionInfo {}
250
+ if len (data ) == 0 {
251
+ return info , redact . Errorf ( "empty nix --version output" )
238
252
}
239
253
240
- lines := strings .Split (info .raw , "\n " )
241
- info .Name , info .Version , _ = strings .Cut (lines [0 ], " (Nix) " )
254
+ lines := strings .Split (string (data ), "\n " )
255
+ found := false
256
+ info .Name , info .Version , found = strings .Cut (lines [0 ], " (Nix) " )
257
+ if ! found {
258
+ return info , redact .Errorf ("parse nix version: %s" , redact .Safe (lines [0 ]))
259
+ }
242
260
for _ , line := range lines {
243
261
name , value , found := strings .Cut (line , ": " )
244
262
if ! found {
@@ -264,18 +282,21 @@ func parseVersionInfo(data []byte) VersionInfo {
264
282
info .DataDir = value
265
283
}
266
284
}
267
- return info
285
+ return info , nil
268
286
}
269
287
270
- func ( v VersionInfo ) version () ( string , error ) {
271
- if v .Version == "" {
272
- firstLine , _ , _ := strings . Cut ( v . raw , " \n " )
273
- if strings . TrimSpace ( firstLine ) == "" {
274
- firstLine = "empty nix -- version output"
275
- }
276
- return "" , redact . Errorf ( "parse nix version: %s" , redact . Safe ( firstLine ))
288
+ // AtLeast returns true if v.Version is >= version per semantic versioning. It
289
+ // always returns false if v.Version is empty or invalid, such as when the
290
+ // current Nix version cannot be parsed. It panics if version is an invalid
291
+ // semver.
292
+ func ( v VersionInfo ) AtLeast ( version string ) bool {
293
+ if ! strings . HasPrefix ( version , "v" ) {
294
+ version = "v" + version
277
295
}
278
- return v .Version , nil
296
+ if ! semver .IsValid (version ) {
297
+ panic (fmt .Sprintf ("nix.atLeast: invalid version %q" , version [1 :]))
298
+ }
299
+ return semver .Compare ("v" + v .Version , version ) >= 0
279
300
}
280
301
281
302
// version is the cached output of `nix --version --debug`.
@@ -292,28 +313,23 @@ func runNixVersion() (VersionInfo, error) {
292
313
cmd := exec .CommandContext (ctx , "nix" , "--version" , "--debug" )
293
314
out , err := cmd .Output ()
294
315
if err != nil {
295
- if errors .Is (err , context .DeadlineExceeded ) {
296
- return VersionInfo {}, redact .Errorf ("nix command: %s: timed out while reading output" , redact .Safe (cmd ))
297
- }
298
-
299
316
var exitErr * exec.ExitError
300
317
if errors .As (err , & exitErr ) && len (exitErr .Stderr ) != 0 {
301
318
return VersionInfo {}, redact .Errorf ("nix command: %s: %q: %v" , redact .Safe (cmd ), exitErr .Stderr , err )
302
319
}
320
+ if errors .Is (ctx .Err (), context .DeadlineExceeded ) {
321
+ return VersionInfo {}, redact .Errorf ("nix command: %s: timed out while reading output: %v" , redact .Safe (cmd ), err )
322
+ }
303
323
return VersionInfo {}, redact .Errorf ("nix command: %s: %v" , redact .Safe (cmd ), err )
304
324
}
305
325
306
326
debug .Log ("nix --version --debug output:\n %s" , out )
307
- return parseVersionInfo (out ), nil
327
+ return parseVersionInfo (out )
308
328
}
309
329
310
330
// Version returns the currently installed version of Nix.
311
- func Version () (string , error ) {
312
- info , err := versionInfo ()
313
- if err != nil {
314
- return "" , err
315
- }
316
- return info .version ()
331
+ func Version () (VersionInfo , error ) {
332
+ return versionInfo ()
317
333
}
318
334
319
335
var nixPlatforms = []string {
0 commit comments