Skip to content

Commit e53edaf

Browse files
author
Bryan C. Mills
committed
cmd/go/internal/mvs: recompute build list in Reqs before minimizing
modload.MinReqs was passing modload.buildList to mvs.Reqs explicitly, apparently as an optimization. However, we do not always have the invariant that modload.buildList is complete: in particular, 'go mod tidy' begins by reducing modload.buildList to only the set of modules that provide packages to the build, which may be substantially smaller than the final build list. Other operations, such as 'go mod graph', do not load the entire import graph, and therefore call Reqs with the unreduced build list. Since Reqs retains modules according to a post-order traversal of the list, an incomplete list may produce a different traversal order — and therefore a different minimal solution, when multiple minimal solutions exist. That caused 'go mod tidy' to produce different output from other 'go' subcommands when certain patterns of dependencies are present. Since passing in the build list is only an optimization anyway, remove the parameter and recompute the actual (complete) list at the beginning of mvs.Reqs itself. That way, it is guaranteed to be complete and in canonical order. Fixes #34086 Change-Id: I3101bb81a1853c4a5e773010da3e44d2d90a570c Reviewed-on: https://go-review.googlesource.com/c/go/+/193397 Run-TryBot: Bryan C. Mills <[email protected]> TryBot-Result: Gobot Gobot <[email protected]> Reviewed-by: Jay Conrod <[email protected]>
1 parent 5d548f1 commit e53edaf

File tree

4 files changed

+109
-11
lines changed

4 files changed

+109
-11
lines changed

src/cmd/go/internal/modload/init.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -634,7 +634,7 @@ func MinReqs() mvs.Reqs {
634634
direct = append(direct, m.Path)
635635
}
636636
}
637-
min, err := mvs.Req(Target, buildList, direct, Reqs())
637+
min, err := mvs.Req(Target, direct, Reqs())
638638
if err != nil {
639639
base.Fatalf("go: %v", err)
640640
}

src/cmd/go/internal/mvs/mvs.go

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -250,10 +250,15 @@ func buildList(target module.Version, reqs Reqs, upgrade func(module.Version) (m
250250
return list, nil
251251
}
252252

253-
// Req returns the minimal requirement list for the target module
254-
// that results in the given build list, with the constraint that all
255-
// module paths listed in base must appear in the returned list.
256-
func Req(target module.Version, list []module.Version, base []string, reqs Reqs) ([]module.Version, error) {
253+
// Req returns the minimal requirement list for the target module,
254+
// with the constraint that all module paths listed in base must
255+
// appear in the returned list.
256+
func Req(target module.Version, base []string, reqs Reqs) ([]module.Version, error) {
257+
list, err := BuildList(target, reqs)
258+
if err != nil {
259+
return nil, err
260+
}
261+
257262
// Note: Not running in parallel because we assume
258263
// that list came from a previous operation that paged
259264
// in all the requirements, so there's no I/O to overlap now.

src/cmd/go/internal/mvs/mvs_test.go

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -280,6 +280,20 @@ D2:
280280
build A: A B1 C1 D1
281281
upgrade* A: A B2 C2 D2
282282
283+
# Cycles with multiple possible solutions.
284+
# (golang.org/issue/34086)
285+
name: cycle3
286+
M: A1 C2
287+
A1: B1
288+
B1: C1
289+
B2: C2
290+
C1:
291+
C2: B2
292+
build M: M A1 B2 C2
293+
req M: A1 B2
294+
req M A: A1 B2
295+
req M C: A1 C2
296+
283297
# Requirement minimization.
284298
285299
name: req1
@@ -390,7 +404,15 @@ func Test(t *testing.T) {
390404
fns = append(fns, func(t *testing.T) {
391405
list, err := Upgrade(m(kf[1]), reqs, ms(kf[2:])...)
392406
if err == nil {
393-
list, err = Req(m(kf[1]), list, nil, reqs)
407+
// Copy the reqs map, but substitute the upgraded requirements in
408+
// place of the target's original requirements.
409+
upReqs := make(reqsMap, len(reqs))
410+
for m, r := range reqs {
411+
upReqs[m] = r
412+
}
413+
upReqs[m(kf[1])] = list
414+
415+
list, err = Req(m(kf[1]), nil, upReqs)
394416
}
395417
checkList(t, key, list, err, val)
396418
})
@@ -418,11 +440,7 @@ func Test(t *testing.T) {
418440
t.Fatalf("req takes at least one argument: %q", line)
419441
}
420442
fns = append(fns, func(t *testing.T) {
421-
list, err := BuildList(m(kf[1]), reqs)
422-
if err != nil {
423-
t.Fatal(err)
424-
}
425-
list, err = Req(m(kf[1]), list, kf[2:], reqs)
443+
list, err := Req(m(kf[1]), kf[2:], reqs)
426444
checkList(t, key, list, err, val)
427445
})
428446
continue
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
# Regression test for https://golang.org/issue/34086:
2+
# 'go mod tidy' produced different go.mod file from other
3+
# subcommands when certain kinds of cycles were present
4+
# in the build graph.
5+
6+
env GO111MODULE=on
7+
8+
cp go.mod go.mod.orig
9+
go mod tidy
10+
cmp go.mod go.mod.orig
11+
12+
# If the go.mod file is already tidy, 'go mod graph' should not modify it.
13+
go mod graph
14+
cmp go.mod go.mod.orig
15+
16+
-- go.mod --
17+
module root
18+
19+
go 1.13
20+
21+
replace (
22+
a v0.1.0 => ./a1
23+
b v0.1.0 => ./b1
24+
b v0.2.0 => ./b2
25+
c v0.1.0 => ./c1
26+
c v0.2.0 => ./c2
27+
)
28+
29+
require (
30+
a v0.1.0
31+
b v0.2.0 // indirect
32+
)
33+
-- main.go --
34+
package main
35+
36+
import _ "a"
37+
38+
func main() {}
39+
40+
-- a1/go.mod --
41+
module a
42+
43+
go 1.13
44+
45+
require b v0.1.0
46+
-- a1/a.go --
47+
package a
48+
49+
import _ "c"
50+
-- b1/go.mod --
51+
module b
52+
53+
go 1.13
54+
55+
require c v0.1.0
56+
-- b2/go.mod --
57+
module b
58+
59+
go 1.13
60+
61+
require c v0.2.0
62+
-- c1/go.mod --
63+
module c
64+
65+
go 1.13
66+
-- c2/c.go --
67+
package c
68+
-- c2/go.mod --
69+
module c
70+
71+
go 1.13
72+
73+
require b v0.2.0
74+
-- c2/c.go --
75+
package c

0 commit comments

Comments
 (0)