зеркало из https://github.com/golang/dep.git
Split out 'rootdata' struct from solver
This separates a bunch of the static state/rules/information that comes from the root project and input parameters into a discrete subsystem. The only real benefit here is focusing the state tracked by the solver in on the actual algorithm of solving, and less so these static rules - which should make it a bit easier for other people to grok.
This commit is contained in:
Родитель
34bfe7eff6
Коммит
5b5b251166
18
bridge.go
18
bridge.go
|
@ -40,6 +40,9 @@ type bridge struct {
|
|||
// held by the solver that it ends up being easier and saner to do this.
|
||||
s *solver
|
||||
|
||||
// Whether to sort version lists for downgrade.
|
||||
down bool
|
||||
|
||||
// Simple, local cache of the root's PackageTree
|
||||
crp *struct {
|
||||
ptree PackageTree
|
||||
|
@ -58,17 +61,18 @@ type bridge struct {
|
|||
|
||||
// Global factory func to create a bridge. This exists solely to allow tests to
|
||||
// override it with a custom bridge and sm.
|
||||
var mkBridge = func(s *solver, sm SourceManager) sourceBridge {
|
||||
var mkBridge = func(s *solver, sm SourceManager, down bool) sourceBridge {
|
||||
return &bridge{
|
||||
sm: sm,
|
||||
s: s,
|
||||
down: down,
|
||||
vlists: make(map[ProjectIdentifier][]Version),
|
||||
}
|
||||
}
|
||||
|
||||
func (b *bridge) GetManifestAndLock(id ProjectIdentifier, v Version) (Manifest, Lock, error) {
|
||||
if id.ProjectRoot == ProjectRoot(b.s.rpt.ImportRoot) {
|
||||
return b.s.rm, b.s.rl, nil
|
||||
if b.s.rd.isRoot(id.ProjectRoot) {
|
||||
return b.s.rd.rm, b.s.rd.rl, nil
|
||||
}
|
||||
|
||||
b.s.mtr.push("b-gmal")
|
||||
|
@ -94,7 +98,7 @@ func (b *bridge) ListVersions(id ProjectIdentifier) ([]Version, error) {
|
|||
return nil, err
|
||||
}
|
||||
|
||||
if b.s.params.Downgrade {
|
||||
if b.down {
|
||||
SortForDowngrade(vl)
|
||||
} else {
|
||||
SortForUpgrade(vl)
|
||||
|
@ -120,7 +124,7 @@ func (b *bridge) SourceExists(id ProjectIdentifier) (bool, error) {
|
|||
}
|
||||
|
||||
func (b *bridge) vendorCodeExists(id ProjectIdentifier) (bool, error) {
|
||||
fi, err := os.Stat(filepath.Join(b.s.params.RootDir, "vendor", string(id.ProjectRoot)))
|
||||
fi, err := os.Stat(filepath.Join(b.s.rd.dir, "vendor", string(id.ProjectRoot)))
|
||||
if err != nil {
|
||||
return false, err
|
||||
} else if fi.IsDir() {
|
||||
|
@ -279,7 +283,7 @@ func (b *bridge) vtu(id ProjectIdentifier, v Version) versionTypeUnion {
|
|||
// The root project is handled separately, as the source manager isn't
|
||||
// responsible for that code.
|
||||
func (b *bridge) ListPackages(id ProjectIdentifier, v Version) (PackageTree, error) {
|
||||
if id.ProjectRoot == ProjectRoot(b.s.rpt.ImportRoot) {
|
||||
if b.s.rd.isRoot(id.ProjectRoot) {
|
||||
panic("should never call ListPackages on root project")
|
||||
}
|
||||
|
||||
|
@ -327,7 +331,7 @@ func (b *bridge) breakLock() {
|
|||
return
|
||||
}
|
||||
|
||||
for _, lp := range b.s.rl.Projects() {
|
||||
for _, lp := range b.s.rd.rl.Projects() {
|
||||
if _, is := b.s.sel.selected(lp.pi); !is {
|
||||
// TODO(sdboyer) use this as an opportunity to detect
|
||||
// inconsistencies between upstream and the lock (e.g., moved tags)?
|
||||
|
|
14
hash.go
14
hash.go
|
@ -41,7 +41,7 @@ func (s *solver) writeHashingInputs(w io.Writer) {
|
|||
// Apply overrides to the constraints from the root. Otherwise, the hash
|
||||
// would be computed on the basis of a constraint from root that doesn't
|
||||
// actually affect solving.
|
||||
wc := s.ovr.overrideAll(s.rm.DependencyConstraints().merge(s.rm.TestDependencyConstraints()))
|
||||
wc := s.rd.combineConstraints()
|
||||
|
||||
for _, pd := range wc {
|
||||
writeString(string(pd.Ident.ProjectRoot))
|
||||
|
@ -59,7 +59,7 @@ func (s *solver) writeHashingInputs(w io.Writer) {
|
|||
// particular subpath, into the hash. We need to do this in a
|
||||
// deterministic order, so expand and sort the map.
|
||||
var pkgs []PackageOrErr
|
||||
for _, perr := range s.rpt.Packages {
|
||||
for _, perr := range s.rd.rpt.Packages {
|
||||
pkgs = append(pkgs, perr)
|
||||
}
|
||||
sort.Sort(sortPackageOrErr(pkgs))
|
||||
|
@ -84,8 +84,8 @@ func (s *solver) writeHashingInputs(w io.Writer) {
|
|||
}
|
||||
|
||||
// Write any required packages given in the root manifest.
|
||||
req := make([]string, 0, len(s.req))
|
||||
for pkg := range s.req {
|
||||
req := make([]string, 0, len(s.rd.req))
|
||||
for pkg := range s.rd.req {
|
||||
req = append(req, pkg)
|
||||
}
|
||||
sort.Strings(req)
|
||||
|
@ -95,8 +95,8 @@ func (s *solver) writeHashingInputs(w io.Writer) {
|
|||
}
|
||||
|
||||
// Add the ignored packages, if any.
|
||||
ig := make([]string, 0, len(s.ig))
|
||||
for pkg := range s.ig {
|
||||
ig := make([]string, 0, len(s.rd.ig))
|
||||
for pkg := range s.rd.ig {
|
||||
ig = append(ig, pkg)
|
||||
}
|
||||
sort.Strings(ig)
|
||||
|
@ -105,7 +105,7 @@ func (s *solver) writeHashingInputs(w io.Writer) {
|
|||
writeString(igp)
|
||||
}
|
||||
|
||||
for _, pc := range s.ovr.asSortedSlice() {
|
||||
for _, pc := range s.rd.ovr.asSortedSlice() {
|
||||
writeString(string(pc.Ident.ProjectRoot))
|
||||
if pc.Ident.Source != "" {
|
||||
writeString(pc.Ident.Source)
|
||||
|
|
|
@ -0,0 +1,173 @@
|
|||
package gps
|
||||
|
||||
import (
|
||||
"sort"
|
||||
|
||||
"github.com/armon/go-radix"
|
||||
)
|
||||
|
||||
// rootdata holds static data and constraining rules from the root project for
|
||||
// use in solving.
|
||||
type rootdata struct {
|
||||
// Path to the root of the project on which gps is operating.
|
||||
dir string
|
||||
|
||||
// Map of packages to ignore.
|
||||
ig map[string]bool
|
||||
|
||||
// Map of packages to require.
|
||||
req map[string]bool
|
||||
|
||||
// A ProjectConstraints map containing the validated (guaranteed non-empty)
|
||||
// overrides declared by the root manifest.
|
||||
ovr ProjectConstraints
|
||||
|
||||
// A map of the ProjectRoot (local names) that should be allowed to change
|
||||
chng map[ProjectRoot]struct{}
|
||||
|
||||
// Flag indicating all projects should be allowed to change, without regard
|
||||
// for lock.
|
||||
chngall bool
|
||||
|
||||
// A map of the project names listed in the root's lock.
|
||||
rlm map[ProjectRoot]LockedProject
|
||||
|
||||
// A defensively-copied instance of the root manifest.
|
||||
rm Manifest
|
||||
|
||||
// A defensively-copied instance of the root lock.
|
||||
rl Lock
|
||||
|
||||
// A defensively-copied instance of params.RootPackageTree
|
||||
rpt PackageTree
|
||||
}
|
||||
|
||||
// rootImportList returns a list of the unique imports from the root data.
|
||||
// Ignores and requires are taken into consideration.
|
||||
func (rd rootdata) externalImportList() []string {
|
||||
reach := rd.rpt.ExternalReach(true, true, rd.ig).ListExternalImports()
|
||||
|
||||
// If there are any requires, slide them into the reach list, as well.
|
||||
if len(rd.req) > 0 {
|
||||
reqs := make([]string, 0, len(rd.req))
|
||||
|
||||
// Make a map of both imported and required pkgs to skip, to avoid
|
||||
// duplication. Technically, a slice would probably be faster (given
|
||||
// small size and bounds check elimination), but this is a one-time op,
|
||||
// so it doesn't matter.
|
||||
skip := make(map[string]bool, len(rd.req))
|
||||
for _, r := range reach {
|
||||
if rd.req[r] {
|
||||
skip[r] = true
|
||||
}
|
||||
}
|
||||
|
||||
for r := range rd.req {
|
||||
if !skip[r] {
|
||||
reqs = append(reqs, r)
|
||||
}
|
||||
}
|
||||
|
||||
reach = append(reach, reqs...)
|
||||
}
|
||||
|
||||
return reach
|
||||
}
|
||||
|
||||
func (rd rootdata) getApplicableConstraints() []workingConstraint {
|
||||
xt := radix.New()
|
||||
combined := rd.combineConstraints()
|
||||
|
||||
type wccount struct {
|
||||
count int
|
||||
wc workingConstraint
|
||||
}
|
||||
for _, wc := range combined {
|
||||
xt.Insert(string(wc.Ident.ProjectRoot), wccount{wc: wc})
|
||||
}
|
||||
|
||||
// Walk all dep import paths we have to consider and mark the corresponding
|
||||
// wc entry in the trie, if any
|
||||
for _, im := range rd.externalImportList() {
|
||||
if isStdLib(im) {
|
||||
continue
|
||||
}
|
||||
|
||||
if pre, v, match := xt.LongestPrefix(im); match && isPathPrefixOrEqual(pre, im) {
|
||||
wcc := v.(wccount)
|
||||
wcc.count++
|
||||
xt.Insert(pre, wcc)
|
||||
}
|
||||
}
|
||||
|
||||
var ret []workingConstraint
|
||||
|
||||
xt.Walk(func(s string, v interface{}) bool {
|
||||
wcc := v.(wccount)
|
||||
if wcc.count > 0 || wcc.wc.overrNet || wcc.wc.overrConstraint {
|
||||
ret = append(ret, wcc.wc)
|
||||
}
|
||||
return false
|
||||
})
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
func (rd rootdata) combineConstraints() []workingConstraint {
|
||||
return rd.ovr.overrideAll(rd.rm.DependencyConstraints().merge(rd.rm.TestDependencyConstraints()))
|
||||
}
|
||||
|
||||
// needVersionListFor indicates whether we need a version list for a given
|
||||
// project root, based solely on general solver inputs (no constraint checking
|
||||
// required). This will be true if:
|
||||
//
|
||||
// - ChangeAll is on
|
||||
// - The project is not in the lock at all
|
||||
// - The project is in the lock, but is also in the list of projects to change
|
||||
func (rd rootdata) needVersionsFor(pr ProjectRoot) bool {
|
||||
if rd.chngall {
|
||||
return true
|
||||
}
|
||||
|
||||
if _, has := rd.rlm[pr]; !has {
|
||||
// not in the lock
|
||||
return true
|
||||
} else if _, has := rd.chng[pr]; has {
|
||||
// in the lock, but marked for change
|
||||
return true
|
||||
}
|
||||
// in the lock, not marked for change
|
||||
return false
|
||||
|
||||
}
|
||||
|
||||
func (rd rootdata) isRoot(pr ProjectRoot) bool {
|
||||
return pr == ProjectRoot(rd.rpt.ImportRoot)
|
||||
}
|
||||
|
||||
// rootAtom creates an atomWithPackages that represents the root project.
|
||||
func (rd rootdata) rootAtom() atomWithPackages {
|
||||
a := atom{
|
||||
id: ProjectIdentifier{
|
||||
ProjectRoot: ProjectRoot(rd.rpt.ImportRoot),
|
||||
},
|
||||
// This is a hack so that the root project doesn't have a nil version.
|
||||
// It's sort of OK because the root never makes it out into the results.
|
||||
// We may need a more elegant solution if we discover other side
|
||||
// effects, though.
|
||||
v: rootRev,
|
||||
}
|
||||
|
||||
list := make([]string, 0, len(rd.rpt.Packages))
|
||||
for path, pkg := range rd.rpt.Packages {
|
||||
if pkg.Err != nil && !rd.ig[path] {
|
||||
list = append(list, path)
|
||||
}
|
||||
}
|
||||
sort.Strings(list)
|
||||
|
||||
return atomWithPackages{
|
||||
a: a,
|
||||
pl: list,
|
||||
}
|
||||
}
|
|
@ -20,7 +20,7 @@ var fixtorun string
|
|||
// TODO(sdboyer) regression test ensuring that locks with only revs for projects don't cause errors
|
||||
func init() {
|
||||
flag.StringVar(&fixtorun, "gps.fix", "", "A single fixture to run in TestBasicSolves or TestBimodalSolves")
|
||||
mkBridge(nil, nil)
|
||||
mkBridge(nil, nil, false)
|
||||
overrideMkBridge()
|
||||
overrideIsStdLib()
|
||||
}
|
||||
|
@ -29,11 +29,12 @@ func init() {
|
|||
func overrideMkBridge() {
|
||||
// For all tests, override the base bridge with the depspecBridge that skips
|
||||
// verifyRootDir calls
|
||||
mkBridge = func(s *solver, sm SourceManager) sourceBridge {
|
||||
mkBridge = func(s *solver, sm SourceManager, down bool) sourceBridge {
|
||||
return &depspecBridge{
|
||||
&bridge{
|
||||
sm: sm,
|
||||
s: s,
|
||||
down: down,
|
||||
vlists: make(map[ProjectIdentifier][]Version),
|
||||
},
|
||||
}
|
||||
|
@ -417,10 +418,11 @@ func TestBadSolveOpts(t *testing.T) {
|
|||
|
||||
// swap out the test mkBridge override temporarily, just to make sure we get
|
||||
// the right error
|
||||
mkBridge = func(s *solver, sm SourceManager) sourceBridge {
|
||||
mkBridge = func(s *solver, sm SourceManager, down bool) sourceBridge {
|
||||
return &bridge{
|
||||
sm: sm,
|
||||
s: s,
|
||||
down: down,
|
||||
vlists: make(map[ProjectIdentifier][]Version),
|
||||
}
|
||||
}
|
||||
|
|
429
solver.go
429
solver.go
|
@ -14,7 +14,7 @@ var rootRev = Revision("")
|
|||
|
||||
// SolveParameters hold all arguments to a solver run.
|
||||
//
|
||||
// Only RootDir and ImportRoot are absolutely required. A nil Manifest is
|
||||
// Only RootDir and RootPackageTree are absolutely required. A nil Manifest is
|
||||
// allowed, though it usually makes little sense.
|
||||
//
|
||||
// Of these properties, only Manifest and Ignore are (directly) incorporated in
|
||||
|
@ -92,14 +92,6 @@ type solver struct {
|
|||
// starts moving forward again.
|
||||
attempts int
|
||||
|
||||
// SolveParameters are the inputs to the solver. They determine both what
|
||||
// data the solver should operate on, and certain aspects of how solving
|
||||
// proceeds.
|
||||
//
|
||||
// Prepare() validates these, so by the time we have a *solver instance, we
|
||||
// know they're valid.
|
||||
params SolveParameters
|
||||
|
||||
// Logger used exclusively for trace output, if the trace option is set.
|
||||
tl *log.Logger
|
||||
|
||||
|
@ -128,12 +120,6 @@ type solver struct {
|
|||
// removal.
|
||||
unsel *unselected
|
||||
|
||||
// Map of packages to ignore.
|
||||
ig map[string]bool
|
||||
|
||||
// Map of packages to require.
|
||||
req map[string]bool
|
||||
|
||||
// A stack of all the currently active versionQueues in the solver. The set
|
||||
// of projects represented here corresponds closely to what's in s.sel,
|
||||
// although s.sel will always contain the root project, and s.vqs never
|
||||
|
@ -142,29 +128,157 @@ type solver struct {
|
|||
// added to an existing project.
|
||||
vqs []*versionQueue
|
||||
|
||||
// A map of the ProjectRoot (local names) that should be allowed to change
|
||||
chng map[ProjectRoot]struct{}
|
||||
|
||||
// A ProjectConstraints map containing the validated (guaranteed non-empty)
|
||||
// overrides declared by the root manifest.
|
||||
ovr ProjectConstraints
|
||||
|
||||
// A map of the project names listed in the root's lock.
|
||||
rlm map[ProjectRoot]LockedProject
|
||||
|
||||
// A defensively-copied instance of the root manifest.
|
||||
rm Manifest
|
||||
|
||||
// A defensively-copied instance of the root lock.
|
||||
rl Lock
|
||||
|
||||
// A defensively-copied instance of params.RootPackageTree
|
||||
rpt PackageTree
|
||||
// Contains data and constraining information from the root project
|
||||
rd rootdata
|
||||
|
||||
// metrics for the current solve run.
|
||||
mtr *metrics
|
||||
}
|
||||
|
||||
func (params SolveParameters) toRootdata() (rootdata, error) {
|
||||
if params.RootDir == "" {
|
||||
return rootdata{}, badOptsFailure("params must specify a non-empty root directory")
|
||||
}
|
||||
if params.RootPackageTree.ImportRoot == "" {
|
||||
return rootdata{}, badOptsFailure("params must include a non-empty import root")
|
||||
}
|
||||
if len(params.RootPackageTree.Packages) == 0 {
|
||||
return rootdata{}, badOptsFailure("at least one package must be present in the PackageTree")
|
||||
}
|
||||
if params.Lock == nil && len(params.ToChange) != 0 {
|
||||
return rootdata{}, badOptsFailure(fmt.Sprintf("update specifically requested for %s, but no lock was provided to upgrade from", params.ToChange))
|
||||
}
|
||||
|
||||
if params.Manifest == nil {
|
||||
params.Manifest = simpleRootManifest{}
|
||||
}
|
||||
|
||||
rd := rootdata{
|
||||
ig: params.Manifest.IgnoredPackages(),
|
||||
req: params.Manifest.RequiredPackages(),
|
||||
ovr: params.Manifest.Overrides(),
|
||||
rpt: params.RootPackageTree.dup(),
|
||||
chng: make(map[ProjectRoot]struct{}),
|
||||
rlm: make(map[ProjectRoot]LockedProject),
|
||||
chngall: params.ChangeAll,
|
||||
dir: params.RootDir,
|
||||
}
|
||||
|
||||
// Ensure the required, ignore and overrides maps are at least initialized
|
||||
if rd.ig == nil {
|
||||
rd.ig = make(map[string]bool)
|
||||
}
|
||||
if rd.req == nil {
|
||||
rd.req = make(map[string]bool)
|
||||
}
|
||||
if rd.ovr == nil {
|
||||
rd.ovr = make(ProjectConstraints)
|
||||
}
|
||||
|
||||
if len(rd.ig) != 0 {
|
||||
var both []string
|
||||
for pkg := range params.Manifest.RequiredPackages() {
|
||||
if rd.ig[pkg] {
|
||||
both = append(both, pkg)
|
||||
}
|
||||
}
|
||||
switch len(both) {
|
||||
case 0:
|
||||
break
|
||||
case 1:
|
||||
return rootdata{}, badOptsFailure(fmt.Sprintf("%q was given as both a required and ignored package", both[0]))
|
||||
default:
|
||||
return rootdata{}, badOptsFailure(fmt.Sprintf("multiple packages given as both required and ignored: %s", strings.Join(both, ", ")))
|
||||
}
|
||||
}
|
||||
|
||||
// Validate no empties in the overrides map
|
||||
var eovr []string
|
||||
for pr, pp := range rd.ovr {
|
||||
if pp.Constraint == nil && pp.Source == "" {
|
||||
eovr = append(eovr, string(pr))
|
||||
}
|
||||
}
|
||||
|
||||
if eovr != nil {
|
||||
// Maybe it's a little nitpicky to do this (we COULD proceed; empty
|
||||
// overrides have no effect), but this errs on the side of letting the
|
||||
// tool/user know there's bad input. Purely as a principle, that seems
|
||||
// preferable to silently allowing progress with icky input.
|
||||
if len(eovr) > 1 {
|
||||
return rootdata{}, badOptsFailure(fmt.Sprintf("Overrides lacked any non-zero properties for multiple project roots: %s", strings.Join(eovr, " ")))
|
||||
}
|
||||
return rootdata{}, badOptsFailure(fmt.Sprintf("An override was declared for %s, but without any non-zero properties", eovr[0]))
|
||||
}
|
||||
|
||||
// Prep safe, normalized versions of root manifest and lock data
|
||||
rd.rm = prepManifest(params.Manifest)
|
||||
|
||||
if params.Lock != nil {
|
||||
for _, lp := range params.Lock.Projects() {
|
||||
rd.rlm[lp.Ident().ProjectRoot] = lp
|
||||
}
|
||||
|
||||
// Also keep a prepped one, mostly for the bridge. This is probably
|
||||
// wasteful, but only minimally so, and yay symmetry
|
||||
rd.rl = prepLock(params.Lock)
|
||||
}
|
||||
|
||||
for _, p := range params.ToChange {
|
||||
if _, exists := rd.rlm[p]; !exists {
|
||||
return rootdata{}, badOptsFailure(fmt.Sprintf("cannot update %s as it is not in the lock", p))
|
||||
}
|
||||
rd.chng[p] = struct{}{}
|
||||
}
|
||||
|
||||
return rd, nil
|
||||
}
|
||||
|
||||
// Prepare readies a Solver for use.
|
||||
//
|
||||
// This function reads and validates the provided SolveParameters. If a problem
|
||||
// with the inputs is detected, an error is returned. Otherwise, a Solver is
|
||||
// returned, ready to hash and check inputs or perform a solving run.
|
||||
func Prepare(params SolveParameters, sm SourceManager) (Solver, error) {
|
||||
if sm == nil {
|
||||
return nil, badOptsFailure("must provide non-nil SourceManager")
|
||||
}
|
||||
if params.Trace && params.TraceLogger == nil {
|
||||
return nil, badOptsFailure("trace requested, but no logger provided")
|
||||
}
|
||||
|
||||
rd, err := params.toRootdata()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s := &solver{
|
||||
tl: params.TraceLogger,
|
||||
rd: rd,
|
||||
}
|
||||
|
||||
// Set up the bridge and ensure the root dir is in good, working order
|
||||
// before doing anything else. (This call is stubbed out in tests, via
|
||||
// overriding mkBridge(), so we can run with virtual RootDir.)
|
||||
s.b = mkBridge(s, sm, params.Downgrade)
|
||||
err = s.b.verifyRootDir(params.RootDir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Initialize stacks and queues
|
||||
s.sel = &selection{
|
||||
deps: make(map[ProjectRoot][]dependency),
|
||||
sm: s.b,
|
||||
}
|
||||
s.unsel = &unselected{
|
||||
sl: make([]bimodalIdentifier, 0),
|
||||
cmp: s.unselectedComparator,
|
||||
}
|
||||
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// A Solver is the main workhorse of gps: given a set of project inputs, it
|
||||
// performs a constraint solving analysis to develop a complete Solution, or
|
||||
// else fail with an informative error.
|
||||
|
@ -186,133 +300,6 @@ type Solver interface {
|
|||
Solve() (Solution, error)
|
||||
}
|
||||
|
||||
// Prepare readies a Solver for use.
|
||||
//
|
||||
// This function reads and validates the provided SolveParameters. If a problem
|
||||
// with the inputs is detected, an error is returned. Otherwise, a Solver is
|
||||
// returned, ready to hash and check inputs or perform a solving run.
|
||||
func Prepare(params SolveParameters, sm SourceManager) (Solver, error) {
|
||||
if sm == nil {
|
||||
return nil, badOptsFailure("must provide non-nil SourceManager")
|
||||
}
|
||||
if params.RootDir == "" {
|
||||
return nil, badOptsFailure("params must specify a non-empty root directory")
|
||||
}
|
||||
if params.RootPackageTree.ImportRoot == "" {
|
||||
return nil, badOptsFailure("params must include a non-empty import root")
|
||||
}
|
||||
if len(params.RootPackageTree.Packages) == 0 {
|
||||
return nil, badOptsFailure("at least one package must be present in the PackageTree")
|
||||
}
|
||||
if params.Trace && params.TraceLogger == nil {
|
||||
return nil, badOptsFailure("trace requested, but no logger provided")
|
||||
}
|
||||
if params.Lock == nil && len(params.ToChange) != 0 {
|
||||
return nil, badOptsFailure(fmt.Sprintf("update specifically requested for %s, but no lock was provided to upgrade from", params.ToChange))
|
||||
}
|
||||
|
||||
if params.Manifest == nil {
|
||||
params.Manifest = simpleRootManifest{}
|
||||
}
|
||||
|
||||
s := &solver{
|
||||
params: params,
|
||||
ig: params.Manifest.IgnoredPackages(),
|
||||
req: params.Manifest.RequiredPackages(),
|
||||
ovr: params.Manifest.Overrides(),
|
||||
tl: params.TraceLogger,
|
||||
rpt: params.RootPackageTree.dup(),
|
||||
}
|
||||
|
||||
if len(s.ig) != 0 {
|
||||
var both []string
|
||||
for pkg := range params.Manifest.RequiredPackages() {
|
||||
if s.ig[pkg] {
|
||||
both = append(both, pkg)
|
||||
}
|
||||
}
|
||||
switch len(both) {
|
||||
case 0:
|
||||
break
|
||||
case 1:
|
||||
return nil, badOptsFailure(fmt.Sprintf("%q was given as both a required and ignored package", both[0]))
|
||||
default:
|
||||
return nil, badOptsFailure(fmt.Sprintf("multiple packages given as both required and ignored: %s", strings.Join(both, ", ")))
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure the ignore and overrides maps are at least initialized
|
||||
if s.ig == nil {
|
||||
s.ig = make(map[string]bool)
|
||||
}
|
||||
if s.ovr == nil {
|
||||
s.ovr = make(ProjectConstraints)
|
||||
}
|
||||
|
||||
// Validate no empties in the overrides map
|
||||
var eovr []string
|
||||
for pr, pp := range s.ovr {
|
||||
if pp.Constraint == nil && pp.Source == "" {
|
||||
eovr = append(eovr, string(pr))
|
||||
}
|
||||
}
|
||||
|
||||
if eovr != nil {
|
||||
// Maybe it's a little nitpicky to do this (we COULD proceed; empty
|
||||
// overrides have no effect), but this errs on the side of letting the
|
||||
// tool/user know there's bad input. Purely as a principle, that seems
|
||||
// preferable to silently allowing progress with icky input.
|
||||
if len(eovr) > 1 {
|
||||
return nil, badOptsFailure(fmt.Sprintf("Overrides lacked any non-zero properties for multiple project roots: %s", strings.Join(eovr, " ")))
|
||||
}
|
||||
return nil, badOptsFailure(fmt.Sprintf("An override was declared for %s, but without any non-zero properties", eovr[0]))
|
||||
}
|
||||
|
||||
// Set up the bridge and ensure the root dir is in good, working order
|
||||
// before doing anything else. (This call is stubbed out in tests, via
|
||||
// overriding mkBridge(), so we can run with virtual RootDir.)
|
||||
s.b = mkBridge(s, sm)
|
||||
err := s.b.verifyRootDir(s.params.RootDir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Initialize maps
|
||||
s.chng = make(map[ProjectRoot]struct{})
|
||||
s.rlm = make(map[ProjectRoot]LockedProject)
|
||||
|
||||
// Initialize stacks and queues
|
||||
s.sel = &selection{
|
||||
deps: make(map[ProjectRoot][]dependency),
|
||||
sm: s.b,
|
||||
}
|
||||
s.unsel = &unselected{
|
||||
sl: make([]bimodalIdentifier, 0),
|
||||
cmp: s.unselectedComparator,
|
||||
}
|
||||
|
||||
// Prep safe, normalized versions of root manifest and lock data
|
||||
s.rm = prepManifest(s.params.Manifest)
|
||||
if s.params.Lock != nil {
|
||||
for _, lp := range s.params.Lock.Projects() {
|
||||
s.rlm[lp.Ident().ProjectRoot] = lp
|
||||
}
|
||||
|
||||
// Also keep a prepped one, mostly for the bridge. This is probably
|
||||
// wasteful, but only minimally so, and yay symmetry
|
||||
s.rl = prepLock(s.params.Lock)
|
||||
}
|
||||
|
||||
for _, p := range s.params.ToChange {
|
||||
if _, exists := s.rlm[p]; !exists {
|
||||
return nil, badOptsFailure(fmt.Sprintf("cannot update %s as it is not in the lock", p))
|
||||
}
|
||||
s.chng[p] = struct{}{}
|
||||
}
|
||||
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// Solve attempts to find a dependency solution for the given project, as
|
||||
// represented by the SolveParameters with which this Solver was created.
|
||||
//
|
||||
|
@ -348,8 +335,8 @@ func (s *solver) Solve() (Solution, error) {
|
|||
}
|
||||
|
||||
s.traceFinish(soln, err)
|
||||
if s.params.Trace {
|
||||
s.mtr.dump(s.params.TraceLogger)
|
||||
if s.tl != nil {
|
||||
s.mtr.dump(s.tl)
|
||||
}
|
||||
return soln, err
|
||||
}
|
||||
|
@ -467,67 +454,14 @@ func (s *solver) solve() (map[atom]map[string]struct{}, error) {
|
|||
// populate the queues at the beginning of a solve run.
|
||||
func (s *solver) selectRoot() error {
|
||||
s.mtr.push("select-root")
|
||||
pa := atom{
|
||||
id: ProjectIdentifier{
|
||||
ProjectRoot: ProjectRoot(s.rpt.ImportRoot),
|
||||
},
|
||||
// This is a hack so that the root project doesn't have a nil version.
|
||||
// It's sort of OK because the root never makes it out into the results.
|
||||
// We may need a more elegant solution if we discover other side
|
||||
// effects, though.
|
||||
v: rootRev,
|
||||
}
|
||||
|
||||
list := make([]string, len(s.rpt.Packages))
|
||||
k := 0
|
||||
for path, pkg := range s.rpt.Packages {
|
||||
if pkg.Err != nil {
|
||||
list[k] = path
|
||||
k++
|
||||
}
|
||||
}
|
||||
list = list[:k]
|
||||
sort.Strings(list)
|
||||
|
||||
a := atomWithPackages{
|
||||
a: pa,
|
||||
pl: list,
|
||||
}
|
||||
|
||||
// Push the root project onto the queue.
|
||||
// TODO(sdboyer) maybe it'd just be better to skip this?
|
||||
s.sel.pushSelection(a, true)
|
||||
awp := s.rd.rootAtom()
|
||||
s.sel.pushSelection(awp, true)
|
||||
|
||||
// If we're looking for root's deps, get it from opts and local root
|
||||
// analysis, rather than having the sm do it
|
||||
mdeps := s.ovr.overrideAll(s.rm.DependencyConstraints().merge(s.rm.TestDependencyConstraints()))
|
||||
reach := s.rpt.ExternalReach(true, true, s.ig).ListExternalImports()
|
||||
|
||||
// If there are any requires, slide them into the reach list, as well.
|
||||
if len(s.req) > 0 {
|
||||
reqs := make([]string, 0, len(s.req))
|
||||
|
||||
// Make a map of both imported and required pkgs to skip, to avoid
|
||||
// duplication. Technically, a slice would probably be faster (given
|
||||
// small size and bounds check elimination), but this is a one-time op,
|
||||
// so it doesn't matter.
|
||||
skip := make(map[string]bool, len(s.req))
|
||||
for _, r := range reach {
|
||||
if s.req[r] {
|
||||
skip[r] = true
|
||||
}
|
||||
}
|
||||
|
||||
for r := range s.req {
|
||||
if !skip[r] {
|
||||
reqs = append(reqs, r)
|
||||
}
|
||||
}
|
||||
|
||||
reach = append(reach, reqs...)
|
||||
}
|
||||
|
||||
deps, err := s.intersectConstraintsWithImports(mdeps, reach)
|
||||
deps, err := s.intersectConstraintsWithImports(s.rd.combineConstraints(), s.rd.externalImportList())
|
||||
if err != nil {
|
||||
// TODO(sdboyer) this could well happen; handle it with a more graceful error
|
||||
panic(fmt.Sprintf("shouldn't be possible %s", err))
|
||||
|
@ -537,16 +471,16 @@ func (s *solver) selectRoot() error {
|
|||
// If we have no lock, or if this dep isn't in the lock, then prefetch
|
||||
// it. See longer explanation in selectAtom() for how we benefit from
|
||||
// parallelism here.
|
||||
if s.needVersionsFor(dep.Ident.ProjectRoot) {
|
||||
if s.rd.needVersionsFor(dep.Ident.ProjectRoot) {
|
||||
go s.b.SyncSourceFor(dep.Ident)
|
||||
}
|
||||
|
||||
s.sel.pushDep(dependency{depender: pa, dep: dep})
|
||||
s.sel.pushDep(dependency{depender: awp.a, dep: dep})
|
||||
// Add all to unselected queue
|
||||
heap.Push(s.unsel, bimodalIdentifier{id: dep.Ident, pl: dep.pl, fromRoot: true})
|
||||
}
|
||||
|
||||
s.traceSelectRoot(s.rpt, deps)
|
||||
s.traceSelectRoot(s.rd.rpt, deps)
|
||||
s.mtr.pop()
|
||||
return nil
|
||||
}
|
||||
|
@ -554,7 +488,7 @@ func (s *solver) selectRoot() error {
|
|||
func (s *solver) getImportsAndConstraintsOf(a atomWithPackages) ([]completeDep, error) {
|
||||
var err error
|
||||
|
||||
if ProjectRoot(s.rpt.ImportRoot) == a.a.id.ProjectRoot {
|
||||
if s.rd.isRoot(a.a.id.ProjectRoot) {
|
||||
panic("Should never need to recheck imports/constraints from root during solve")
|
||||
}
|
||||
|
||||
|
@ -570,7 +504,7 @@ func (s *solver) getImportsAndConstraintsOf(a atomWithPackages) ([]completeDep,
|
|||
return nil, err
|
||||
}
|
||||
|
||||
allex := ptree.ExternalReach(false, false, s.ig)
|
||||
allex := ptree.ExternalReach(false, false, s.rd.ig)
|
||||
// Use a map to dedupe the unique external packages
|
||||
exmap := make(map[string]struct{})
|
||||
// Add to the list those packages that are reached by the packages
|
||||
|
@ -603,7 +537,7 @@ func (s *solver) getImportsAndConstraintsOf(a atomWithPackages) ([]completeDep,
|
|||
}
|
||||
sort.Strings(reach)
|
||||
|
||||
deps := s.ovr.overrideAll(m.DependencyConstraints())
|
||||
deps := s.rd.ovr.overrideAll(m.DependencyConstraints())
|
||||
return s.intersectConstraintsWithImports(deps, reach)
|
||||
}
|
||||
|
||||
|
@ -629,8 +563,7 @@ func (s *solver) intersectConstraintsWithImports(deps []workingConstraint, reach
|
|||
|
||||
// Look for a prefix match; it'll be the root project/repo containing
|
||||
// the reached package
|
||||
if pre, idep, match := xt.LongestPrefix(rp); match {
|
||||
if isPathPrefixOrEqual(pre, rp) {
|
||||
if pre, idep, match := xt.LongestPrefix(rp); match && isPathPrefixOrEqual(pre, rp) {
|
||||
// Match is valid; put it in the dmap, either creating a new
|
||||
// completeDep or appending it to the existing one for this base
|
||||
// project/prefix.
|
||||
|
@ -646,7 +579,6 @@ func (s *solver) intersectConstraintsWithImports(deps []workingConstraint, reach
|
|||
}
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// No match. Let the SourceManager try to figure out the root
|
||||
root, err := s.b.DeduceProjectRoot(rp)
|
||||
|
@ -656,7 +588,7 @@ func (s *solver) intersectConstraintsWithImports(deps []workingConstraint, reach
|
|||
}
|
||||
|
||||
// Make a new completeDep with an open constraint, respecting overrides
|
||||
pd := s.ovr.override(root, ProjectProperties{Constraint: Any()})
|
||||
pd := s.rd.ovr.override(root, ProjectProperties{Constraint: Any()})
|
||||
|
||||
// Insert the pd into the trie so that further deps from this
|
||||
// project get caught by the prefix search
|
||||
|
@ -682,7 +614,7 @@ func (s *solver) intersectConstraintsWithImports(deps []workingConstraint, reach
|
|||
func (s *solver) createVersionQueue(bmi bimodalIdentifier) (*versionQueue, error) {
|
||||
id := bmi.id
|
||||
// If on the root package, there's no queue to make
|
||||
if ProjectRoot(s.rpt.ImportRoot) == id.ProjectRoot {
|
||||
if s.rd.isRoot(id.ProjectRoot) {
|
||||
return newVersionQueue(id, nil, nil, s.b)
|
||||
}
|
||||
|
||||
|
@ -704,7 +636,7 @@ func (s *solver) createVersionQueue(bmi bimodalIdentifier) (*versionQueue, error
|
|||
}
|
||||
|
||||
var lockv Version
|
||||
if len(s.rlm) > 0 {
|
||||
if len(s.rd.rlm) > 0 {
|
||||
lockv, err = s.getLockVersionIfValid(id)
|
||||
if err != nil {
|
||||
// Can only get an error here if an upgrade was expressly requested on
|
||||
|
@ -722,7 +654,7 @@ func (s *solver) createVersionQueue(bmi bimodalIdentifier) (*versionQueue, error
|
|||
// TODO(sdboyer) nested loop; prime candidate for a cache somewhere
|
||||
for _, dep := range s.sel.getDependenciesOn(bmi.id) {
|
||||
// Skip the root, of course
|
||||
if ProjectRoot(s.rpt.ImportRoot) == dep.depender.id.ProjectRoot {
|
||||
if s.rd.isRoot(dep.depender.id.ProjectRoot) {
|
||||
continue
|
||||
}
|
||||
|
||||
|
@ -857,7 +789,7 @@ func (s *solver) findValidVersion(q *versionQueue, pl []string) error {
|
|||
func (s *solver) getLockVersionIfValid(id ProjectIdentifier) (Version, error) {
|
||||
// If the project is specifically marked for changes, then don't look for a
|
||||
// locked version.
|
||||
if _, explicit := s.chng[id.ProjectRoot]; explicit || s.params.ChangeAll {
|
||||
if _, explicit := s.rd.chng[id.ProjectRoot]; explicit || s.rd.chngall {
|
||||
// For projects with an upstream or cache repository, it's safe to
|
||||
// ignore what's in the lock, because there's presumably more versions
|
||||
// to be found and attempted in the repository. If it's only in vendor,
|
||||
|
@ -880,7 +812,7 @@ func (s *solver) getLockVersionIfValid(id ProjectIdentifier) (Version, error) {
|
|||
}
|
||||
}
|
||||
|
||||
lp, exists := s.rlm[id.ProjectRoot]
|
||||
lp, exists := s.rd.rlm[id.ProjectRoot]
|
||||
if !exists {
|
||||
return nil, nil
|
||||
}
|
||||
|
@ -922,29 +854,6 @@ func (s *solver) getLockVersionIfValid(id ProjectIdentifier) (Version, error) {
|
|||
return v, nil
|
||||
}
|
||||
|
||||
// needVersionListFor indicates whether we need a version list for a given
|
||||
// project root, based solely on general solver inputs (no constraint checking
|
||||
// required). This will be true if:
|
||||
//
|
||||
// - ChangeAll is on
|
||||
// - The project is not in the lock at all
|
||||
// - The project is in the lock, but is also in the list of projects to change
|
||||
func (s *solver) needVersionsFor(pr ProjectRoot) bool {
|
||||
if s.params.ChangeAll {
|
||||
return true
|
||||
}
|
||||
|
||||
if _, has := s.rlm[pr]; !has {
|
||||
// not in the lock
|
||||
return true
|
||||
} else if _, has := s.chng[pr]; has {
|
||||
// in the lock, but marked for change
|
||||
return true
|
||||
}
|
||||
// in the lock, not marked for change
|
||||
return false
|
||||
}
|
||||
|
||||
// backtrack works backwards from the current failed solution to find the next
|
||||
// solution to try.
|
||||
func (s *solver) backtrack() bool {
|
||||
|
@ -1059,8 +968,8 @@ func (s *solver) unselectedComparator(i, j int) bool {
|
|||
return false
|
||||
}
|
||||
|
||||
_, ilock := s.rlm[iname.ProjectRoot]
|
||||
_, jlock := s.rlm[jname.ProjectRoot]
|
||||
_, ilock := s.rd.rlm[iname.ProjectRoot]
|
||||
_, jlock := s.rd.rlm[jname.ProjectRoot]
|
||||
|
||||
switch {
|
||||
case ilock && !jlock:
|
||||
|
@ -1105,7 +1014,7 @@ func (s *solver) fail(id ProjectIdentifier) {
|
|||
// selection?
|
||||
|
||||
// skip if the root project
|
||||
if ProjectRoot(s.rpt.ImportRoot) != id.ProjectRoot {
|
||||
if !s.rd.isRoot(id.ProjectRoot) {
|
||||
// just look for the first (oldest) one; the backtracker will necessarily
|
||||
// traverse through and pop off any earlier ones
|
||||
for _, vq := range s.vqs {
|
||||
|
@ -1167,7 +1076,7 @@ func (s *solver) selectAtom(a atomWithPackages, pkgonly bool) {
|
|||
// few microseconds before blocking later. Best case, the dep doesn't
|
||||
// come up next, but some other dep comes up that wasn't prefetched, and
|
||||
// both fetches proceed in parallel.
|
||||
if s.needVersionsFor(dep.Ident.ProjectRoot) {
|
||||
if s.rd.needVersionsFor(dep.Ident.ProjectRoot) {
|
||||
go s.b.SyncSourceFor(dep.Ident)
|
||||
}
|
||||
|
||||
|
|
20
trace.go
20
trace.go
|
@ -15,7 +15,7 @@ const (
|
|||
)
|
||||
|
||||
func (s *solver) traceCheckPkgs(bmi bimodalIdentifier) {
|
||||
if !s.params.Trace {
|
||||
if s.tl == nil {
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -24,7 +24,7 @@ func (s *solver) traceCheckPkgs(bmi bimodalIdentifier) {
|
|||
}
|
||||
|
||||
func (s *solver) traceCheckQueue(q *versionQueue, bmi bimodalIdentifier, cont bool, offset int) {
|
||||
if !s.params.Trace {
|
||||
if s.tl == nil {
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -49,7 +49,7 @@ func (s *solver) traceCheckQueue(q *versionQueue, bmi bimodalIdentifier, cont bo
|
|||
// traceStartBacktrack is called with the bmi that first failed, thus initiating
|
||||
// backtracking
|
||||
func (s *solver) traceStartBacktrack(bmi bimodalIdentifier, err error, pkgonly bool) {
|
||||
if !s.params.Trace {
|
||||
if s.tl == nil {
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -67,7 +67,7 @@ func (s *solver) traceStartBacktrack(bmi bimodalIdentifier, err error, pkgonly b
|
|||
// traceBacktrack is called when a package or project is poppped off during
|
||||
// backtracking
|
||||
func (s *solver) traceBacktrack(bmi bimodalIdentifier, pkgonly bool) {
|
||||
if !s.params.Trace {
|
||||
if s.tl == nil {
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -84,7 +84,7 @@ func (s *solver) traceBacktrack(bmi bimodalIdentifier, pkgonly bool) {
|
|||
|
||||
// Called just once after solving has finished, whether success or not
|
||||
func (s *solver) traceFinish(sol solution, err error) {
|
||||
if !s.params.Trace {
|
||||
if s.tl == nil {
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -101,15 +101,15 @@ func (s *solver) traceFinish(sol solution, err error) {
|
|||
|
||||
// traceSelectRoot is called just once, when the root project is selected
|
||||
func (s *solver) traceSelectRoot(ptree PackageTree, cdeps []completeDep) {
|
||||
if !s.params.Trace {
|
||||
if s.tl == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// This duplicates work a bit, but we're in trace mode and it's only once,
|
||||
// so who cares
|
||||
rm := ptree.ExternalReach(true, true, s.ig)
|
||||
rm := ptree.ExternalReach(true, true, s.rd.ig)
|
||||
|
||||
s.tl.Printf("Root project is %q", s.rpt.ImportRoot)
|
||||
s.tl.Printf("Root project is %q", s.rd.rpt.ImportRoot)
|
||||
|
||||
var expkgs int
|
||||
for _, cdep := range cdeps {
|
||||
|
@ -124,7 +124,7 @@ func (s *solver) traceSelectRoot(ptree PackageTree, cdeps []completeDep) {
|
|||
|
||||
// traceSelect is called when an atom is successfully selected
|
||||
func (s *solver) traceSelect(awp atomWithPackages, pkgonly bool) {
|
||||
if !s.params.Trace {
|
||||
if s.tl == nil {
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -140,7 +140,7 @@ func (s *solver) traceSelect(awp atomWithPackages, pkgonly bool) {
|
|||
}
|
||||
|
||||
func (s *solver) traceInfo(args ...interface{}) {
|
||||
if !s.params.Trace {
|
||||
if s.tl == nil {
|
||||
return
|
||||
}
|
||||
|
||||
|
|
Загрузка…
Ссылка в новой задаче