Skip to content

Commit

Permalink
cmd/go: prune more dependencies in 'go get'
Browse files Browse the repository at this point in the history
Prior to this change, 'go get' pulled in every version of each module
whose path is explicitly listed in the go.mod file. When graph pruning
is enabled (that is, when the main module is at 'go 1.17' or higher),
that pulled in transitive dependencies of older-than-selected versions
of dependencies, which are normally pruned out by other 'go' commands
(including 'go mod tidy' and 'go mod graph').

To make matters worse, different parts of `go get` were making
different assumptions about which kinds of conflicts would be
reported: the modget package assumed that any conflict is necessarily
due to some explicit constraint, but 'go get' was imposing an
additional constraint that modules could not be incidentally upgraded
in the course of a downgrade. When that additional constraint failed,
the modload package reported the failure as though it were a real
(caller-supplied) constraint, confusing the caller (which couldn't
identify any specific package or argument that caused the failure).

This change fixes both of those problems by replacing the
modload.EditRequirements algorithm with a different one.

The new algorithm is, roughly, as follows.

1. Propose a list of “root requirements” to be written to the updated
   go.mod file.

2. Load the module graph from those requirements mostly as usual, but
   if any root is upgraded due to transitive dependencies, retain the
   original roots and the paths leading from those roots to the
   upgrades. (This forms an “extended graph”, in which we can trace a
   path from to each version that appears in the graph starting at one
   or more of the original roots.)

3. Identify which roots caused any module path to be upgraded above
   its passed-in version constraint. For each such root, either report
   an unresolvable conflict (if the root itself is constrained to a
   specific version) or identify an updated version to propose: either
   a downgrade to the next-highest version, or an upgrade to the
   actually-selected version of the root (if that version is allowed).
   To avoid looping forever or devolving into an NP-complete search,
   we never propose a version that was already rejected previously,
   regardless of what other roots were present alongside it at the
   time.

4. If the version of any root was changed, repeat from (1).

This algorithm is guaranteed to terminate, because there are finitely
many root versions and we permanently reject at least one each time we
downgrade its path to a lower version.

In addition, this change implements support for the '-v' flag to log
more information about version changes at each iteration.

Fixes #56494.
Fixes #55955.

Change-Id: Iebc17dd7586594d5732e228043c3c4c6da230f44
Reviewed-on: https://go-review.googlesource.com/c/go/+/471595
TryBot-Result: Gopher Robot <[email protected]>
Auto-Submit: Bryan Mills <[email protected]>
Run-TryBot: Bryan Mills <[email protected]>
Reviewed-by: Michael Matloob <[email protected]>
  • Loading branch information
