diff --git a/nix/nix.go b/nix/nix.go index 0e37730ef89..6405cb89f57 100644 --- a/nix/nix.go +++ b/nix/nix.go @@ -179,13 +179,21 @@ const ( // // The semantic component is sourced from . // It's been modified to tolerate Nix prerelease versions, which don't have a -// hyphen before the prerelease component and contain underscores. -var versionRegexp = regexp.MustCompile(`^(.+) \(.+\) ((?P0|[1-9]\d*)\.(?P0|[1-9]\d*)\.(?P0|[1-9]\d*)(?:(?:-|pre)(?P(?: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[0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?)$`) +// hyphen before the prerelease component and contain underscores. The patch +// component is optional because newer Nix versions may omit it (e.g. +// 2.33pre20251107_479b6b73). +var versionRegexp = regexp.MustCompile(`^(.+) \(.+\) ((?P0|[1-9]\d*)\.(?P0|[1-9]\d*)(?:\.(?P0|[1-9]\d*))?(?:(?:-|pre)(?P(?: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[0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?)$`) // preReleaseRegexp matches Nix prerelease version strings, which are not valid // semvers. var preReleaseRegexp = regexp.MustCompile(`pre(?P[0-9]+)_(?P[a-f0-9]{4,40})$`) +// missingPatchRegexp matches a version that only has major and minor components +// (no patch), such as "2.33" or "2.33-pre.20251107+479b6b73". Newer Nix +// versions may omit the patch component, which makes the version an invalid +// semver. +var missingPatchRegexp = regexp.MustCompile(`^(\d+\.\d+)($|[-+])`) + // Info contains information about a Nix installation. type Info struct { // Name identifies the Nix implementation. It is usually "nix" but may @@ -295,15 +303,34 @@ func (i Info) AtLeast(version string) bool { if !semver.IsValid(version) { panic(fmt.Sprintf("nix.atLeast: invalid version %q", version[1:])) } - if semver.IsValid("v" + i.Version) { - return semver.Compare("v"+i.Version, version) >= 0 + current := coerceSemver(i.Version) + if current == "" { + return false } + return semver.Compare(current, version) >= 0 +} - // If the version isn't a valid semver, check to see if it's a - // prerelease (e.g., 2.23.0pre20240526_7de033d6) and coerce it to a - // valid version (2.23.0-pre.20240526+7de033d6) so we can compare it. - prerelease := preReleaseRegexp.ReplaceAllString(i.Version, "-pre.$date+$commit") - return semver.Compare("v"+prerelease, version) >= 0 +// coerceSemver converts a Nix version string into a valid semantic version +// string with a leading "v", or returns an empty string if it cannot. Nix +// versions are not always valid semvers: +// - prerelease versions omit the hyphen before the prerelease identifier and +// use underscores (e.g. 2.23.0pre20240526_7de033d6), and +// - newer Nix versions may omit the patch component (e.g. +// 2.33pre20251107_479b6b73). +func coerceSemver(version string) string { + if version == "" { + return "" + } + // Rewrite a Nix prerelease suffix (preNNN_commit) into a valid semver + // prerelease + build metadata: -pre.+. + version = preReleaseRegexp.ReplaceAllString(version, "-pre.$date+$commit") + // Insert a zero patch component when it's missing. + version = missingPatchRegexp.ReplaceAllString(version, "$1.0$2") + version = "v" + version + if !semver.IsValid(version) { + return "" + } + return version } // sourceProfileMutex guards against multiple goroutines attempting to source diff --git a/nix/nix_test.go b/nix/nix_test.go index 1604edf39a6..31e7d9f671c 100644 --- a/nix/nix_test.go +++ b/nix/nix_test.go @@ -112,6 +112,9 @@ func TestParseVersionInfoShort(t *testing.T) { {"nix (Nix) 2.23.0pre20240526_7de033d6", "nix", "2.23.0pre20240526_7de033d6"}, {"command (Nix) name (Nix) 2.21.2", "command (Nix) name", "2.21.2"}, {"nix (Lix, like Nix) 2.90.0-beta.1", "nix", "2.90.0-beta.1"}, + // Newer Nix versions may omit the patch component. + // https://github.com/jetify-com/devbox/issues/2766 + {"nix (Nix) 2.33pre20251107_479b6b73", "nix", "2.33pre20251107_479b6b73"}, } for _, tt := range cases { @@ -186,6 +189,22 @@ func TestVersionInfoAtLeast(t *testing.T) { t.Errorf("got %s < %s", info.Version, "2.23.0-pre.1") } + // Newer Nix prerelease versions may omit the patch component. + // https://github.com/jetify-com/devbox/issues/2766 + info.Version = "2.33pre20251107_479b6b73" + if !info.AtLeast(Version2_12) { + t.Errorf("got %s < %s", info.Version, Version2_12) + } + if !info.AtLeast(MinVersion) { + t.Errorf("got %s < %s", info.Version, MinVersion) + } + if !info.AtLeast("2.33.0-pre.1") { + t.Errorf("got %s < %s", info.Version, "2.33.0-pre.1") + } + if info.AtLeast("2.34.0") { + t.Errorf("got %s >= %s", info.Version, "2.34.0") + } + t.Run("ArgEmptyPanic", func(t *testing.T) { defer func() { if r := recover(); r == nil {