From 062e0f1de1d9ccfeb423f10f833a7b264463f159 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 16 Jun 2026 14:09:01 +0000 Subject: [PATCH 1/3] Fix Nix version parsing for prereleases that omit the patch component Modern Nix prerelease versions such as 2.33pre20251107_479b6b73 omit the patch component (2.33 rather than 2.33.0). versionRegexp required a patch component, so these versions failed to parse and EnsureNixInstalled reported an empty version ("Your version is ."), refusing to run. Make the patch component optional in versionRegexp and inject a ".0" patch when coercing such prereleases to a valid semver in Info.AtLeast. Fixes #2766 --- nix/nix.go | 15 +++++++++++++-- nix/nix_test.go | 22 ++++++++++++++++++++++ 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/nix/nix.go b/nix/nix.go index 0e37730ef89..a97d9c4507e 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,10 @@ 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). semver + // requires a patch component when there's a prerelease, 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 { From 717b075ecbff563008b0ffc7db02542e62d98881 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 16 Jun 2026 14:12:51 +0000 Subject: [PATCH 2/3] Clarify comment: x/mod/semver requires explicit patch component Address review feedback: semver always requires MAJOR.MINOR.PATCH, so reword the comment to make clear the .0 insertion works around golang.org/x/mod/semver's parsing requirement. --- nix/nix.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/nix/nix.go b/nix/nix.go index a97d9c4507e..706f882cf19 100644 --- a/nix/nix.go +++ b/nix/nix.go @@ -311,8 +311,9 @@ func (i Info) AtLeast(version string) bool { // 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). semver - // requires a patch component when there's a prerelease, so insert ".0". + // 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 } From 489f5dd6908bd4c813dc3ccbba3cd9912488d3e7 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 16 Jun 2026 14:27:58 +0000 Subject: [PATCH 3/3] Re-trigger CI The previous run's Elixir example test failed on a transient network error downloading Hex from builds.hex.pm (TLS handshake), unrelated to this change.