From 337f7b1f5d18e8b2469a37174c24baced4c23801 Mon Sep 17 00:00:00 2001 From: Ian Alexander Date: Thu, 13 Nov 2025 16:38:20 -0500 Subject: [PATCH] cmd/go: update default go directive in mod or work init This commit updates the default go directive when initializing a new module. The current logic is to use the latest version supported by the toolchain. This behavior is simple, predictable, and importantly, it can work while completely offline (i.e., no internet connection required). This commit changes the default version to the following behavior: * If the current toolchain version is a stable version of Go 1.N.M, default to go 1.(N-1).0 * If the current toolchain version is a pre-release version of Go 1.N (Release Candidate M) or a development version of Go 1.N, default to go 1.(N-2).0 This behavior maintains the property of being able to work offline. Fixes #74748. Change-Id: I81f62eef29f1dd51060067c8075f61e7bcf57c20 Reviewed-on: https://go-review.googlesource.com/c/go/+/720480 Commit-Queue: Ian Alexander Reviewed-by: Dmitri Shuralyov TryBot-Bypass: Dmitri Shuralyov Reviewed-by: Michael Matloob Reviewed-by: Michael Matloob --- src/cmd/go/internal/modload/init.go | 35 +++++++-- src/cmd/go/internal/workcmd/init.go | 4 +- src/cmd/go/testdata/script/mod_edit.txt | 71 ++++++++++--------- .../go/testdata/script/mod_init_version.txt | 47 ++++++++++++ src/cmd/go/testdata/script/work.txt | 3 +- src/cmd/go/testdata/script/work_edit.txt | 5 +- src/cmd/go/testdata/script/work_init_path.txt | 10 +-- .../testdata/script/work_init_toolchain.txt | 8 +-- .../go/testdata/script/work_init_version.txt | 35 +++++++++ .../testdata/script/work_sync_toolchain.txt | 7 +- .../go/testdata/script/work_use_toolchain.txt | 6 +- 11 files changed, 171 insertions(+), 60 deletions(-) create mode 100644 src/cmd/go/testdata/script/mod_init_version.txt create mode 100644 src/cmd/go/testdata/script/work_init_version.txt diff --git a/src/cmd/go/internal/modload/init.go b/src/cmd/go/internal/modload/init.go index bbdd0e95b5..c4ff965669 100644 --- a/src/cmd/go/internal/modload/init.go +++ b/src/cmd/go/internal/modload/init.go @@ -29,6 +29,7 @@ import ( "cmd/go/internal/lockedfile" "cmd/go/internal/modfetch" "cmd/go/internal/search" + igover "internal/gover" "golang.org/x/mod/modfile" "golang.org/x/mod/module" @@ -826,7 +827,7 @@ func WriteWorkFile(path string, wf *modfile.WorkFile) error { wf.Cleanup() out := modfile.Format(wf.Syntax) - return os.WriteFile(path, out, 0666) + return os.WriteFile(path, out, 0o666) } // UpdateWorkGoVersion updates the go line in wf to be at least goVers, @@ -1200,7 +1201,7 @@ func CreateModFile(loaderstate *State, ctx context.Context, modPath string) { modFile := new(modfile.File) modFile.AddModuleStmt(modPath) loaderstate.MainModules = makeMainModules(loaderstate, []module.Version{modFile.Module.Mod}, []string{modRoot}, []*modfile.File{modFile}, []*modFileIndex{nil}, nil) - addGoStmt(modFile, modFile.Module.Mod, gover.Local()) // Add the go directive before converted module requirements. + addGoStmt(modFile, modFile.Module.Mod, DefaultModInitGoVersion()) // Add the go directive before converted module requirements. rs := requirementsFromModFiles(loaderstate, ctx, nil, []*modfile.File{modFile}, nil) rs, err := updateRoots(loaderstate, ctx, rs.direct, rs, nil, nil, false) @@ -1811,9 +1812,7 @@ Run 'go help mod init' for more information. return "", fmt.Errorf(msg, dir, reason) } -var ( - importCommentRE = lazyregexp.New(`(?m)^package[ \t]+[^ \t\r\n/]+[ \t]+//[ \t]+import[ \t]+(\"[^"]+\")[ \t]*\r?\n`) -) +var importCommentRE = lazyregexp.New(`(?m)^package[ \t]+[^ \t\r\n/]+[ \t]+//[ \t]+import[ \t]+(\"[^"]+\")[ \t]*\r?\n`) func findImportComment(file string) string { data, err := os.ReadFile(file) @@ -2252,3 +2251,29 @@ func CheckGodebug(verb, k, v string) error { } return fmt.Errorf("unknown %s %q", verb, k) } + +// DefaultModInitGoVersion returns the appropriate go version to include in a +// newly initialized module or work file. +// +// If the current toolchain version is a stable version of Go 1.N.M, default to +// go 1.(N-1).0 +// +// If the current toolchain version is a pre-release version of Go 1.N (Release +// Candidate M) or a development version of Go 1.N, default to go 1.(N-2).0 +func DefaultModInitGoVersion() string { + v := gover.Local() + if isPrereleaseOrDevelVersion(v) { + v = gover.Prev(gover.Prev(v)) + } else { + v = gover.Prev(v) + } + if strings.Count(v, ".") < 2 { + v += ".0" + } + return v +} + +func isPrereleaseOrDevelVersion(s string) bool { + v := igover.Parse(s) + return v.Kind != "" || v.Patch == "" +} diff --git a/src/cmd/go/internal/workcmd/init.go b/src/cmd/go/internal/workcmd/init.go index 896740f080..eff7a8c763 100644 --- a/src/cmd/go/internal/workcmd/init.go +++ b/src/cmd/go/internal/workcmd/init.go @@ -12,7 +12,6 @@ import ( "cmd/go/internal/base" "cmd/go/internal/fsys" - "cmd/go/internal/gover" "cmd/go/internal/modload" "golang.org/x/mod/modfile" @@ -58,10 +57,9 @@ func runInit(ctx context.Context, cmd *base.Command, args []string) { base.Fatalf("go: %s already exists", gowork) } - goV := gover.Local() // Use current Go version by default wf := new(modfile.WorkFile) wf.Syntax = new(modfile.FileSyntax) - wf.AddGoStmt(goV) + wf.AddGoStmt(modload.DefaultModInitGoVersion()) workUse(ctx, moduleLoaderState, gowork, wf, args) modload.WriteWorkFile(gowork, wf) } diff --git a/src/cmd/go/testdata/script/mod_edit.txt b/src/cmd/go/testdata/script/mod_edit.txt index 6b7dd2c2c5..16d46610a2 100644 --- a/src/cmd/go/testdata/script/mod_edit.txt +++ b/src/cmd/go/testdata/script/mod_edit.txt @@ -1,5 +1,8 @@ env GO111MODULE=on +# Set go version so that we can test produced mod files for equality. +env TESTGO_VERSION=go1.26.0 + # Test that go mod edits and related mod flags work. # Also test that they can use a dummy name that isn't resolvable. golang.org/issue/24100 @@ -10,16 +13,16 @@ stderr 'cannot determine module path' go mod init x.x/y/z stderr 'creating new go.mod: module x.x/y/z' -cmpenv go.mod $WORK/go.mod.init +cmp go.mod $WORK/go.mod.init ! go mod init -cmpenv go.mod $WORK/go.mod.init +cmp go.mod $WORK/go.mod.init # go mod edits go mod edit -droprequire=x.1 -require=x.1@v1.0.0 -require=x.2@v1.1.0 -droprequire=x.2 -exclude='x.1 @ v1.2.0' -exclude=x.1@v1.2.1 -exclude=x.1@v2.0.0+incompatible -replace=x.1@v1.3.0=y.1@v1.4.0 -replace='x.1@v1.4.0 = ../z' -retract=v1.6.0 -retract=[v1.1.0,v1.2.0] -retract=[v1.3.0,v1.4.0] -retract=v1.0.0 -cmpenv go.mod $WORK/go.mod.edit1 +cmp go.mod $WORK/go.mod.edit1 go mod edit -droprequire=x.1 -dropexclude=x.1@v1.2.1 -dropexclude=x.1@v2.0.0+incompatible -dropreplace=x.1@v1.3.0 -require=x.3@v1.99.0 -dropretract=v1.0.0 -dropretract=[v1.1.0,v1.2.0] -cmpenv go.mod $WORK/go.mod.edit2 +cmp go.mod $WORK/go.mod.edit2 # -exclude and -retract reject invalid versions. ! go mod edit -exclude=example.com/m@bad @@ -36,11 +39,11 @@ stderr '^go: -exclude=example.com/m/v2@v1\.0\.0: version "v1\.0\.0" invalid: sho ! go mod edit -exclude=gopkg.in/example.v1@v2.0.0 stderr '^go: -exclude=gopkg\.in/example\.v1@v2\.0\.0: version "v2\.0\.0" invalid: should be v1, not v2$' -cmpenv go.mod $WORK/go.mod.edit2 +cmp go.mod $WORK/go.mod.edit2 # go mod edit -json go mod edit -json -cmpenv stdout $WORK/go.mod.json +cmp stdout $WORK/go.mod.json # go mod edit -json (retractions with rationales) go mod edit -json $WORK/go.mod.retractrationale @@ -56,66 +59,66 @@ cmp stdout $WORK/go.mod.empty.json # go mod edit -replace go mod edit -replace=x.1@v1.3.0=y.1/v2@v2.3.5 -replace=x.1@v1.4.0=y.1/v2@v2.3.5 -cmpenv go.mod $WORK/go.mod.edit3 +cmp go.mod $WORK/go.mod.edit3 go mod edit -replace=x.1=y.1/v2@v2.3.6 -cmpenv go.mod $WORK/go.mod.edit4 +cmp go.mod $WORK/go.mod.edit4 go mod edit -dropreplace=x.1 -cmpenv go.mod $WORK/go.mod.edit5 +cmp go.mod $WORK/go.mod.edit5 go mod edit -replace=x.1=../y.1/@v2 -cmpenv go.mod $WORK/go.mod.edit6 +cmp go.mod $WORK/go.mod.edit6 ! go mod edit -replace=x.1=y.1/@v2 stderr '^go: -replace=x.1=y.1/@v2: invalid new path: malformed import path "y.1/": trailing slash$' # go mod edit -fmt cp $WORK/go.mod.badfmt go.mod go mod edit -fmt -print # -print should avoid writing file -cmpenv stdout $WORK/go.mod.goodfmt +cmp stdout $WORK/go.mod.goodfmt cmp go.mod $WORK/go.mod.badfmt go mod edit -fmt # without -print, should write file (and nothing to stdout) ! stdout . -cmpenv go.mod $WORK/go.mod.goodfmt +cmp go.mod $WORK/go.mod.goodfmt # go mod edit -module cd $WORK/m go mod init a.a/b/c go mod edit -module x.x/y/z -cmpenv go.mod go.mod.edit +cmp go.mod go.mod.edit # golang.org/issue/30513: don't require go-gettable module paths. cd $WORK/local go mod init foo go mod edit -module local-only -require=other-local@v1.0.0 -replace other-local@v1.0.0=./other -cmpenv go.mod go.mod.edit +cmp go.mod go.mod.edit # go mod edit -godebug cd $WORK/g cp go.mod.start go.mod go mod edit -godebug key=value -cmpenv go.mod go.mod.edit +cmp go.mod go.mod.edit go mod edit -dropgodebug key2 -cmpenv go.mod go.mod.edit +cmp go.mod go.mod.edit go mod edit -dropgodebug key -cmpenv go.mod go.mod.start +cmp go.mod go.mod.start # go mod edit -tool cd $WORK/h cp go.mod.start go.mod go mod edit -tool example.com/tool -cmpenv go.mod go.mod.edit +cmp go.mod go.mod.edit go mod edit -droptool example.com/tool2 -cmpenv go.mod go.mod.edit +cmp go.mod go.mod.edit go mod edit -droptool example.com/tool -cmpenv go.mod go.mod.start +cmp go.mod go.mod.start # go mod edit -ignore cd $WORK/i cp go.mod.start go.mod go mod edit -ignore example.com/ignore -cmpenv go.mod go.mod.edit +cmp go.mod go.mod.edit go mod edit -dropignore example.com/ignore2 -cmpenv go.mod go.mod.edit +cmp go.mod go.mod.edit go mod edit -dropignore example.com/ignore -cmpenv go.mod go.mod.start +cmp go.mod go.mod.start -- x.go -- package x @@ -126,11 +129,11 @@ package w -- $WORK/go.mod.init -- module x.x/y/z -go $goversion +go 1.25.0 -- $WORK/go.mod.edit1 -- module x.x/y/z -go $goversion +go 1.25.0 require x.1 v1.0.0 @@ -154,7 +157,7 @@ retract ( -- $WORK/go.mod.edit2 -- module x.x/y/z -go $goversion +go 1.25.0 exclude x.1 v1.2.0 @@ -171,7 +174,7 @@ require x.3 v1.99.0 "Module": { "Path": "x.x/y/z" }, - "Go": "$goversion", + "Go": "1.25.0", "Require": [ { "Path": "x.3", @@ -211,7 +214,7 @@ require x.3 v1.99.0 -- $WORK/go.mod.edit3 -- module x.x/y/z -go $goversion +go 1.25.0 exclude x.1 v1.2.0 @@ -229,7 +232,7 @@ require x.3 v1.99.0 -- $WORK/go.mod.edit4 -- module x.x/y/z -go $goversion +go 1.25.0 exclude x.1 v1.2.0 @@ -244,7 +247,7 @@ require x.3 v1.99.0 -- $WORK/go.mod.edit5 -- module x.x/y/z -go $goversion +go 1.25.0 exclude x.1 v1.2.0 @@ -257,7 +260,7 @@ require x.3 v1.99.0 -- $WORK/go.mod.edit6 -- module x.x/y/z -go $goversion +go 1.25.0 exclude x.1 v1.2.0 @@ -272,7 +275,7 @@ replace x.1 => ../y.1/@v2 -- $WORK/local/go.mod.edit -- module local-only -go $goversion +go 1.25.0 require other-local v1.0.0 @@ -304,7 +307,7 @@ retract [v1.8.1, v1.8.2] -- $WORK/m/go.mod.edit -- module x.x/y/z -go $goversion +go 1.25.0 -- $WORK/go.mod.retractrationale -- module x.x/y/z @@ -405,4 +408,4 @@ module g go 1.24 -ignore example.com/ignore \ No newline at end of file +ignore example.com/ignore diff --git a/src/cmd/go/testdata/script/mod_init_version.txt b/src/cmd/go/testdata/script/mod_init_version.txt new file mode 100644 index 0000000000..5b909bc1d7 --- /dev/null +++ b/src/cmd/go/testdata/script/mod_init_version.txt @@ -0,0 +1,47 @@ +env TESTGO_VERSION=go1.28-devel +go mod init example.com +cmp go.mod go.mod.want-1.26.0 +rm go.mod +env TESTGO_VERSION=go1.26.0 +go mod init example.com +cmp go.mod go.mod.want-1.25.0 +rm go.mod +env TESTGO_VERSION=go1.22.2 +go mod init example.com +cmp go.mod go.mod.want-1.21.0 +rm go.mod +env TESTGO_VERSION=go1.25.0-xyzzy +go mod init example.com +cmp go.mod go.mod.want-1.24.0 +rm go.mod +env TESTGO_VERSION=go1.23rc3 +go mod init example.com +cmp go.mod go.mod.want-1.21.0 +rm go.mod +env TESTGO_VERSION=go1.18beta2 +go mod init example.com +cmp go.mod go.mod.want-1.16.0 +-- go.mod.want-1.26.0 -- +module example.com + +go 1.26.0 +-- go.mod.want-1.25.0 -- +module example.com + +go 1.25.0 +-- go.mod.want-1.24.0 -- +module example.com + +go 1.24.0 +-- go.mod.want-1.22.0 -- +module example.com + +go 1.22.0 +-- go.mod.want-1.21.0 -- +module example.com + +go 1.21.0 +-- go.mod.want-1.16.0 -- +module example.com + +go 1.16.0 diff --git a/src/cmd/go/testdata/script/work.txt b/src/cmd/go/testdata/script/work.txt index 3159792868..a2d3e85d03 100644 --- a/src/cmd/go/testdata/script/work.txt +++ b/src/cmd/go/testdata/script/work.txt @@ -1,4 +1,5 @@ [short] skip 'runs go run' +env TESTGO_VERSION=go1.26.0 ! go work init doesnotexist stderr 'go: directory doesnotexist does not exist' @@ -74,7 +75,7 @@ use ( ../src/a ) -- go.work.want -- -go $goversion +go 1.25.0 use ( ./a diff --git a/src/cmd/go/testdata/script/work_edit.txt b/src/cmd/go/testdata/script/work_edit.txt index 021346653f..2642d5b4c5 100644 --- a/src/cmd/go/testdata/script/work_edit.txt +++ b/src/cmd/go/testdata/script/work_edit.txt @@ -1,4 +1,5 @@ # Test editing go.work files. +env TESTGO_VERSION=go1.26.0 go work init m cmpenv go.work go.work.want_initial @@ -54,11 +55,11 @@ module m go 1.18 -- go.work.want_initial -- -go $goversion +go 1.25.0 use ./m -- go.work.want_use_n -- -go $goversion +go 1.25.0 use ( ./m diff --git a/src/cmd/go/testdata/script/work_init_path.txt b/src/cmd/go/testdata/script/work_init_path.txt index 0a2d3729fc..72a55fc3d4 100644 --- a/src/cmd/go/testdata/script/work_init_path.txt +++ b/src/cmd/go/testdata/script/work_init_path.txt @@ -2,7 +2,7 @@ # 'go work init . .. foo/bar' should produce a go.work file # with the same paths as 'go work init; go work use -r ..', # and it should have 'use .' rather than 'use ./.' inside. - +env TESTGO_VERSION=go1.23 cd dir go work init . .. foo/bar @@ -12,19 +12,19 @@ go work init go work use -r .. cmp go.work go.work.init -cmpenv go.work $WORK/go.work.want +cmp go.work $WORK/go.work.want -- go.mod -- module example go 1.18 -- dir/go.mod -- module example -go 1.18 +go 1.21.0 -- dir/foo/bar/go.mod -- module example -go 1.18 +go 1.21.0 -- $WORK/go.work.want -- -go $goversion +go 1.21.0 use ( . diff --git a/src/cmd/go/testdata/script/work_init_toolchain.txt b/src/cmd/go/testdata/script/work_init_toolchain.txt index 900ea2cf2f..012d22bea3 100644 --- a/src/cmd/go/testdata/script/work_init_toolchain.txt +++ b/src/cmd/go/testdata/script/work_init_toolchain.txt @@ -8,13 +8,13 @@ go mod edit -C m1_22_0 -go=1.22.0 -toolchain=go1.99.0 # work init writes the current Go version to the go line go work init -grep '^go 1.50$' go.work +grep '^go 1.48.0$' go.work ! grep toolchain go.work -# work init with older modules should leave go 1.50 in the go.work. +# work init with older modules should leave go 1.48.0 in the go.work. rm go.work go work init ./m1_22_0 -grep '^go 1.50$' go.work +grep '^go 1.48.0$' go.work ! grep toolchain go.work # work init with newer modules should bump go, @@ -31,5 +31,5 @@ env GOTOOLCHAIN=auto go work init ./m1_22_0 stderr '^go: m1_22_0'${/}'go.mod requires go >= 1.22.0; switching to go1.22.9$' cat go.work -grep '^go 1.22.9$' go.work +grep '^go 1.22.0$' go.work ! grep toolchain go.work diff --git a/src/cmd/go/testdata/script/work_init_version.txt b/src/cmd/go/testdata/script/work_init_version.txt new file mode 100644 index 0000000000..7472dedd48 --- /dev/null +++ b/src/cmd/go/testdata/script/work_init_version.txt @@ -0,0 +1,35 @@ +env TESTGO_VERSION=go1.28-devel +go work init +cmp go.work go.work.want-1.26.0 +rm go.work +env TESTGO_VERSION=go1.26.0 +go work init +cmp go.work go.work.want-1.25.0 +rm go.work +env TESTGO_VERSION=go1.22.2 +go work init +cmp go.work go.work.want-1.21.0 +rm go.work +env TESTGO_VERSION=go1.25.0-xyzzy +go work init +cmp go.work go.work.want-1.24.0 +rm go.work +env TESTGO_VERSION=go1.24rc3 +go work init +cmp go.work go.work.want-1.22.0 +rm go.work +env TESTGO_VERSION=go1.18beta2 +go work init +cmp go.work go.work.want-1.16.0 +-- go.work.want-1.26.0 -- +go 1.26.0 +-- go.work.want-1.25.0 -- +go 1.25.0 +-- go.work.want-1.24.0 -- +go 1.24.0 +-- go.work.want-1.22.0 -- +go 1.22.0 +-- go.work.want-1.21.0 -- +go 1.21.0 +-- go.work.want-1.16.0 -- +go 1.16.0 diff --git a/src/cmd/go/testdata/script/work_sync_toolchain.txt b/src/cmd/go/testdata/script/work_sync_toolchain.txt index 989d6bb792..28b45556cd 100644 --- a/src/cmd/go/testdata/script/work_sync_toolchain.txt +++ b/src/cmd/go/testdata/script/work_sync_toolchain.txt @@ -11,13 +11,14 @@ go mod init -C m1_24_rc0 go mod edit -C m1_24_rc0 -go=1.24rc0 -toolchain=go1.99.2 go work init ./m1_22_0 ./m1_22_1 -grep '^go 1.50$' go.work +cat go.work +grep '^go 1.48.0$' go.work ! grep toolchain go.work -# work sync with older modules should leave go 1.50 in the go.work. +# work sync with older modules should leave go 1.48.0 in the go.work. go work sync cat go.work -grep '^go 1.50$' go.work +grep '^go 1.48.0$' go.work ! grep toolchain go.work # work sync with newer modules should update go 1.21 -> 1.22.1 and toolchain -> go1.22.9 in go.work diff --git a/src/cmd/go/testdata/script/work_use_toolchain.txt b/src/cmd/go/testdata/script/work_use_toolchain.txt index d81e4a4c3e..8ab2a99013 100644 --- a/src/cmd/go/testdata/script/work_use_toolchain.txt +++ b/src/cmd/go/testdata/script/work_use_toolchain.txt @@ -11,12 +11,12 @@ go mod init -C m1_24_rc0 go mod edit -C m1_24_rc0 -go=1.24rc0 -toolchain=go1.99.2 go work init -grep '^go 1.50$' go.work +grep '^go 1.48.0$' go.work ! grep toolchain go.work -# work use with older modules should leave go 1.50 in the go.work. +# work use with older modules should leave go 1.48.0 in the go.work. go work use ./m1_22_0 -grep '^go 1.50$' go.work +grep '^go 1.48.0$' go.work ! grep toolchain go.work # work use with newer modules should bump go and toolchain, -- 2.52.0