"cmd/go/internal/mvs"
"cmd/go/internal/par"
"context"
+ "errors"
"fmt"
"os"
"reflect"
// roots) until the set of roots has converged.
func tidyPrunedRoots(ctx context.Context, mainModule module.Version, direct map[string]bool, pkgs []*loadPkg) (*Requirements, error) {
var (
- roots []module.Version
- pathIncluded = map[string]bool{mainModule.Path: true}
+ roots []module.Version
+ pathIsRoot = map[string]bool{mainModule.Path: true}
)
// We start by adding roots for every package in "all".
//
if !pkg.flags.has(pkgInAll) {
continue
}
- if pkg.fromExternalModule() && !pathIncluded[pkg.mod.Path] {
+ if pkg.fromExternalModule() && !pathIsRoot[pkg.mod.Path] {
roots = append(roots, pkg.mod)
- pathIncluded[pkg.mod.Path] = true
+ pathIsRoot[pkg.mod.Path] = true
}
queue = append(queue, pkg)
queued[pkg] = true
queue = append(queue, pkg.test)
queued[pkg.test] = true
}
- if !pathIncluded[m.Path] {
+
+ if !pathIsRoot[m.Path] {
if s := mg.Selected(m.Path); cmpVersion(s, m.Version) < 0 {
roots = append(roots, m)
+ pathIsRoot[m.Path] = true
}
- pathIncluded[m.Path] = true
}
}
}
}
+ roots = tidy.rootModules
_, err := tidy.Graph(ctx)
if err != nil {
return nil, err
}
+
+ // We try to avoid adding explicit requirements for test-only dependencies of
+ // packages in external modules. However, if we drop the explicit
+ // requirements, that may change an import from unambiguous (due to lazy
+ // module loading) to ambiguous (because lazy module loading no longer
+ // disambiguates it). For any package that has become ambiguous, we try
+ // to fix it by promoting its module to an explicit root.
+ // (See https://go.dev/issue/60313.)
+ q := par.NewQueue(runtime.GOMAXPROCS(0))
+ for {
+ var disambiguateRoot sync.Map
+ for _, pkg := range pkgs {
+ if pkg.mod.Path == "" || pathIsRoot[pkg.mod.Path] {
+ // Lazy module loading will cause pkg.mod to be checked before any other modules
+ // that are only indirectly required. It is as unambiguous as possible.
+ continue
+ }
+ pkg := pkg
+ q.Add(func() {
+ skipModFile := true
+ _, _, _, _, err := importFromModules(ctx, pkg.path, tidy, nil, skipModFile)
+ if aie := (*AmbiguousImportError)(nil); errors.As(err, &aie) {
+ disambiguateRoot.Store(pkg.mod, true)
+ }
+ })
+ }
+ <-q.Idle()
+
+ disambiguateRoot.Range(func(k, _ any) bool {
+ m := k.(module.Version)
+ roots = append(roots, m)
+ pathIsRoot[m.Path] = true
+ return true
+ })
+
+ if len(roots) > len(tidy.rootModules) {
+ module.Sort(roots)
+ tidy = newRequirements(pruned, roots, tidy.direct)
+ _, err = tidy.Graph(ctx)
+ if err != nil {
+ return nil, err
+ }
+ // Adding these roots may have pulled additional modules into the module
+ // graph, causing additional packages to become ambiguous. Keep iterating
+ // until we reach a fixed point.
+ continue
+ }
+
+ break
+ }
+
return tidy, nil
}
--- /dev/null
+# Regression test for https://go.dev/issue/60313: 'go mod tidy' did not preserve
+# dependencies needed to prevent 'ambiguous import' errors in external test
+# dependencies.
+
+cp go.mod go.mod.orig
+go mod tidy
+cmp go.mod go.mod.orig
+
+-- go.mod --
+module example
+
+go 1.20
+
+require (
+ example.net/a v0.1.0
+ example.net/b v0.1.0
+)
+
+require example.net/outer/inner v0.1.0 // indirect
+
+replace (
+ example.net/a v0.1.0 => ./a
+ example.net/b v0.1.0 => ./b
+ example.net/outer v0.1.0 => ./outer
+ example.net/outer/inner v0.1.0 => ./inner
+)
+-- example.go --
+package example
+
+import (
+ _ "example.net/a"
+ _ "example.net/b"
+)
+-- a/go.mod --
+module example.net/a
+
+go 1.20
+
+require example.net/outer/inner v0.1.0
+-- a/a.go --
+package a
+-- a/a_test.go --
+package a_test
+
+import _ "example.net/outer/inner"
+-- b/go.mod --
+module example.net/b
+
+go 1.20
+
+require example.net/outer v0.1.0
+-- b/b.go --
+package b
+-- b/b_test.go --
+package b_test
+
+import _ "example.net/outer/inner"
+-- inner/go.mod --
+module example.net/outer/inner
+
+go 1.20
+-- inner/inner.go --
+package inner
+-- outer/go.mod --
+module example.net/outer
+
+go 1.20
+-- outer/inner/inner.go --
+package inner