Skip to content

Commit

Permalink
cmd/go/internal/vgo: track directly-used vs indirectly-used modules
Browse files Browse the repository at this point in the history
A cleanup pass in mvs.BuildList discards modules that are not reachable
in the requirement graph as satisfied for this build. For example, suppose:

	A -> B1, C1
	B1 -> D1
	B2 -> nothing
	C1 -> nothing
	D1 -> nothing
	D2 -> nothing

The effective build list is A, B1, C1, D1 (no cleanup possible).

Suppose that we update from B1 to B2. The effective build list
becomes A, B2, C1, D1, and since there is no path through those
module versions from A to D, the cleanup pass drops D.

This cleanup, which is not in https://research.swtch.com/vgo-mvs,
aims to avoid user confusion by not listing irrelevant modules in
the output of commands like "vgo list -m all".

Unfortunately, the cleanup is not sound in general, because
there is no guarantee all of A's needs are listed as direct requirements.
For example, maybe A imports D. In that case, dropping D and then
building A will re-add the latest version of D (D2 instead of D1).
The most common time this happens is after an upgrade.

The fix is to make sure that go.mod does list all of the modules
required directly by A, and to make sure that the go.mod
minimizer (Algorithm R in the blog post) does not remove
direct requirements in the name of simplifying go.mod.

The way this is done is to annotate the requirements NOT used
directly by A with a known comment, "// indirect".

For example suppose A imports rsc.io/quote. Then the go.mod
looks like it always has:

	module m

	require rsc.io/quote v1.5.2

But now suppose we upgrade our packages to their latest versions.
Then go.mod becomes:

	module m

	require (
        		golang.org/x/text v0.3.0 // indirect
        		rsc.io/quote v1.5.2
        		rsc.io/sampler v1.99.99 // indirect
        	)

The "// indirect" comments indicate that this requirement is used
only to upgrade something needed outside module m, not to satisfy
any packages in module m itself.

