blob: b79da2cea2a3c0e64314c035644973e48d1ff82d [file] [log] [blame]
package dependency
import (
"container/list"
"errors"
"runtime"
"sort"
//"go/build"
"os"
"path/filepath"
"strings"
"github.com/Masterminds/glide/cfg"
"github.com/Masterminds/glide/msg"
gpath "github.com/Masterminds/glide/path"
"github.com/Masterminds/glide/util"
)
// MissingPackageHandler handles the case where a package is missing during scanning.
//
// It returns true if the package can be passed to the resolver, false otherwise.
// False may be returned even if error is nil.
type MissingPackageHandler interface {
// NotFound is called when the Resolver fails to find a package with the given name.
//
// NotFound returns true when the resolver should attempt to re-resole the
// dependency (e.g. when NotFound has gone and fetched the missing package).
//
// When NotFound returns false, the Resolver does not try to do any additional
// work on the missing package.
//
// NotFound only returns errors when it fails to perform its internal goals.
// When it returns false with no error, this indicates that the handler did
// its job, but the resolver should not do any additional work on the
// package.
NotFound(pkg string, addTest bool) (bool, error)
// OnGopath is called when the Resolver finds a dependency, but it's only on GOPATH.
//
// OnGopath provides an opportunity to copy, move, warn, or ignore cases like this.
//
// OnGopath returns true when the resolver should attempt to re-resolve the
// dependency (e.g. when the dependency is copied to a new location).
//
// When OnGopath returns false, the Resolver does not try to do any additional
// work on the package.
//
// An error indicates that OnGopath cannot complete its intended operation.
// Not all false results are errors.
OnGopath(pkg string, addTest bool) (bool, error)
// InVendor is called when the Resolver finds a dependency in the vendor/ directory.
//
// This can be used update a project found in the vendor/ folder.
InVendor(pkg string, addTest bool) error
// PkgPath is called to find the location locally to scan. This gives the
// handler to do things such as use a cached location.
PkgPath(pkg string) string
}
// DefaultMissingPackageHandler is the default handler for missing packages.
//
// When asked to handle a missing package, it will report the miss as a warning,
// and then store the package in the Missing slice for later access.
type DefaultMissingPackageHandler struct {
Missing []string
Gopath []string
Prefix string
}
// NotFound prints a warning and then stores the package name in Missing.
//
// It never returns an error, and it always returns false.
func (d *DefaultMissingPackageHandler) NotFound(pkg string, addTest bool) (bool, error) {
msg.Warn("Package %s is not installed", pkg)
d.Missing = append(d.Missing, pkg)
return false, nil
}
// OnGopath is run when a package is missing from vendor/ but found in the GOPATH
func (d *DefaultMissingPackageHandler) OnGopath(pkg string, addTest bool) (bool, error) {
msg.Warn("Package %s is only on GOPATH.", pkg)
d.Gopath = append(d.Gopath, pkg)
return false, nil
}
// InVendor is run when a package is found in the vendor/ folder
func (d *DefaultMissingPackageHandler) InVendor(pkg string, addTest bool) error {
msg.Info("Package %s found in vendor/ folder", pkg)
return nil
}
// PkgPath returns the path to the package
func (d *DefaultMissingPackageHandler) PkgPath(pkg string) string {
if d.Prefix != "" {
return filepath.Join(d.Prefix, pkg)
}
return pkg
}
// VersionHandler sets the version for a package when found while scanning.
//
// When a package if found it needs to be on the correct version before
// scanning its contents to be sure to pick up the right elements for that
// version.
type VersionHandler interface {
// Process provides an opportunity to process the codebase for version setting.
Process(pkg string) error
// SetVersion sets the version for a package. An error is returned if there
// was a problem setting the version.
SetVersion(pkg string, testDep bool) error
}
// DefaultVersionHandler is the default handler for setting the version.
//
// The default handler leaves the current version and skips setting a version.
// For a handler that alters the version see the handler included in the repo
// package as part of the installer.
type DefaultVersionHandler struct{}
// Process a package to aide in version setting.
func (d *DefaultVersionHandler) Process(pkg string) error {
return nil
}
// SetVersion here sends a message when a package is found noting that it
// did not set the version.
func (d *DefaultVersionHandler) SetVersion(pkg string, testDep bool) error {
msg.Warn("Version not set for package %s", pkg)
return nil
}
// Resolver resolves a dependency tree.
//
// It operates in two modes:
// - local resolution (ResolveLocal) determines the dependencies of the local project.
// - vendor resolving (Resolve, ResolveAll) determines the dependencies of vendored
// projects.
//
// Local resolution is for guessing initial dependencies. Vendor resolution is
// for determining vendored dependencies.
type Resolver struct {
Handler MissingPackageHandler
VersionHandler VersionHandler
VendorDir string
BuildContext *util.BuildCtxt
Config *cfg.Config
// ResolveAllFiles toggles deep scanning.
// If this is true, resolve by scanning all files, not by walking the
// import tree.
ResolveAllFiles bool
// ResolveTest sets if test dependencies should be resolved.
ResolveTest bool
// Items already in the queue.
alreadyQ map[string]bool
// Attempts to scan that had unrecoverable error.
hadError map[string]bool
basedir string
seen map[string]bool
// findCache caches hits from Find. This reduces the number of filesystem
// touches that have to be done for dependency resolution.
findCache map[string]*PkgInfo
}
// NewResolver returns a new Resolver initialized with the DefaultMissingPackageHandler.
//
// This will return an error if the given path does not meet the basic criteria
// for a Go source project. For example, basedir must have a vendor subdirectory.
//
// The BuildContext uses the "go/build".Default to resolve dependencies.
func NewResolver(basedir string) (*Resolver, error) {
var err error
basedir, err = filepath.Abs(basedir)
if err != nil {
return nil, err
}
basedir, err = checkForBasedirSymlink(basedir)
if err != nil {
return nil, err
}
vdir := filepath.Join(basedir, "vendor")
buildContext, err := util.GetBuildContext()
if err != nil {
return nil, err
}
r := &Resolver{
Handler: &DefaultMissingPackageHandler{Missing: []string{}, Gopath: []string{}},
VersionHandler: &DefaultVersionHandler{},
basedir: basedir,
VendorDir: vdir,
BuildContext: buildContext,
seen: map[string]bool{},
alreadyQ: map[string]bool{},
hadError: map[string]bool{},
findCache: map[string]*PkgInfo{},
// The config instance here should really be replaced with a real one.
Config: &cfg.Config{},
}
// TODO: Make sure the build context is correctly set up. Especially in
// regards to GOROOT, which is not always set.
return r, nil
}
// Resolve takes a package name and returns all of the imported package names.
//
// If a package is not found, this calls the Fetcher. If the Fetcher returns
// true, it will re-try traversing that package for dependencies. Otherwise it
// will add that package to the deps array and continue on without trying it.
// And if the Fetcher returns an error, this will stop resolution and return
// the error.
//
// If basepath is set to $GOPATH, this will start from that package's root there.
// If basepath is set to a project's vendor path, the scanning will begin from
// there.
func (r *Resolver) Resolve(pkg, basepath string) ([]string, error) {
target := filepath.Join(basepath, filepath.FromSlash(pkg))
//msg.Debug("Scanning %s", target)
l := list.New()
l.PushBack(target)
// In this mode, walk the entire tree.
if r.ResolveAllFiles {
return r.resolveList(l, false, false)
}
return r.resolveImports(l, false, false)
}
// dirHasPrefix tests whether the directory dir begins with prefix.
func dirHasPrefix(dir, prefix string) bool {
if runtime.GOOS != "windows" {
return strings.HasPrefix(dir, prefix)
}
return len(dir) >= len(prefix) && strings.EqualFold(dir[:len(prefix)], prefix)
}
// ResolveLocal resolves dependencies for the current project.
//
// This begins with the project, builds up a list of external dependencies.
//
// If the deep flag is set to true, this will then resolve all of the dependencies
// of the dependencies it has found. If not, it will return just the packages that
// the base project relies upon.
func (r *Resolver) ResolveLocal(deep bool) ([]string, []string, error) {
// We build a list of local source to walk, then send this list
// to resolveList.
msg.Debug("Resolving local dependencies")
l := list.New()
tl := list.New()
alreadySeen := map[string]bool{}
talreadySeen := map[string]bool{}
err := filepath.Walk(r.basedir, func(path string, fi os.FileInfo, err error) error {
if err != nil && err != filepath.SkipDir {
return err
}
pt := strings.TrimPrefix(path, r.basedir+string(os.PathSeparator))
pt = strings.TrimSuffix(pt, string(os.PathSeparator))
if r.Config.HasExclude(pt) {
msg.Debug("Excluding %s", pt)
return filepath.SkipDir
}
if !fi.IsDir() {
return nil
}
if !srcDir(fi) {
return filepath.SkipDir
}
// Scan for dependencies, and anything that's not part of the local
// package gets added to the scan list.
var imps []string
var testImps []string
p, err := r.BuildContext.ImportDir(path, 0)
if err != nil {
if strings.HasPrefix(err.Error(), "no buildable Go source") {
return nil
} else if strings.HasPrefix(err.Error(), "found packages ") {
// If we got here it's because a package and multiple packages
// declared. This is often because of an example with a package
// or main but +build ignore as a build tag. In that case we
// try to brute force the packages with a slower scan.
imps, testImps, err = IterativeScan(path)
if err != nil {
return err
}
} else {
return err
}
} else {
imps = p.Imports
testImps = dedupeStrings(p.TestImports, p.XTestImports)
}
// We are only looking for dependencies in vendor. No root, cgo, etc.
for _, imp := range imps {
if r.Config.HasIgnore(imp) {
continue
}
if alreadySeen[imp] {
continue
}
alreadySeen[imp] = true
info := r.FindPkg(imp)
switch info.Loc {
case LocUnknown, LocVendor:
l.PushBack(filepath.Join(r.VendorDir, filepath.FromSlash(imp))) // Do we need a path on this?
case LocGopath:
if !dirHasPrefix(info.Path, r.basedir) {
// FIXME: This is a package outside of the project we're
// scanning. It should really be on vendor. But we don't
// want it to reference GOPATH. We want it to be detected
// and moved.
l.PushBack(filepath.Join(r.VendorDir, filepath.FromSlash(imp)))
}
case LocRelative:
if strings.HasPrefix(imp, "./"+gpath.VendorDir) {
msg.Warn("Go package resolving will resolve %s without the ./%s/ prefix", imp, gpath.VendorDir)
}
}
}
if r.ResolveTest {
for _, imp := range testImps {
if talreadySeen[imp] {
continue
}
talreadySeen[imp] = true
info := r.FindPkg(imp)
switch info.Loc {
case LocUnknown, LocVendor:
tl.PushBack(filepath.Join(r.VendorDir, filepath.FromSlash(imp))) // Do we need a path on this?
case LocGopath:
if !dirHasPrefix(info.Path, r.basedir) {
// FIXME: This is a package outside of the project we're
// scanning. It should really be on vendor. But we don't
// want it to reference GOPATH. We want it to be detected
// and moved.
tl.PushBack(filepath.Join(r.VendorDir, filepath.FromSlash(imp)))
}
case LocRelative:
if strings.HasPrefix(imp, "./"+gpath.VendorDir) {
msg.Warn("Go package resolving will resolve %s without the ./%s/ prefix", imp, gpath.VendorDir)
}
}
}
}
return nil
})
if err != nil {
msg.Err("Failed to build an initial list of packages to scan: %s", err)
return []string{}, []string{}, err
}
if deep {
if r.ResolveAllFiles {
re, err := r.resolveList(l, false, false)
if err != nil {
return []string{}, []string{}, err
}
tre, err := r.resolveList(l, false, true)
return re, tre, err
}
re, err := r.resolveImports(l, false, false)
if err != nil {
return []string{}, []string{}, err
}
tre, err := r.resolveImports(tl, true, true)
return re, tre, err
}
// If we're not doing a deep scan, we just convert the list into an
// array and return.
res := make([]string, 0, l.Len())
for e := l.Front(); e != nil; e = e.Next() {
res = append(res, e.Value.(string))
}
tres := make([]string, 0, l.Len())
if r.ResolveTest {
for e := tl.Front(); e != nil; e = e.Next() {
tres = append(tres, e.Value.(string))
}
}
return res, tres, nil
}
// ResolveAll takes a list of packages and returns an inclusive list of all
// vendored dependencies.
//
// While this will scan all of the source code it can find, it will only return
// packages that were either explicitly passed in as deps, or were explicitly
// imported by the code.
//
// Packages that are either CGO or on GOROOT are ignored. Packages that are
// on GOPATH, but not vendored currently generate a warning.
//
// If one of the passed in packages does not exist in the vendor directory,
// an error is returned.
func (r *Resolver) ResolveAll(deps []*cfg.Dependency, addTest bool) ([]string, error) {
queue := sliceToQueue(deps, r.VendorDir)
if r.ResolveAllFiles {
return r.resolveList(queue, false, addTest)
}
return r.resolveImports(queue, false, addTest)
}
// Stripv strips the vendor/ prefix from vendored packages.
func (r *Resolver) Stripv(str string) string {
return strings.TrimPrefix(str, r.VendorDir+string(os.PathSeparator))
}
// vpath adds an absolute vendor path.
func (r *Resolver) vpath(str string) string {
return filepath.Join(r.basedir, "vendor", str)
}
// resolveImports takes a list of existing packages and resolves their imports.
//
// It returns a list of all of the packages that it can determine are required
// for the given code to function.
//
// The expectation is that each item in the queue is an absolute path to a
// vendored package. This attempts to read that package, and then find
// its referenced packages. Those packages are then added to the list
// to be scanned next.
//
// The resolver's handler is used in the cases where a package cannot be
// located.
//
// testDeps specifies if the test dependencies should be resolved and addTest
// specifies if the dependencies should be added to the Config.DevImports. This
// is important because we may resolve normal dependencies of test deps and add
// them to the DevImports list.
func (r *Resolver) resolveImports(queue *list.List, testDeps, addTest bool) ([]string, error) {
msg.Debug("Resolving import path")
// When test deps passed in but not resolving return empty.
if (testDeps || addTest) && !r.ResolveTest {
return []string{}, nil
}
for e := queue.Front(); e != nil; e = e.Next() {
vdep := e.Value.(string)
dep := r.Stripv(vdep)
// Check if marked in the Q and then explicitly mark it. We want to know
// if it had previously been marked and ensure it for the future.
_, foundQ := r.alreadyQ[dep]
r.alreadyQ[dep] = true
// If we've already encountered an error processing this dependency
// skip it.
_, foundErr := r.hadError[dep]
if foundErr {
continue
}
// Skip ignored packages
if r.Config.HasIgnore(dep) {
msg.Debug("Ignoring: %s", dep)
continue
}
r.VersionHandler.Process(dep)
// Here, we want to import the package and see what imports it has.
msg.Debug("Trying to open %s", vdep)
var imps []string
pkg, err := r.BuildContext.ImportDir(r.Handler.PkgPath(dep), 0)
if err != nil && strings.HasPrefix(err.Error(), "found packages ") {
// If we got here it's because a package and multiple packages
// declared. This is often because of an example with a package
// or main but +build ignore as a build tag. In that case we
// try to brute force the packages with a slower scan.
msg.Debug("Using Iterative Scanning for %s", dep)
if testDeps {
_, imps, err = IterativeScan(r.Handler.PkgPath(dep))
} else {
imps, _, err = IterativeScan(r.Handler.PkgPath(dep))
}
if err != nil {
msg.Err("Iterative scanning error %s: %s", dep, err)
continue
}
} else if err != nil {
errStr := err.Error()
msg.Debug("ImportDir error on %s: %s", r.Handler.PkgPath(dep), err)
if strings.HasPrefix(errStr, "no buildable Go source") {
msg.Debug("No subpackages declared. Skipping %s.", dep)
continue
} else if os.IsNotExist(err) && !foundErr && !foundQ {
// If the location doesn't exist, there hasn't already been an
// error, it's not already been in the Q then try to fetch it.
// When there's an error or it's already in the Q (it should be
// fetched if it's marked in r.alreadyQ) we skip to make sure
// not to get stuck in a recursion.
// If the location doesn't exist try to fetch it.
if ok, err2 := r.Handler.NotFound(dep, addTest); ok {
r.alreadyQ[dep] = true
// By adding to the queue it will get reprocessed now that
// it exists.
queue.PushBack(r.vpath(dep))
r.VersionHandler.SetVersion(dep, addTest)
} else if err2 != nil {
r.hadError[dep] = true
msg.Err("Error looking for %s: %s", dep, err2)
} else {
r.hadError[dep] = true
// TODO (mpb): Should we toss this into a Handler to
// see if this is on GOPATH and copy it?
msg.Info("Not found in vendor/: %s (1)", dep)
}
} else if strings.Contains(errStr, "no such file or directory") {
r.hadError[dep] = true
msg.Err("Error scanning %s: %s", dep, err)
msg.Err("This error means the referenced package was not found.")
msg.Err("Missing file or directory errors usually occur when multiple packages")
msg.Err("share a common dependency and the first reference encountered by the scanner")
msg.Err("sets the version to one that does not contain a subpackage needed required")
msg.Err("by another package that uses the shared dependency. Try setting a")
msg.Err("version in your glide.yaml that works for all packages that share this")
msg.Err("dependency.")
} else {
r.hadError[dep] = true
msg.Err("Error scanning %s: %s", dep, err)
}
continue
} else {
if testDeps {
imps = dedupeStrings(pkg.TestImports, pkg.XTestImports)
} else {
imps = pkg.Imports
}
}
// Range over all of the identified imports and see which ones we
// can locate.
for _, imp := range imps {
if r.Config.HasIgnore(imp) {
msg.Debug("Ignoring: %s", imp)
continue
}
pi := r.FindPkg(imp)
if pi.Loc != LocCgo && pi.Loc != LocGoroot && pi.Loc != LocAppengine {
msg.Debug("Package %s imports %s", dep, imp)
}
switch pi.Loc {
case LocVendor:
msg.Debug("In vendor: %s", imp)
if _, ok := r.alreadyQ[imp]; !ok {
msg.Debug("Marking %s to be scanned.", imp)
r.alreadyQ[dep] = true
queue.PushBack(r.vpath(imp))
if err := r.Handler.InVendor(imp, addTest); err == nil {
r.VersionHandler.SetVersion(imp, addTest)
} else {
msg.Warn("Error updating %s: %s", imp, err)
}
}
case LocUnknown:
msg.Debug("Missing %s. Trying to resolve.", imp)
if ok, err := r.Handler.NotFound(imp, addTest); ok {
r.alreadyQ[dep] = true
queue.PushBack(r.vpath(imp))
r.VersionHandler.SetVersion(imp, addTest)
} else if err != nil {
r.hadError[dep] = true
msg.Err("Error looking for %s: %s", imp, err)
} else {
r.hadError[dep] = true
msg.Err("Not found: %s (2)", imp)
}
case LocGopath:
msg.Debug("Found on GOPATH, not vendor: %s", imp)
if _, ok := r.alreadyQ[imp]; !ok {
// Only scan it if it gets moved into vendor/
if ok, _ := r.Handler.OnGopath(imp, addTest); ok {
r.alreadyQ[dep] = true
queue.PushBack(r.vpath(imp))
r.VersionHandler.SetVersion(imp, addTest)
}
}
}
}
}
if len(r.hadError) > 0 {
// Errors occured so we return.
return []string{}, errors.New("Error resolving imports")
}
// FIXME: From here to the end is a straight copy of the resolveList() func.
res := make([]string, 0, queue.Len())
// In addition to generating a list
for e := queue.Front(); e != nil; e = e.Next() {
t := r.Stripv(e.Value.(string))
root, sp := util.NormalizeName(t)
// Skip ignored packages
if r.Config.HasIgnore(e.Value.(string)) {
msg.Debug("Ignoring: %s", e.Value.(string))
continue
}
// TODO(mattfarina): Need to eventually support devImport
existing := r.Config.Imports.Get(root)
if existing == nil && addTest {
existing = r.Config.DevImports.Get(root)
}
if existing != nil {
if sp != "" && !existing.HasSubpackage(sp) {
existing.Subpackages = append(existing.Subpackages, sp)
}
} else {
newDep := &cfg.Dependency{
Name: root,
}
if sp != "" {
newDep.Subpackages = []string{sp}
}
if addTest {
r.Config.DevImports = append(r.Config.DevImports, newDep)
} else {
r.Config.Imports = append(r.Config.Imports, newDep)
}
}
res = append(res, t)
}
return res, nil
}
// resolveList takes a list and resolves it.
//
// This walks the entire file tree for the given dependencies, not just the
// parts that are imported directly. Using this will discover dependencies
// regardless of OS, and arch.
func (r *Resolver) resolveList(queue *list.List, testDeps, addTest bool) ([]string, error) {
// When test deps passed in but not resolving return empty.
if testDeps && !r.ResolveTest {
return []string{}, nil
}
var failedDep string
for e := queue.Front(); e != nil; e = e.Next() {
dep := e.Value.(string)
t := strings.TrimPrefix(dep, r.VendorDir+string(os.PathSeparator))
if r.Config.HasIgnore(t) {
msg.Debug("Ignoring: %s", t)
continue
}
r.VersionHandler.Process(t)
//msg.Warn("#### %s ####", dep)
//msg.Info("Seen Count: %d", len(r.seen))
// Catch the outtermost dependency.
failedDep = dep
err := filepath.Walk(dep, func(path string, fi os.FileInfo, err error) error {
if err != nil && err != filepath.SkipDir {
return err
}
// Skip files.
if !fi.IsDir() {
return nil
}
// Skip dirs that are not source.
if !srcDir(fi) {
//msg.Debug("Skip resource %s", fi.Name())
return filepath.SkipDir
}
// Anything that comes through here has already been through
// the queue.
r.alreadyQ[path] = true
e := r.queueUnseen(path, queue, testDeps, addTest)
if err != nil {
failedDep = path
//msg.Err("Failed to fetch dependency %s: %s", path, err)
}
return e
})
if err != nil && err != filepath.SkipDir {
msg.Err("Dependency %s failed to resolve: %s.", failedDep, err)
return []string{}, err
}
}
res := make([]string, 0, queue.Len())
// In addition to generating a list
for e := queue.Front(); e != nil; e = e.Next() {
t := strings.TrimPrefix(e.Value.(string), r.VendorDir+string(os.PathSeparator))
root, sp := util.NormalizeName(t)
existing := r.Config.Imports.Get(root)
if existing == nil && addTest {
existing = r.Config.DevImports.Get(root)
}
if existing != nil {
if sp != "" && !existing.HasSubpackage(sp) {
existing.Subpackages = append(existing.Subpackages, sp)
}
} else {
newDep := &cfg.Dependency{
Name: root,
}
if sp != "" {
newDep.Subpackages = []string{sp}
}
if addTest {
r.Config.DevImports = append(r.Config.DevImports, newDep)
} else {
r.Config.Imports = append(r.Config.Imports, newDep)
}
}
res = append(res, e.Value.(string))
}
return res, nil
}
// queueUnseenImports scans a package's imports and adds any new ones to the
// processing queue.
func (r *Resolver) queueUnseen(pkg string, queue *list.List, testDeps, addTest bool) error {
// A pkg is marked "seen" as soon as we have inspected it the first time.
// Seen means that we have added all of its imports to the list.
// Already queued indicates that we've either already put it into the queue
// or intentionally not put it in the queue for fatal reasons (e.g. no
// buildable source).
deps, err := r.imports(pkg, testDeps, addTest)
if err != nil && !strings.HasPrefix(err.Error(), "no buildable Go source") {
msg.Err("Could not find %s: %s", pkg, err)
return err
// NOTE: If we uncomment this, we get lots of "no buildable Go source" errors,
// which don't ever seem to be helpful. They don't actually indicate an error
// condition, and it's perfectly okay to run into that condition.
//} else if err != nil {
// msg.Warn(err.Error())
}
for _, d := range deps {
if _, ok := r.alreadyQ[d]; !ok {
r.alreadyQ[d] = true
queue.PushBack(d)
}
}
return nil
}
// imports gets all of the imports for a given package.
//
// If the package is in GOROOT, this will return an empty list (but not
// an error).
// If it cannot resolve the pkg, it will return an error.
func (r *Resolver) imports(pkg string, testDeps, addTest bool) ([]string, error) {
if r.Config.HasIgnore(pkg) {
msg.Debug("Ignoring %s", pkg)
return []string{}, nil
}
// If this pkg is marked seen, we don't scan it again.
if _, ok := r.seen[pkg]; ok {
msg.Debug("Already saw %s", pkg)
return []string{}, nil
}
// FIXME: On error this should try to NotFound to the dependency, and then import
// it again.
var imps []string
p, err := r.BuildContext.ImportDir(r.Handler.PkgPath(pkg), 0)
if err != nil && strings.HasPrefix(err.Error(), "found packages ") {
// If we got here it's because a package and multiple packages
// declared. This is often because of an example with a package
// or main but +build ignore as a build tag. In that case we
// try to brute force the packages with a slower scan.
if testDeps {
_, imps, err = IterativeScan(r.Handler.PkgPath(pkg))
} else {
imps, _, err = IterativeScan(r.Handler.PkgPath(pkg))
}
if err != nil {
return []string{}, err
}
} else if err != nil {
return []string{}, err
} else {
if testDeps {
imps = dedupeStrings(p.TestImports, p.XTestImports)
} else {
imps = p.Imports
}
}
// It is okay to scan a package more than once. In some cases, this is
// desirable because the package can change between scans (e.g. as a result
// of a failed scan resolving the situation).
msg.Debug("=> Scanning %s (%s)", p.ImportPath, pkg)
r.seen[pkg] = true
// Optimization: If it's in GOROOT, it has no imports worth scanning.
if p.Goroot {
return []string{}, nil
}
// We are only looking for dependencies in vendor. No root, cgo, etc.
buf := []string{}
for _, imp := range imps {
if r.Config.HasIgnore(imp) {
msg.Debug("Ignoring %s", imp)
continue
}
info := r.FindPkg(imp)
switch info.Loc {
case LocUnknown:
// Do we resolve here?
found, err := r.Handler.NotFound(imp, addTest)
if err != nil {
msg.Err("Failed to fetch %s: %s", imp, err)
}
if found {
buf = append(buf, filepath.Join(r.VendorDir, filepath.FromSlash(imp)))
r.VersionHandler.SetVersion(imp, addTest)
continue
}
r.seen[info.Path] = true
case LocVendor:
//msg.Debug("Vendored: %s", imp)
buf = append(buf, info.Path)
if err := r.Handler.InVendor(imp, addTest); err == nil {
r.VersionHandler.SetVersion(imp, addTest)
} else {
msg.Warn("Error updating %s: %s", imp, err)
}
case LocGopath:
found, err := r.Handler.OnGopath(imp, addTest)
if err != nil {
msg.Err("Failed to fetch %s: %s", imp, err)
}
// If the Handler marks this as found, we drop it into the buffer
// for subsequent processing. Otherwise, we assume that we're
// in a less-than-perfect, but functional, situation.
if found {
buf = append(buf, filepath.Join(r.VendorDir, filepath.FromSlash(imp)))
r.VersionHandler.SetVersion(imp, addTest)
continue
}
msg.Warn("Package %s is on GOPATH, but not vendored. Ignoring.", imp)
r.seen[info.Path] = true
default:
// Local packages are an odd case. CGO cannot be scanned.
msg.Debug("===> Skipping %s", imp)
}
}
return buf, nil
}
// sliceToQueue is a special-purpose function for unwrapping a slice of
// dependencies into a queue of fully qualified paths.
func sliceToQueue(deps []*cfg.Dependency, basepath string) *list.List {
l := list.New()
for _, e := range deps {
if len(e.Subpackages) > 0 {
for _, v := range e.Subpackages {
ip := e.Name
if v != "." && v != "" {
ip = ip + "/" + v
}
msg.Debug("Adding local Import %s to queue", ip)
l.PushBack(filepath.Join(basepath, filepath.FromSlash(ip)))
}
} else {
msg.Debug("Adding local Import %s to queue", e.Name)
l.PushBack(filepath.Join(basepath, filepath.FromSlash(e.Name)))
}
}
return l
}
// PkgLoc describes the location of the package.
type PkgLoc uint8
const (
// LocUnknown indicates the package location is unknown (probably not present)
LocUnknown PkgLoc = iota
// LocLocal inidcates that the package is in a local dir, not GOPATH or GOROOT.
LocLocal
// LocVendor indicates that the package is in a vendor/ dir
LocVendor
// LocGopath inidcates that the package is in GOPATH
LocGopath
// LocGoroot indicates that the package is in GOROOT
LocGoroot
// LocCgo indicates that the package is a a CGO package
LocCgo
// LocAppengine indicates the package is part of the appengine SDK. It's a
// special build mode. https://blog.golang.org/the-app-engine-sdk-and-workspaces-gopath
// Why does a Google product get a special case build mode with a local
// package?
LocAppengine
// LocRelative indicates the package is a relative directory
LocRelative
)
// PkgInfo represents metadata about a package found by the resolver.
type PkgInfo struct {
Name, Path string
Vendored bool
Loc PkgLoc
}
// FindPkg takes a package name and attempts to find it on the filesystem
//
// The resulting PkgInfo will indicate where it was found.
func (r *Resolver) FindPkg(name string) *PkgInfo {
// We cachae results for FindPkg to reduce the number of filesystem ops
// that we have to do. This is a little risky because certain directories,
// like GOPATH, can be modified while we're running an operation, and
// render the cache inaccurate.
//
// Unfound items (LocUnknown) are never cached because we assume that as
// part of the response, the Resolver may fetch that dependency.
if i, ok := r.findCache[name]; ok {
//msg.Info("Cache hit on %s", name)
return i
}
// 502 individual packages scanned.
// No cache:
// glide -y etcd.yaml list 0.27s user 0.19s system 85% cpu 0.534 total
// With cache:
// glide -y etcd.yaml list 0.22s user 0.15s system 85% cpu 0.438 total
var p string
info := &PkgInfo{
Name: name,
}
if strings.HasPrefix(name, "./") || strings.HasPrefix(name, "../") {
info.Loc = LocRelative
r.findCache[name] = info
return info
}
// Check _only_ if this dep is in the current vendor directory.
p = filepath.Join(r.VendorDir, filepath.FromSlash(name))
if pkgExists(p) {
info.Path = p
info.Loc = LocVendor
info.Vendored = true
r.findCache[name] = info
return info
}
// TODO: Do we need this if we always flatten?
// Recurse backward to scan other vendor/ directories
//for wd := cwd; wd != "/"; wd = filepath.Dir(wd) {
//p = filepath.Join(wd, "vendor", filepath.FromSlash(name))
//if fi, err = os.Stat(p); err == nil && (fi.IsDir() || isLink(fi)) {
//info.Path = p
//info.PType = ptypeVendor
//info.Vendored = true
//return info
//}
//}
// Check $GOPATH
for _, rr := range filepath.SplitList(r.BuildContext.GOPATH) {
p = filepath.Join(rr, "src", filepath.FromSlash(name))
if pkgExists(p) {
info.Path = p
info.Loc = LocGopath
r.findCache[name] = info
return info
}
}
// Check $GOROOT
for _, rr := range filepath.SplitList(r.BuildContext.GOROOT) {
p = filepath.Join(rr, "src", filepath.FromSlash(name))
if pkgExists(p) {
info.Path = p
info.Loc = LocGoroot
r.findCache[name] = info
return info
}
}
// If this is "C", we're dealing with cgo
if name == "C" {
info.Loc = LocCgo
r.findCache[name] = info
} else if name == "appengine" || name == "appengine_internal" ||
strings.HasPrefix(name, "appengine/") ||
strings.HasPrefix(name, "appengine_internal/") {
// Appengine is a special case when it comes to Go builds. It is a local
// looking package only available within appengine. It's a special case
// where Google products are playing with each other.
// https://blog.golang.org/the-app-engine-sdk-and-workspaces-gopath
info.Loc = LocAppengine
r.findCache[name] = info
} else if name == "context" || name == "net/http/httptrace" {
// context and net/http/httptrace are packages being added to
// the Go 1.7 standard library. Some packages, such as golang.org/x/net
// are importing it with build flags in files for go1.7. Need to detect
// this and handle it.
info.Loc = LocGoroot
r.findCache[name] = info
}
return info
}
func pkgExists(path string) bool {
fi, err := os.Stat(path)
return err == nil && (fi.IsDir() || isLink(fi))
}
// isLink returns true if the given FileInfo is a symbolic link.
func isLink(fi os.FileInfo) bool {
return fi.Mode()&os.ModeSymlink == os.ModeSymlink
}
// IsSrcDir returns true if this is a directory that could have source code,
// false otherwise.
//
// Directories with _ or . prefixes are skipped, as are testdata and vendor.
func IsSrcDir(fi os.FileInfo) bool {
return srcDir(fi)
}
func srcDir(fi os.FileInfo) bool {
if !fi.IsDir() {
return false
}
// Ignore _foo and .foo
if strings.HasPrefix(fi.Name(), "_") || strings.HasPrefix(fi.Name(), ".") {
return false
}
// Ignore testdata. For now, ignore vendor.
if fi.Name() == "testdata" || fi.Name() == "vendor" {
return false
}
return true
}
// checkForBasedirSymlink checks to see if the given basedir is actually a
// symlink. In the case that it is a symlink, the symlink is read and returned.
// If the basedir is not a symlink, the provided basedir argument is simply
// returned back to the caller.
func checkForBasedirSymlink(basedir string) (string, error) {
fi, err := os.Lstat(basedir)
if err != nil {
return "", err
}
if fi.Mode()&os.ModeSymlink != 0 {
return os.Readlink(basedir)
}
return basedir, nil
}
// helper func to merge, dedupe, and sort strings
func dedupeStrings(s1, s2 []string) (r []string) {
dedupe := make(map[string]bool)
if len(s1) > 0 && len(s2) > 0 {
for _, i := range s1 {
dedupe[i] = true
}
for _, i := range s2 {
dedupe[i] = true
}
for i := range dedupe {
r = append(r, i)
}
// And then re-sort them
sort.Strings(r)
} else if len(s1) > 0 {
r = s1
} else if len(s2) > 0 {
r = s2
}
return
}