Bryan C. Mills authored and gopherbot committed May 17, 2023
1 parent 9fd8769 commit a094a82
Show file tree
Hide file tree
Showing 13 changed files with 1,100 additions and 537 deletions.
23 changes: 22 additions & 1 deletion src/cmd/go/internal/modget/get.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import (
"sync"

"cmd/go/internal/base"
"cmd/go/internal/cfg"
"cmd/go/internal/imports"
"cmd/go/internal/modfetch"
"cmd/go/internal/modload"
Expand Down Expand Up @@ -1748,6 +1749,16 @@ func (r *resolver) updateBuildList(ctx context.Context, additions []module.Versi
return false
}

if cfg.BuildV {
// Log complete paths for the conflicts before we summarize them.
for _, c := range constraint.Conflicts {
fmt.Fprintf(os.Stderr, "go: %v\n", c.String())
}
}

// modload.EditBuildList reports constraint errors at
// the module level, but 'go get' operates on packages.
// Rewrite the errors to explain them in terms of packages.
reason := func(m module.Version) string {
rv, ok := r.resolvedVersion[m.Path]
if !ok {
Expand All @@ -1756,7 +1767,17 @@ func (r *resolver) updateBuildList(ctx context.Context, additions []module.Versi
return rv.reason.ResolvedString(module.Version{Path: m.Path, Version: rv.version})
}
for _, c := range constraint.Conflicts {
base.Errorf("go: %v requires %v, not %v", reason(c.Source), c.Dep, reason(c.Constraint))
adverb := ""
if len(c.Path) > 2 {
adverb = "indirectly "
}
firstReason := reason(c.Path[0])
last := c.Path[len(c.Path)-1]
if c.Err != nil {
base.Errorf("go: %v %srequires %v: %v", firstReason, adverb, last, c.UnwrapModuleError())
} else {
base.Errorf("go: %v %srequires %v, not %v", firstReason, adverb, last, reason(c.Constraint))
}
}
return false
}
Expand Down
110 changes: 96 additions & 14 deletions src/cmd/go/internal/modload/buildlist.go
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,11 @@ func newRequirements(pruning modPruning, rootModules []module.Version, direct ma
return rs
}

// String returns a string describing the Requirements for debugging.
func (rs *Requirements) String() string {
return fmt.Sprintf("{%v %v}", rs.pruning, rs.rootModules)
}

// initVendor initializes rs.graph from the given list of vendored module
// dependencies, overriding the graph that would normally be loaded from module
// requirements.
Expand Down Expand Up @@ -235,7 +240,7 @@ func (rs *Requirements) hasRedundantRoot() bool {
// returns a non-nil error of type *mvs.BuildListError.
func (rs *Requirements) Graph(ctx context.Context) (*ModuleGraph, error) {
rs.graphOnce.Do(func() {
mg, mgErr := readModGraph(ctx, rs.pruning, rs.rootModules)
mg, mgErr := readModGraph(ctx, rs.pruning, rs.rootModules, nil)
rs.graph.Store(&cachedGraph{mg, mgErr})
})
cached := rs.graph.Load()
Expand Down Expand Up @@ -266,9 +271,12 @@ var readModGraphDebugOnce sync.Once
// readModGraph reads and returns the module dependency graph starting at the
// given roots.
//
// The requirements of the module versions found in the unprune map are included
// in the graph even if they would normally be pruned out.
//
// Unlike LoadModGraph, readModGraph does not attempt to diagnose or update
// inconsistent roots.
func readModGraph(ctx context.Context, pruning modPruning, roots []module.Version) (*ModuleGraph, error) {
func readModGraph(ctx context.Context, pruning modPruning, roots []module.Version, unprune map[module.Version]bool) (*ModuleGraph, error) {
if pruning == pruned {
// Enable diagnostics for lazy module loading
// (https://golang.org/ref/mod#lazy-loading) only if the module graph is
Expand Down Expand Up @@ -355,13 +363,14 @@ func readModGraph(ctx context.Context, pruning modPruning, roots []module.Versio
// cannot assume that the explicit requirements of m (added by loadOne)
// are sufficient to build the packages it contains. We must load its full
// transitive dependency graph to be sure that we see all relevant
// dependencies.
if pruning != pruned || summary.pruning == unpruned {
nextPruning := summary.pruning
if pruning == unpruned {
nextPruning = unpruned
}
for _, r := range summary.require {
// dependencies. In addition, we must load the requirements of any module
// that is explicitly marked as unpruned.
nextPruning := summary.pruning
if pruning == unpruned {
nextPruning = unpruned
}
for _, r := range summary.require {
if pruning != pruned || summary.pruning == unpruned || unprune[r] {
enqueue(r, nextPruning)
}
}
Expand Down Expand Up @@ -607,17 +616,90 @@ func (e *ConstraintError) Error() string {
b := new(strings.Builder)
b.WriteString("version constraints conflict:")
for _, c := range e.Conflicts {
fmt.Fprintf(b, "\n\t%v requires %v, but %v is requested", c.Source, c.Dep, c.Constraint)
fmt.Fprintf(b, "\n\t%s", c.Summary())
}
return b.String()
}

// A Conflict documents that Source requires Dep, which conflicts with Constraint.
// (That is, Dep has the same module path as Constraint but a higher version.)
// A Conflict is a path of requirements starting at a root or proposed root in
// the requirement graph, explaining why that root either causes a module passed
// in the mustSelect list to EditBuildList to be unattainable, or introduces an
// unresolvable error in loading the requirement graph.
type Conflict struct {
Source module.Version
Dep module.Version
// Path is a path of requirements starting at some module version passed in
// the mustSelect argument and ending at a module whose requirements make that
// version unacceptable. (Path always has len ≥ 1.)
Path []module.Version

// If Err is nil, Constraint is a module version passed in the mustSelect
// argument that has the same module path as, and a lower version than,
// the last element of the Path slice.
Constraint module.Version

// If Constraint is unset, Err is an error encountered when loading the
// requirements of the last element in Path.
Err error
}

// UnwrapModuleError returns c.Err, but unwraps it if it is a module.ModuleError
// with a version and path matching the last entry in the Path slice.
func (c Conflict) UnwrapModuleError() error {
me, ok := c.Err.(*module.ModuleError)
if ok && len(c.Path) > 0 {
last := c.Path[len(c.Path)-1]
if me.Path == last.Path && me.Version == last.Version {
return me.Err
}
}
return c.Err
}

// Summary returns a string that describes only the first and last modules in
// the conflict path.
func (c Conflict) Summary() string {
if len(c.Path) == 0 {
return "(internal error: invalid Conflict struct)"
}
first := c.Path[0]
last := c.Path[len(c.Path)-1]
if len(c.Path) == 1 {
if c.Err != nil {
return fmt.Sprintf("%s: %v", first, c.UnwrapModuleError())
}
return fmt.Sprintf("%s is above %s", first, c.Constraint.Version)
}

adverb := ""
if len(c.Path) > 2 {
adverb = "indirectly "
}
if c.Err != nil {
return fmt.Sprintf("%s %srequires %s: %v", first, adverb, last, c.UnwrapModuleError())
}
return fmt.Sprintf("%s %srequires %s, but %s is requested", first, adverb, last, c.Constraint.Version)
}

// String returns a string that describes the full conflict path.
func (c Conflict) String() string {
if len(c.Path) == 0 {
return "(internal error: invalid Conflict struct)"
}
b := new(strings.Builder)
fmt.Fprintf(b, "%v", c.Path[0])
if len(c.Path) == 1 {
fmt.Fprintf(b, " found")
} else {
for _, r := range c.Path[1:] {
fmt.Fprintf(b, " requires\n\t%v", r)
}
}
if c.Constraint != (module.Version{}) {
fmt.Fprintf(b, ", but %v is requested", c.Constraint.Version)
}
if c.Err != nil {
fmt.Fprintf(b, ": %v", c.UnwrapModuleError())
}
return b.String()
}

// tidyRoots trims the root dependencies to the minimal requirements needed to
Expand Down
Loading

0 comments on commit a094a82

Please sign in to comment.