blob: c5de4866bf467907bafe5d1370b5f2ed39c8c173 [file] [log] [blame]
// Copyright 2020 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package mvs
import (
"fmt"
"golang.org/x/mod/module"
)
// Graph implements an incremental version of the MVS algorithm, with the
// requirements pushed by the caller instead of pulled by the MVS traversal.
type Graph struct {
cmp func(v1, v2 string) int
roots []module.Version
required map[module.Version][]module.Version
isRoot map[module.Version]bool // contains true for roots and false for reachable non-roots
selected map[string]string // path → version
}
// NewGraph returns an incremental MVS graph containing only a set of root
// dependencies and using the given max function for version strings.
//
// The caller must ensure that the root slice is not modified while the Graph
// may be in use.
func NewGraph(cmp func(v1, v2 string) int, roots []module.Version) *Graph {
g := &Graph{
cmp: cmp,
roots: roots[:len(roots):len(roots)],
required: make(map[module.Version][]module.Version),
isRoot: make(map[module.Version]bool),
selected: make(map[string]string),
}
for _, m := range roots {
g.isRoot[m] = true
if g.cmp(g.Selected(m.Path), m.Version) < 0 {
g.selected[m.Path] = m.Version
}
}
return g
}
// Require adds the information that module m requires all modules in reqs.
// The reqs slice must not be modified after it is passed to Require.
//
// m must be reachable by some existing chain of requirements from g's target,
// and Require must not have been called for it already.
//
// If any of the modules in reqs has the same path as g's target,
// the target must have higher precedence than the version in req.
func (g *Graph) Require(m module.Version, reqs []module.Version) {
// To help catch disconnected-graph bugs, enforce that all required versions
// are actually reachable from the roots (and therefore should affect the
// selected versions of the modules they name).
if _, reachable := g.isRoot[m]; !reachable {
panic(fmt.Sprintf("%v is not reachable from any root", m))
}
// Truncate reqs to its capacity to avoid aliasing bugs if it is later
// returned from RequiredBy and appended to.
reqs = reqs[:len(reqs):len(reqs)]
if _, dup := g.required[m]; dup {
panic(fmt.Sprintf("requirements of %v have already been set", m))
}
g.required[m] = reqs
for _, dep := range reqs {
// Mark dep reachable, regardless of whether it is selected.
if _, ok := g.isRoot[dep]; !ok {
g.isRoot[dep] = false
}
if g.cmp(g.Selected(dep.Path), dep.Version) < 0 {
g.selected[dep.Path] = dep.Version
}
}
}
// RequiredBy returns the slice of requirements passed to Require for m, if any,
// with its capacity reduced to its length.
// If Require has not been called for m, RequiredBy(m) returns ok=false.
//
// The caller must not modify the returned slice, but may safely append to it
// and may rely on it not to be modified.
func (g *Graph) RequiredBy(m module.Version) (reqs []module.Version, ok bool) {
reqs, ok = g.required[m]
return reqs, ok
}
// Selected returns the selected version of the given module path.
//
// If no version is selected, Selected returns version "none".
func (g *Graph) Selected(path string) (version string) {
v, ok := g.selected[path]
if !ok {
return "none"
}
return v
}
// BuildList returns the selected versions of all modules present in the Graph,
// beginning with the selected versions of each module path in the roots of g.
//
// The order of the remaining elements in the list is deterministic
// but arbitrary.
func (g *Graph) BuildList() []module.Version {
seenRoot := make(map[string]bool, len(g.roots))
var list []module.Version
for _, r := range g.roots {
if seenRoot[r.Path] {
// Multiple copies of the same root, with the same or different versions,
// are a bit of a degenerate case: we will take the transitive
// requirements of both roots into account, but only the higher one can
// possibly be selected. However — especially given that we need the
// seenRoot map for later anyway — it is simpler to support this
// degenerate case than to forbid it.
continue
}
if v := g.Selected(r.Path); v != "none" {
list = append(list, module.Version{Path: r.Path, Version: v})
}
seenRoot[r.Path] = true
}
uniqueRoots := list
for path, version := range g.selected {
if !seenRoot[path] {
list = append(list, module.Version{Path: path, Version: version})
}
}
module.Sort(list[len(uniqueRoots):])
return list
}
// WalkBreadthFirst invokes f once, in breadth-first order, for each module
// version other than "none" that appears in the graph, regardless of whether
// that version is selected.
func (g *Graph) WalkBreadthFirst(f func(m module.Version)) {
var queue []module.Version
enqueued := make(map[module.Version]bool)
for _, m := range g.roots {
if m.Version != "none" {
queue = append(queue, m)
enqueued[m] = true
}
}
for len(queue) > 0 {
m := queue[0]
queue = queue[1:]
f(m)
reqs, _ := g.RequiredBy(m)
for _, r := range reqs {
if !enqueued[r] && r.Version != "none" {
queue = append(queue, r)
enqueued[r] = true
}
}
}
}
// FindPath reports a shortest requirement path starting at one of the roots of
// the graph and ending at a module version m for which f(m) returns true, or
// nil if no such path exists.
func (g *Graph) FindPath(f func(module.Version) bool) []module.Version {
// firstRequires[a] = b means that in a breadth-first traversal of the
// requirement graph, the module version a was first required by b.
firstRequires := make(map[module.Version]module.Version)
queue := g.roots
for _, m := range g.roots {
firstRequires[m] = module.Version{}
}
for len(queue) > 0 {
m := queue[0]
queue = queue[1:]
if f(m) {
// Construct the path reversed (because we're starting from the far
// endpoint), then reverse it.
path := []module.Version{m}
for {
m = firstRequires[m]
if m.Path == "" {
break
}
path = append(path, m)
}
i, j := 0, len(path)-1
for i < j {
path[i], path[j] = path[j], path[i]
i++
j--
}
return path
}
reqs, _ := g.RequiredBy(m)
for _, r := range reqs {
if _, seen := firstRequires[r]; !seen {
queue = append(queue, r)
firstRequires[r] = m
}
}
}
return nil
}