Vgo adds and removes these comments automatically.
If we add a direct import of golang.org/x/text to some package in m,
then the first time we build that package vgo strips the "// indirect"
on the golang.org/x/text requirement line. If we then remove that
package, the requirement remains listed as direct (the conservative
choice) until the next "vgo mod -sync", which considers all packages
in m and can mark the requirement indirect again.
Algorithm R is modified to be given a set of import paths that must
be preserved in the final output (all the ones not marked // indirect).

Maintenance of this extra information makes the cleanup pass safe.

Seeing all directly-imported modules in go.mod
and distinguishing between directly- and indirectly-imported modules in go.mod
are two of the most commonly-requested features,
so it's extra nice that the fix for the cleanup-induced bug
makes go.mod closer to what users expect.

Fixes golang/go#24042.
Fixes golang/go#25371.
Fixes golang/go#25969.

Change-Id: I4ed0729b867723fe90e836c2325f740b55b2b27b
Reviewed-on: https://go-review.googlesource.com/121304
Reviewed-by: Bryan C. Mills <bcmills@google.com>
  • Loading branch information
rsc committed Jul 10, 2018
1 parent 6296b2f commit 552f8db
Show file tree
Hide file tree
Showing 14 changed files with 252 additions and 47 deletions.
1 change: 1 addition & 0 deletions vendor/cmd/go/internal/list/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,7 @@ applied to a Go struct, but now a Module struct:
Time *time.Time // time version was created
Update *Module // available update, if any (with -u)
Main bool // is this the main module?
Indirect bool // is this module only an indirect dependency of main module?
Dir string // directory holding files for this module, if any
Error *ModuleError // error loading module
}
Expand Down
25 changes: 21 additions & 4 deletions vendor/cmd/go/internal/modcmd/mod.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,9 +84,20 @@ Go types:
type GoMod struct {
Module Module
Require []Module
Require []Require
Exclude []Module
Replace []struct{ Old, New Module }
Replace []Replace
}
type Require struct {
Path string
Version string
Indirect bool
}
type Replace string {
Old Module
New Module
}
Note that this only describes the go.mod file itself, not other modules
Expand Down Expand Up @@ -432,11 +443,17 @@ func flagDropReplace(arg string) {
// fileJSON is the -json output data structure.
type fileJSON struct {
Module module.Version
Require []module.Version
Require []requireJSON
Exclude []module.Version
Replace []replaceJSON
}

type requireJSON struct {
Path string
Version string `json:",omitempty"`
Indirect bool `json:",omitempty"`
}

type replaceJSON struct {
Old module.Version
New module.Version
Expand All @@ -449,7 +466,7 @@ func modPrintJSON() {
var f fileJSON
f.Module = modFile.Module.Mod
for _, r := range modFile.Require {
f.Require = append(f.Require, r.Mod)
f.Require = append(f.Require, requireJSON{Path: r.Mod.Path, Version: r.Mod.Version, Indirect: r.Indirect})
}
for _, x := range modFile.Exclude {
f.Exclude = append(f.Exclude, x.Mod)
Expand Down
2 changes: 1 addition & 1 deletion vendor/cmd/go/internal/modconv/convert.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ func ConvertLegacyConfig(f *modfile.File, file string, data []byte) error {
}
sort.Strings(paths)
for _, path := range paths {
f.AddNewRequire(path, need[path])
f.AddNewRequire(path, need[path], false)
}

for _, r := range mf.Replace {
Expand Down
79 changes: 68 additions & 11 deletions vendor/cmd/go/internal/modfile/rule.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,9 @@ type Module struct {

// A Require is a single require statement.
type Require struct {
Mod module.Version
Syntax *Line
Mod module.Version
Indirect bool // has "// indirect" comment
Syntax *Line
}

// An Exclude is a single exclude statement.
Expand Down Expand Up @@ -182,8 +183,9 @@ func (f *File) add(errs *bytes.Buffer, line *Line, verb string, args []string, f
}
if verb == "require" {
f.Require = append(f.Require, &Require{
Mod: module.Version{Path: s, Version: v},
Syntax: line,
Mod: module.Version{Path: s, Version: v},
Syntax: line,
Indirect: isIndirect(line),
})
} else {
f.Exclude = append(f.Exclude, &Exclude{
Expand Down Expand Up @@ -253,6 +255,54 @@ func (f *File) add(errs *bytes.Buffer, line *Line, verb string, args []string, f
}
}

// isIndirect reports whether line has a "// indirect" comment,
// meaning it is in go.mod only for its effect on indirect dependencies,
// so that it can be dropped entirely once the effective version of the
// indirect dependency reaches the given minimum version.
func isIndirect(line *Line) bool {
if len(line.Suffix) == 0 {
return false
}
f := strings.Fields(line.Suffix[0].Token)
return (len(f) == 2 && f[1] == "indirect" || len(f) > 2 && f[1] == "indirect;") && f[0] == "//"
}

// setIndirect sets line to have (or not have) a "// indirect" comment.
func setIndirect(line *Line, indirect bool) {
if isIndirect(line) == indirect {
return
}
if indirect {
// Adding comment.
if len(line.Suffix) == 0 {
// New comment.
line.Suffix = []Comment{{Token: "// indirect", Suffix: true}}
return
}
// Insert at beginning of existing comment.
com := &line.Suffix[0]
space := " "
if len(com.Token) > 2 && com.Token[2] == ' ' || com.Token[2] == '\t' {
space = ""
}
com.Token = "// indirect;" + space + com.Token[2:]
return
}

// Removing comment.
f := strings.Fields(line.Suffix[0].Token)
if len(f) == 2 {
// Remove whole comment.
line.Suffix = nil
return
}

// Remove comment prefix.
com := &line.Suffix[0]
i := strings.Index(com.Token, "indirect;")
com.Token = "//" + com.Token[i+len("indirect;"):]
}

// IsDirectoryPath reports whether the given path should be interpreted
// as a directory path. Just like on the go command line, relative paths
// and rooted paths are directory paths; the rest are module paths.
Expand Down Expand Up @@ -403,24 +453,29 @@ func (f *File) AddRequire(path, vers string) error {
}

if need {
f.AddNewRequire(path, vers)
f.AddNewRequire(path, vers, false)
}
return nil
}

func (f *File) AddNewRequire(path, vers string) {
f.Require = append(f.Require, &Require{module.Version{Path: path, Version: vers}, f.Syntax.addLine(nil, "require", AutoQuote(path), vers)})
func (f *File) AddNewRequire(path, vers string, indirect bool) {
line := f.Syntax.addLine(nil, "require", AutoQuote(path), vers)
setIndirect(line, indirect)
f.Require = append(f.Require, &Require{module.Version{Path: path, Version: vers}, indirect, line})
}

func (f *File) SetRequire(req []module.Version) {
func (f *File) SetRequire(req []*Require) {
need := make(map[string]string)
for _, m := range req {
need[m.Path] = m.Version
indirect := make(map[string]bool)
for _, r := range req {
need[r.Mod.Path] = r.Mod.Version
indirect[r.Mod.Path] = r.Indirect
}

for _, r := range f.Require {
if v, ok := need[r.Mod.Path]; ok {
r.Mod.Version = v
r.Indirect = indirect[r.Mod.Path]
}
}

Expand All @@ -434,6 +489,7 @@ func (f *File) SetRequire(req []module.Version) {
if p, err := parseString(&line.Token[0]); err == nil && need[p] != "" {
line.Token[1] = need[p]
delete(need, p)
setIndirect(line, indirect[p])
newLines = append(newLines, line)
}
}
Expand All @@ -448,6 +504,7 @@ func (f *File) SetRequire(req []module.Version) {
if p, err := parseString(&stmt.Token[1]); err == nil && need[p] != "" {
stmt.Token[2] = need[p]
delete(need, p)
setIndirect(stmt, indirect[p])
} else {
continue // drop stmt
}
Expand All @@ -458,7 +515,7 @@ func (f *File) SetRequire(req []module.Version) {
f.Syntax.Stmt = newStmts

for path, vers := range need {
f.AddNewRequire(path, vers)
f.AddNewRequire(path, vers, indirect[path])
}
f.SortBlocks()
}
Expand Down
17 changes: 9 additions & 8 deletions vendor/cmd/go/internal/modinfo/info.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,15 @@ import "time"
// and the fields are documented in the help text in ../list/list.go

type ModulePublic struct {
Path string `json:",omitempty"` // module path
Version string `json:",omitempty"` // module version
Replace *ModulePublic `json:",omitempty"` // replaced by this module
Time *time.Time `json:",omitempty"` // time version was created
Update *ModulePublic `json:",omitempty"` // available update (with -u)
Main bool `json:",omitempty"` // is this the main module?
Dir string `json:",omitempty"` // directory holding local copy of files, if any
Error *ModuleError `json:",omitempty"` // error loading module
Path string `json:",omitempty"` // module path
Version string `json:",omitempty"` // module version
Replace *ModulePublic `json:",omitempty"` // replaced by this module
Time *time.Time `json:",omitempty"` // time version was created
Update *ModulePublic `json:",omitempty"` // available update (with -u)
Main bool `json:",omitempty"` // is this the main module?
Indirect bool `json:",omitempty"` // module is only indirectly needed by main module
Dir string `json:",omitempty"` // directory holding local copy of files, if any
Error *ModuleError `json:",omitempty"` // error loading module
}

type ModuleError struct {
Expand Down
2 changes: 1 addition & 1 deletion vendor/cmd/go/internal/module/module.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ type Version struct {
// and uses Version = "".
// Second, during MVS calculations the version "none" is used
// to represent the decision to take no version of a given module.
Version string
Version string `json:",omitempty"`
}

// Check checks that a given module path, version pair is valid.
Expand Down
12 changes: 10 additions & 2 deletions vendor/cmd/go/internal/mvs/mvs.go
Original file line number Diff line number Diff line change
Expand Up @@ -142,8 +142,9 @@ func buildList(target module.Version, reqs Reqs, upgrade func(module.Version) mo
}

// Req returns the minimal requirement list for the target module
// that results in the given build list.
func Req(target module.Version, list []module.Version, reqs Reqs) ([]module.Version, error) {
// that results in the given build list, with the constraint that all
// module paths listed in base must appear in the returned list.
func Req(target module.Version, list []module.Version, base []string, reqs Reqs) ([]module.Version, error) {
// Note: Not running in parallel because we assume
// that list came from a previous operation that paged
// in all the requirements, so there's no I/O to overlap now.
Expand Down Expand Up @@ -197,7 +198,14 @@ func Req(target module.Version, list []module.Version, reqs Reqs) ([]module.Vers
max[m.Path] = m.Version
}
}
// First walk the base modules that must be listed.
var min []module.Version
for _, path := range base {
m := module.Version{Path: path, Version: max[path]}
min = append(min, m)
walk(m)
}
// Now the reverse postorder to bring in anything else.
for i := len(postorder) - 1; i >= 0; i-- {
m := postorder[i]
if max[m.Path] != m.Version {
Expand Down
44 changes: 40 additions & 4 deletions vendor/cmd/go/internal/mvs/mvs_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,13 @@ D2:
build A: A B1 C1 D1
upgrade* A: A B2 C2
name: simplify
A: B1 C1
B1: C2
C1: D1
C2:
build A: A B1 C2
name: up1
A: B1 C1
B1:
Expand Down Expand Up @@ -259,6 +266,22 @@ C2:
D2:
build A: A B1
upgrade* A: A B2
# Requirement minimization.
name: req1
A: B1 C1 D1 E1 F1
B1: C1 E1 F1
req A: B1 D1
req A C: B1 C1 D1
name: req2
A: G1 H1
G1: H1
H1: G1
req A: G1
req A G: G1
req A H: H1
`

func Test(t *testing.T) {
Expand Down Expand Up @@ -341,19 +364,19 @@ func Test(t *testing.T) {
continue
case "upgradereq":
if len(kf) < 2 {
t.Fatalf("upgrade takes at least one arguments: %q", line)
t.Fatalf("upgrade takes at least one argument: %q", line)
}
fns = append(fns, func(t *testing.T) {
list, err := Upgrade(m(kf[1]), reqs, ms(kf[2:])...)
if err == nil {
list, err = Req(m(kf[1]), list, reqs)
list, err = Req(m(kf[1]), list, nil, reqs)
}
checkList(t, key, list, err, val)
})
continue
case "upgrade":
if len(kf) < 2 {
t.Fatalf("upgrade takes at least one arguments: %q", line)
t.Fatalf("upgrade takes at least one argument: %q", line)
}
fns = append(fns, func(t *testing.T) {
list, err := Upgrade(m(kf[1]), reqs, ms(kf[2:])...)
Expand All @@ -362,13 +385,26 @@ func Test(t *testing.T) {
continue
case "downgrade":
if len(kf) < 2 {
t.Fatalf("downgrade takes at least one arguments: %q", line)
t.Fatalf("downgrade takes at least one argument: %q", line)
}
fns = append(fns, func(t *testing.T) {
list, err := Downgrade(m(kf[1]), reqs, ms(kf[1:])...)
checkList(t, key, list, err, val)
})
continue
case "req":
if len(kf) < 2 {
t.Fatalf("req takes at least one argument: %q", line)
}
fns = append(fns, func(t *testing.T) {
list, err := BuildList(m(kf[1]), reqs)
if err != nil {
t.Fatal(err)
}
list, err = Req(m(kf[1]), list, kf[2:], reqs)
checkList(t, key, list, err, val)
})
continue
}
if len(kf) == 1 && 'A' <= key[0] && key[0] <= 'Z' {
var rs []module.Version
Expand Down
Loading

0 comments on commit 552f8db

Please sign in to comment.