diff --git a/nix/nix.go b/nix/nix.go index 0e37730ef89..706f882cf19 100644 --- a/nix/nix.go +++ b/nix/nix.go @@ -179,13 +179,20 @@ 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 some Nix prereleases omit it (for example, +// 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 leading major.minor version that has no patch +// component (for example, the "2.33" in "2.33-pre.20251107+479b6b73"). It's +// used to insert a ".0" patch so the version is a valid 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 @@ -303,6 +310,11 @@ func (i Info) AtLeast(version string) bool { // 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") + // Some Nix prereleases omit the patch component (e.g., + // 2.33pre20251107_479b6b73 -> 2.33-pre.20251107+479b6b73). + // golang.org/x/mod/semver won't parse a prerelease without an explicit + // patch component, so insert ".0". + prerelease = missingPatchRegexp.ReplaceAllString(prerelease, "${1}.0${2}") return semver.Compare("v"+prerelease, version) >= 0 } diff --git a/nix/nix_test.go b/nix/nix_test.go index 1604edf39a6..fe02e73693a 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"}, + // Some Nix prereleases 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,25 @@ func TestVersionInfoAtLeast(t *testing.T) { t.Errorf("got %s < %s", info.Version, "2.23.0-pre.1") } + // Nix prereleases that omit the patch component must still compare. + // 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.33.0") { + t.Errorf("got %s >= %s", info.Version, "2.33.0") + } + 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 {