cmd/go: loosen go work sync version requirements

In go work sync, we may resolve lower versions of modules for the
workspace than what we would in an individual module because replace
directives in the workspace can hide requirements that would bump
versions up. If we try to force those versions for each module we'll run
into a conflict. Instead, make the new requirements additive so it only
bumps the versions up.

We still have the issue that we originally resolve the modules using the
workspace's replaces but later apply the requirements using the modules,
but that's unavoidable so we just have to work around it. The important
thing is that we now don't throw away the error and silently avoid
changing the go.mod file. Additionally, in the case there are errors,
we'll check for them ahead of time and only write out the synced go.mod
files if there were no errors.

Fixes #65363

Change-Id: I25bd98719eb5de0530d50eba3ccf4b2a6a6a6964
Reviewed-on: https://go-review.googlesource.com/c/go/+/762602
Reviewed-by: Dmitri Shuralyov <dmitshur@golang.org>
Reviewed-by: Michael Matloob <matloob@google.com>
TryBot-Bypass: Michael Matloob <matloob@golang.org>
Reviewed-by: Dmitri Shuralyov <dmitshur@google.com>
This commit is contained in:
Michael Matloob 2026-04-03 18:45:17 -04:00
parent a221442229
commit 8191cd8868
4 changed files with 115 additions and 17 deletions

View file

@ -59,7 +59,7 @@ func runSync(ctx context.Context, cmd *base.Command, args []string) {
if err != nil {
toolchain.SwitchOrFatal(moduleLoader, ctx, err)
}
mustSelectFor := map[module.Version][]module.Version{}
addFor := map[module.Version][]module.Version{}
mms := moduleLoader.MainModules
@ -78,22 +78,23 @@ func runSync(ctx context.Context, cmd *base.Command, args []string) {
opts.MainModule = module.Version{} // reset
var (
mustSelect []module.Version
inMustSelect = map[module.Version]bool{}
addReq []module.Version
inAddReq = map[module.Version]bool{}
)
for _, pkg := range pkgs {
if r := moduleLoader.PackageModule(pkg); r.Version != "" && !inMustSelect[r] {
if r := moduleLoader.PackageModule(pkg); r.Version != "" && !inAddReq[r] {
// r has a known version, so force that version.
mustSelect = append(mustSelect, r)
inMustSelect[r] = true
addReq = append(addReq, r)
inAddReq[r] = true
}
}
gover.ModSort(mustSelect) // ensure determinism
mustSelectFor[m] = mustSelect
gover.ModSort(addReq) // ensure determinism
addFor[m] = addReq
}
workFilePath := modload.WorkFilePath(moduleLoader) // save go.work path because EnterModule clobbers it.
workFilePath := modload.WorkFilePath(moduleLoader)
var loaders []*modload.Loader
var goV string
for _, m := range mms.Versions() {
if mms.ModRoot(m) == "" && m.Path == "command-line-arguments" {
@ -103,9 +104,9 @@ func runSync(ctx context.Context, cmd *base.Command, args []string) {
continue
}
// Use EnterModule to reset the global state in modload to be in
// single-module mode using the modroot of m.
modload.EnterModule(moduleLoader, ctx, mms.ModRoot(m))
// Use EnterModule to make a loader with a single work module.
loader := modload.NewLoader()
modload.EnterModule(loader, ctx, mms.ModRoot(m))
// Edit the build list in the same way that 'go get' would if we
// requested the relevant module versions explicitly.
@ -115,12 +116,12 @@ func runSync(ctx context.Context, cmd *base.Command, args []string) {
// so we don't write some go.mods with the "before" toolchain
// and others with the "after" toolchain. If nothing else, that
// discrepancy could show up in auto-recorded toolchain lines.
changed, err := modload.EditBuildList(moduleLoader, ctx, nil, mustSelectFor[m])
changed, err := modload.EditBuildList(loader, ctx, addFor[m], nil)
if err != nil {
continue
base.Fatal(err)
}
if changed {
modload.LoadPackages(moduleLoader, ctx, modload.PackageOpts{
modload.LoadPackages(loader, ctx, modload.PackageOpts{
Tags: imports.AnyTags(),
Tidy: true,
VendorModulesInGOROOTSrc: true,
@ -130,9 +131,17 @@ func runSync(ctx context.Context, cmd *base.Command, args []string) {
SilenceMissingStdImports: true,
SilencePackageErrors: true,
}, "all")
modload.WriteGoMod(moduleLoader, ctx, modload.WriteOpts{})
// Run UpdateGoModFromReqs before we run WriteGoMod so we can catch errors in it early.
if _, _, _, err := modload.UpdateGoModFromReqs(loader, ctx, modload.WriteOpts{}); err != nil {
base.Fatal(err)
}
loaders = append(loaders, loader)
}
goV = gover.Max(goV, moduleLoader.MainModules.GoVersion(moduleLoader))
goV = gover.Max(goV, loader.MainModules.GoVersion(loader))
}
base.ExitIfErrors()
for _, loader := range loaders {
modload.WriteGoMod(loader, ctx, modload.WriteOpts{})
}
wf, err := modload.ReadWorkFile(workFilePath)

View file

@ -0,0 +1,12 @@
-- .mod --
module example.com/syncreplace
go 1.27
require rsc.io/quote v1.0.0
-- syncreplace.go --
package syncreplace
import _ "rsc.io/quote"
-- .info --
{"Version":"v1.0.0"}

View file

@ -0,0 +1,12 @@
-- .mod --
module example.com/syncreplace
go 1.27
require rsc.io/quote v1.1.0
-- syncreplace.go --
package syncreplace
import _ "rsc.io/quote"
-- .info --
{"Version":"v1.1.0"}

View file

@ -0,0 +1,65 @@
go work sync
cmp a/go.mod a/go.mod.want
cmp b/go.mod b/go.mod.want
-- go.work --
go 1.27
use (
./a
./b
)
-- a/go.mod --
module example.com/a
go 1.27
replace example.com/syncreplace v1.1.0 => example.com/syncreplace v1.0.0
require (
example.com/syncreplace v1.1.0
rsc.io/quote v1.0.0
)
-- a/go.mod.want --
module example.com/a
go 1.27
replace example.com/syncreplace v1.1.0 => example.com/syncreplace v1.0.0
require (
example.com/syncreplace v1.1.0
rsc.io/quote v1.0.0
)
-- a/a.go --
package a
import (
_ "example.com/syncreplace"
_ "rsc.io/quote"
)
-- b/go.mod --
module example.com/b
go 1.27
require (
example.com/syncreplace v1.1.0
rsc.io/quote v1.0.0
)
-- b/go.mod.want --
module example.com/b
go 1.27
require (
example.com/syncreplace v1.1.0
rsc.io/quote v1.1.0
)
-- b/b.go --
package b
import (
_ "example.com/syncreplace"
_ "rsc.io/quote"
)