diff --git a/src/cmd/go/internal/modload/buildlist.go b/src/cmd/go/internal/modload/buildlist.go index 7cebb9f265..76500ab33f 100644 --- a/src/cmd/go/internal/modload/buildlist.go +++ b/src/cmd/go/internal/modload/buildlist.go @@ -315,9 +315,13 @@ func readModGraph(ctx context.Context, pruning modPruning, roots []module.Versio mg.g.Require(MainModules.mustGetSingleMainModule(), roots) } + type dedupKey struct { + m module.Version + pruning modPruning + } var ( - loadQueue = par.NewQueue(runtime.GOMAXPROCS(0)) - loadingUnpruned sync.Map // module.Version → nil; the set of modules that have been or are being loaded via roots that do not support pruning + loadQueue = par.NewQueue(runtime.GOMAXPROCS(0)) + loading sync.Map // dedupKey → nil; the set of modules that have been or are being loaded ) // loadOne synchronously loads the explicit requirements for module m. @@ -345,13 +349,11 @@ func readModGraph(ctx context.Context, pruning modPruning, roots []module.Versio return } - if pruning == unpruned { - if _, dup := loadingUnpruned.LoadOrStore(m, nil); dup { - // m has already been enqueued for loading. Since unpruned loading may - // follow cycles in the requirement graph, we need to return early - // to avoid making the load queue infinitely long. - return - } + if _, dup := loading.LoadOrStore(dedupKey{m, pruning}, nil); dup { + // m has already been enqueued for loading. Since unpruned loading may + // follow cycles in the requirement graph, we need to return early + // to avoid making the load queue infinitely long. + return } loadQueue.Add(func() { diff --git a/src/cmd/go/testdata/script/mod_get_issue60490.txt b/src/cmd/go/testdata/script/mod_get_issue60490.txt new file mode 100644 index 0000000000..e0ac26a875 --- /dev/null +++ b/src/cmd/go/testdata/script/mod_get_issue60490.txt @@ -0,0 +1,48 @@ +# Regression test for https://go.dev/issue/60490: 'go get' should not cause an +# infinite loop for cycles introduced in the pruned module graph. + +go get example.net/c@v0.1.0 + +-- go.mod -- +module example + +go 1.19 + +require ( + example.net/a v0.1.0 + example.net/b v0.1.0 +) + +replace ( + example.net/a v0.1.0 => ./a1 + example.net/a v0.2.0 => ./a2 + example.net/b v0.1.0 => ./b1 + example.net/b v0.2.0 => ./b2 + example.net/c v0.1.0 => ./c1 +) +-- a1/go.mod -- +module example.net/a + +go 1.19 +-- a2/go.mod -- +module example.net/a + +go 1.19 + +require example.net/b v0.2.0 +-- b1/go.mod -- +module example.net/b + +go 1.19 +-- b2/go.mod -- +module example.net/b + +go 1.19 + +require example.net/a v0.2.0 +-- c1/go.mod -- +module example.net/c + +go 1.19 + +require example.net/a v0.2.0