blob: 379cd4b0523d7f2d131f10fb6dd661d7d7dc433f [file] [log] [blame]
package gps
import (
"fmt"
"os"
"path/filepath"
"sync/atomic"
"github.com/Masterminds/semver"
)
// sourceBridges provide an adapter to SourceManagers that tailor operations
// for a single solve run.
type sourceBridge interface {
SourceManager // composes SourceManager
verifyRootDir(path string) error
pairRevision(id ProjectIdentifier, r Revision) []Version
pairVersion(id ProjectIdentifier, v UnpairedVersion) PairedVersion
vendorCodeExists(id ProjectIdentifier) (bool, error)
matches(id ProjectIdentifier, c Constraint, v Version) bool
matchesAny(id ProjectIdentifier, c1, c2 Constraint) bool
intersect(id ProjectIdentifier, c1, c2 Constraint) Constraint
breakLock()
}
// bridge is an adapter around a proper SourceManager. It provides localized
// caching that's tailored to the requirements of a particular solve run.
//
// Finally, it provides authoritative version/constraint operations, ensuring
// that any possible approach to a match - even those not literally encoded in
// the inputs - is achieved.
type bridge struct {
// The underlying, adapted-to SourceManager
sm SourceManager
// The solver which we're assisting.
//
// The link between solver and bridge is circular, which is typically a bit
// awkward, but the bridge needs access to so many of the input arguments
// held by the solver that it ends up being easier and saner to do this.
s *solver
// Simple, local cache of the root's PackageTree
crp *struct {
ptree PackageTree
err error
}
// Map of project root name to their available version list. This cache is
// layered on top of the proper SourceManager's cache; the only difference
// is that this keeps the versions sorted in the direction required by the
// current solve run
vlists map[ProjectIdentifier][]Version
// Indicates whether lock breaking has already been run
lockbroken int32
}
// 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(*solver, SourceManager) sourceBridge = func(s *solver, sm SourceManager) sourceBridge {
return &bridge{
sm: sm,
s: s,
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
}
return b.sm.GetManifestAndLock(id, v)
}
func (b *bridge) AnalyzerInfo() (string, *semver.Version) {
return b.sm.AnalyzerInfo()
}
func (b *bridge) ListVersions(id ProjectIdentifier) ([]Version, error) {
if vl, exists := b.vlists[id]; exists {
return vl, nil
}
vl, err := b.sm.ListVersions(id)
// TODO(sdboyer) cache errors, too?
if err != nil {
return nil, err
}
if b.s.params.Downgrade {
SortForDowngrade(vl)
} else {
SortForUpgrade(vl)
}
b.vlists[id] = vl
return vl, nil
}
func (b *bridge) RevisionPresentIn(id ProjectIdentifier, r Revision) (bool, error) {
return b.sm.RevisionPresentIn(id, r)
}
func (b *bridge) SourceExists(id ProjectIdentifier) (bool, error) {
return b.sm.SourceExists(id)
}
func (b *bridge) vendorCodeExists(id ProjectIdentifier) (bool, error) {
fi, err := os.Stat(filepath.Join(b.s.params.RootDir, "vendor", string(id.ProjectRoot)))
if err != nil {
return false, err
} else if fi.IsDir() {
return true, nil
}
return false, nil
}
func (b *bridge) pairVersion(id ProjectIdentifier, v UnpairedVersion) PairedVersion {
vl, err := b.ListVersions(id)
if err != nil {
return nil
}
// doing it like this is a bit sloppy
for _, v2 := range vl {
if p, ok := v2.(PairedVersion); ok {
if p.Matches(v) {
return p
}
}
}
return nil
}
func (b *bridge) pairRevision(id ProjectIdentifier, r Revision) []Version {
vl, err := b.ListVersions(id)
if err != nil {
return nil
}
p := []Version{r}
// doing it like this is a bit sloppy
for _, v2 := range vl {
if pv, ok := v2.(PairedVersion); ok {
if pv.Matches(r) {
p = append(p, pv)
}
}
}
return p
}
// matches performs a typical match check between the provided version and
// constraint. If that basic check fails and the provided version is incomplete
// (e.g. an unpaired version or bare revision), it will attempt to gather more
// information on one or the other and re-perform the comparison.
func (b *bridge) matches(id ProjectIdentifier, c2 Constraint, v Version) bool {
if c2.Matches(v) {
return true
}
// There's a wide field of possible ways that pairing might result in a
// match. For each possible type of version, start by carving out all the
// cases where the constraint would have provided an authoritative match
// result.
switch tv := v.(type) {
case PairedVersion:
switch tc := c2.(type) {
case PairedVersion, Revision, noneConstraint:
// These three would all have been authoritative matches
return false
case UnpairedVersion:
// Only way paired and unpaired could match is if they share an
// underlying rev
pv := b.pairVersion(id, tc)
if pv == nil {
return false
}
return pv.Matches(v)
case semverConstraint:
// Have to check all the possible versions for that rev to see if
// any match the semver constraint
for _, pv := range b.pairRevision(id, tv.Underlying()) {
if tc.Matches(pv) {
return true
}
}
return false
}
case Revision:
switch tc := c2.(type) {
case PairedVersion, Revision, noneConstraint:
// These three would all have been authoritative matches
return false
case UnpairedVersion:
// Only way paired and unpaired could match is if they share an
// underlying rev
pv := b.pairVersion(id, tc)
if pv == nil {
return false
}
return pv.Matches(v)
case semverConstraint:
// Have to check all the possible versions for the rev to see if
// any match the semver constraint
for _, pv := range b.pairRevision(id, tv) {
if tc.Matches(pv) {
return true
}
}
return false
}
// UnpairedVersion as input has the most weird cases. It's also the one
// we'll probably see the least
case UnpairedVersion:
switch tc := c2.(type) {
case noneConstraint:
// obviously
return false
case Revision, PairedVersion:
// Easy case for both - just pair the uv and see if it matches the revision
// constraint
pv := b.pairVersion(id, tv)
if pv == nil {
return false
}
return tc.Matches(pv)
case UnpairedVersion:
// Both are unpaired versions. See if they share an underlying rev.
pv := b.pairVersion(id, tv)
if pv == nil {
return false
}
pc := b.pairVersion(id, tc)
if pc == nil {
return false
}
return pc.Matches(pv)
case semverConstraint:
// semverConstraint can't ever match a rev, but we do need to check
// if any other versions corresponding to this rev work.
pv := b.pairVersion(id, tv)
if pv == nil {
return false
}
for _, ttv := range b.pairRevision(id, pv.Underlying()) {
if c2.Matches(ttv) {
return true
}
}
return false
}
default:
panic("unreachable")
}
return false
}
// matchesAny is the authoritative version of Constraint.MatchesAny.
func (b *bridge) matchesAny(id ProjectIdentifier, c1, c2 Constraint) bool {
if c1.MatchesAny(c2) {
return true
}
// This approach is slightly wasteful, but just SO much less verbose, and
// more easily understood.
var uc1, uc2 Constraint
if v1, ok := c1.(Version); ok {
uc1 = b.vtu(id, v1)
} else {
uc1 = c1
}
if v2, ok := c2.(Version); ok {
uc2 = b.vtu(id, v2)
} else {
uc2 = c2
}
return uc1.MatchesAny(uc2)
}
// intersect is the authoritative version of Constraint.Intersect.
func (b *bridge) intersect(id ProjectIdentifier, c1, c2 Constraint) Constraint {
rc := c1.Intersect(c2)
if rc != none {
return rc
}
// This approach is slightly wasteful, but just SO much less verbose, and
// more easily understood.
var uc1, uc2 Constraint
if v1, ok := c1.(Version); ok {
uc1 = b.vtu(id, v1)
} else {
uc1 = c1
}
if v2, ok := c2.(Version); ok {
uc2 = b.vtu(id, v2)
} else {
uc2 = c2
}
return uc1.Intersect(uc2)
}
// vtu creates a versionTypeUnion for the provided version.
//
// This union may (and typically will) end up being nothing more than the single
// input version, but creating a versionTypeUnion guarantees that 'local'
// constraint checks (direct method calls) are authoritative.
func (b *bridge) vtu(id ProjectIdentifier, v Version) versionTypeUnion {
switch tv := v.(type) {
case Revision:
return versionTypeUnion(b.pairRevision(id, tv))
case PairedVersion:
return versionTypeUnion(b.pairRevision(id, tv.Underlying()))
case UnpairedVersion:
pv := b.pairVersion(id, tv)
if pv == nil {
return versionTypeUnion{tv}
}
return versionTypeUnion(b.pairRevision(id, pv.Underlying()))
}
return nil
}
// listPackages lists all the packages contained within the given project at a
// particular version.
//
// 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) {
panic("should never call ListPackages on root project")
}
return b.sm.ListPackages(id, v)
}
func (b *bridge) ExportProject(id ProjectIdentifier, v Version, path string) error {
//return b.sm.ExportProject(id, v, path)
panic("bridge should never be used to ExportProject")
}
// verifyRoot ensures that the provided path to the project root is in good
// working condition. This check is made only once, at the beginning of a solve
// run.
func (b *bridge) verifyRootDir(path string) error {
if fi, err := os.Stat(path); err != nil {
return badOptsFailure(fmt.Sprintf("could not read project root (%s): %s", path, err))
} else if !fi.IsDir() {
return badOptsFailure(fmt.Sprintf("project root (%s) is a file, not a directory", path))
}
return nil
}
func (b *bridge) DeduceProjectRoot(ip string) (ProjectRoot, error) {
return b.sm.DeduceProjectRoot(ip)
}
// breakLock is called when the solver has to break a version recorded in the
// lock file. It prefetches all the projects in the solver's lock , so that the
// information is already on hand if/when the solver needs it.
//
// Projects that have already been selected are skipped, as it's generally unlikely that the
// solver will have to backtrack through and fully populate their version queues.
func (b *bridge) breakLock() {
// No real conceivable circumstance in which multiple calls are made to
// this, but being that this is the entrance point to a bunch of async work,
// protect it with an atomic CAS in case things change in the future.
if !atomic.CompareAndSwapInt32(&b.lockbroken, 0, 1) {
return
}
for _, lp := range b.s.rl.Projects() {
if _, is := b.s.sel.selected(lp.pi); !is {
// ListPackages guarantees that all the necessary network work will
// be done, so go with that
//
// TODO(sdboyer) use this as an opportunity to detect
// inconsistencies between upstream and the lock (e.g., moved tags)?
pi, v := lp.pi, lp.Version()
go func() {
// Sync first
b.sm.SyncSourceFor(pi)
// Preload the package info for the locked version, too, as
// we're more likely to need that
b.sm.ListPackages(pi, v)
}()
}
}
}
func (b *bridge) SyncSourceFor(id ProjectIdentifier) error {
return b.sm.SyncSourceFor(id)
}
// versionTypeUnion represents a set of versions that are, within the scope of
// this solver run, equivalent.
//
// The simple case here is just a pair - a normal version plus its underlying
// revision - but if a tag or branch point at the same rev, then we consider
// them equivalent. Again, however, this equivalency is short-lived; it must be
// re-assessed during every solver run.
//
// The union members are treated as being OR'd together: all constraint
// operations attempt each member, and will take the most open/optimistic
// answer.
//
// This technically does allow tags to match branches - something we
// otherwise try hard to avoid - but because the original input constraint never
// actually changes (and is never written out in the Result), there's no harmful
// case of a user suddenly riding a branch when they expected a fixed tag.
type versionTypeUnion []Version
// This should generally not be called, but is required for the interface. If it
// is called, we have a bigger problem (the type has escaped the solver); thus,
// panic.
func (av versionTypeUnion) String() string {
panic("versionTypeUnion should never be turned into a string; it is solver internal-only")
}
// This should generally not be called, but is required for the interface. If it
// is called, we have a bigger problem (the type has escaped the solver); thus,
// panic.
func (av versionTypeUnion) Type() string {
panic("versionTypeUnion should never need to answer a Type() call; it is solver internal-only")
}
// Matches takes a version, and returns true if that version matches any version
// contained in the union.
//
// This DOES allow tags to match branches, albeit indirectly through a revision.
func (av versionTypeUnion) Matches(v Version) bool {
av2, oav := v.(versionTypeUnion)
for _, v1 := range av {
if oav {
for _, v2 := range av2 {
if v1.Matches(v2) {
return true
}
}
} else if v1.Matches(v) {
return true
}
}
return false
}
// MatchesAny returns true if any of the contained versions (which are also
// constraints) in the union successfully MatchAny with the provided
// constraint.
func (av versionTypeUnion) MatchesAny(c Constraint) bool {
av2, oav := c.(versionTypeUnion)
for _, v1 := range av {
if oav {
for _, v2 := range av2 {
if v1.MatchesAny(v2) {
return true
}
}
} else if v1.MatchesAny(c) {
return true
}
}
return false
}
// Intersect takes a constraint, and attempts to intersect it with all the
// versions contained in the union until one returns non-none. If that never
// happens, then none is returned.
//
// In order to avoid weird version floating elsewhere in the solver, the union
// always returns the input constraint. (This is probably obviously correct, but
// is still worth noting.)
func (av versionTypeUnion) Intersect(c Constraint) Constraint {
av2, oav := c.(versionTypeUnion)
for _, v1 := range av {
if oav {
for _, v2 := range av2 {
if rc := v1.Intersect(v2); rc != none {
return rc
}
}
} else if rc := v1.Intersect(c); rc != none {
return rc
}
}
return none
}
func (av versionTypeUnion) _private() {}