Merging in the new dependency resolver
diff --git a/cmd/flatten.go b/cmd/flatten.go index eac3773..cfff217 100644 --- a/cmd/flatten.go +++ b/cmd/flatten.go
@@ -56,7 +56,10 @@ f := &flattening{conf, vend, vend, deps, packages} - err := recFlatten(f, force, home, cache, cacheGopath, skipGopath) + // The assumption here is that once something has been scanned once in a + // run, there is no need to scan it again. + scanned := map[string]bool{} + err := recFlatten(f, force, home, cache, cacheGopath, skipGopath, scanned) if err != nil { return conf, err } @@ -106,7 +109,7 @@ var updateCache = map[string]bool{} // refFlatten recursively flattens the vendor tree. -func recFlatten(f *flattening, force bool, home string, cache, cacheGopath, skipGopath bool) error { +func recFlatten(f *flattening, force bool, home string, cache, cacheGopath, skipGopath bool, scanned map[string]bool) error { Debug("---> Inspecting %s for changes (%d packages).\n", f.curr, len(f.scan)) for _, imp := range f.scan { Debug("----> Scanning %s", imp) @@ -120,7 +123,7 @@ mod = m } else if m, ok = mergeGb(base, imp, f.deps, f.top); ok { mod = m - } else if m, ok = mergeGuess(base, imp, f.deps, f.top); ok { + } else if m, ok = mergeGuess(base, imp, f.deps, f.top, scanned); ok { mod = m } @@ -133,7 +136,7 @@ curr: base, deps: f.deps, scan: mod} - recFlatten(f2, force, home, cache, cacheGopath, skipGopath) + recFlatten(f2, force, home, cache, cacheGopath, skipGopath, scanned) } } @@ -261,7 +264,7 @@ // This always returns true because it always handles the job of searching // for dependencies. So generally it should be the last merge strategy // that you try. -func mergeGuess(dir, pkg string, deps map[string]*cfg.Dependency, vend string) ([]string, bool) { +func mergeGuess(dir, pkg string, deps map[string]*cfg.Dependency, vend string, scanned map[string]bool) ([]string, bool) { Info("Scanning %s for dependencies.", pkg) buildContext, err := GetBuildContext() if err != nil { @@ -275,18 +278,27 @@ Warn("Directory is missing: %s", dir) return res, true } + d := walkDeps(buildContext, dir, pkg) - for _, name := range d { - name, _ := NormalizeName(name) - if _, ok := deps[name]; ok { - Debug("====> Seen %s already. Skipping", name) + for _, oname := range d { + if _, ok := scanned[oname]; ok { + //Info("===> Scanned %s already. Skipping", name) continue } + Info("=> Scanning %s", oname) + name, _ := NormalizeName(oname) + //if _, ok := deps[name]; ok { + //scanned[oname] = true + //Debug("====> Seen %s already. Skipping", name) + //continue + //} + + repo := util.GetRootFromPackage(name) found := findPkg(buildContext, name, dir) switch found.PType { case ptypeUnknown: + Info("==> Unknown %s (%s)", name, oname) Debug("✨☆ Undownloaded dependency: %s", name) - repo := util.GetRootFromPackage(name) nd := &cfg.Dependency{ Name: name, Repository: "https://" + repo, @@ -294,6 +306,8 @@ deps[name] = nd res = append(res, name) case ptypeGoroot, ptypeCgo: + scanned[oname] = true + // Why do we break rather than continue? break default: // We're looking for dependencies that might exist in $GOPATH @@ -304,6 +318,7 @@ deps[name] = nd res = append(res, name) } + scanned[oname] = true } }
diff --git a/cmd/tree.go b/cmd/tree.go index 61ba869..60181ec 100644 --- a/cmd/tree.go +++ b/cmd/tree.go
@@ -9,6 +9,8 @@ "strings" "github.com/Masterminds/cookoo" + "github.com/Masterminds/glide/dependency" + "github.com/Masterminds/glide/msg" ) // Tree prints a tree representing dependencies. @@ -40,40 +42,55 @@ // ListDeps lists all of the dependencies of the current project. // // Params: +// - dir (string): basedir +// - deep (bool): whether to do a deep scan or a shallow scan // // Returns: // func ListDeps(c cookoo.Context, p *cookoo.Params) (interface{}, cookoo.Interrupt) { - buildContext, err := GetBuildContext() - if err != nil { - return nil, err - } basedir := p.Get("dir", ".").(string) - myName := guessPackageName(buildContext, basedir) + deep := p.Get("deep", true).(bool) - basedir, err = filepath.Abs(basedir) + basedir, err := filepath.Abs(basedir) if err != nil { return nil, err } - direct := map[string]*pinfo{} - d := walkDeps(buildContext, basedir, myName) - for _, i := range d { - listDeps(buildContext, direct, i, basedir) + r, err := dependency.NewResolver(basedir) + if err != nil { + return nil, err } + h := &dependency.DefaultMissingPackageHandler{Missing: []string{}, Gopath: []string{}} + r.Handler = h - sortable := make([]string, len(direct)) - i := 0 - for k := range direct { - sortable[i] = k - i++ + sortable, err := r.ResolveLocal(deep) + if err != nil { + return nil, err } sort.Strings(sortable) + fmt.Println("INSTALLED packages:") for _, k := range sortable { - t := direct[k].PType - fmt.Printf("%s (Location: %s)\n", k, ptypeString(t)) + v, err := filepath.Rel(basedir, k) + if err != nil { + msg.Warn("Failed to Rel path: %s", err) + v = k + } + fmt.Printf("\t%s\n", v) + } + + if len(h.Missing) > 0 { + fmt.Println("\nMISSING packages:") + for _, pkg := range h.Missing { + fmt.Printf("\t%s\n", pkg) + } + } + if len(h.Gopath) > 0 { + fmt.Println("\nGOPATH packages:") + for _, pkg := range h.Gopath { + fmt.Printf("\t%s\n", pkg) + } } return nil, nil @@ -231,7 +248,11 @@ pkg, err := b.ImportDir(path, 0) if err != nil { - return err + if !strings.HasPrefix(err.Error(), "no buildable Go source") { + Warn("Error: %s (%s)", err, path) + // Not sure if we should return here. + //return err + } } if pkg.Goroot {
diff --git a/cmd/update_references.go b/cmd/update_references.go index 1cf47ec..92fe235 100644 --- a/cmd/update_references.go +++ b/cmd/update_references.go
@@ -70,6 +70,7 @@ func discoverDependencyTree(f *flattening) error { Debug("---> Inspecting %s for dependencies (%d packages).\n", f.curr, len(f.scan)) + scanned := map[string]bool{} for _, imp := range f.scan { Debug("----> Scanning %s", imp) base := path.Join(f.top, imp) @@ -82,7 +83,7 @@ mod = m } else if m, ok = mergeGb(base, imp, f.deps, f.top); ok { mod = m - } else if m, ok = mergeGuess(base, imp, f.deps, f.top); ok { + } else if m, ok = mergeGuess(base, imp, f.deps, f.top, scanned); ok { mod = m }
diff --git a/dependency/resolver.go b/dependency/resolver.go new file mode 100644 index 0000000..943c3aa --- /dev/null +++ b/dependency/resolver.go
@@ -0,0 +1,530 @@ +package dependency + +import ( + "container/list" + "go/build" + "os" + "path/filepath" + "strings" + + "github.com/Masterminds/glide/cfg" + "github.com/Masterminds/glide/msg" +) + +// 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) (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) (bool, error) +} + +// 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 +} + +// 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) (bool, error) { + msg.Warn("Package %s is not installed", pkg) + d.Missing = append(d.Missing, pkg) + return false, nil +} + +func (d *DefaultMissingPackageHandler) OnGopath(pkg string) (bool, error) { + msg.Warn("Package %s is only on GOPATH.", pkg) + d.Gopath = append(d.Gopath, pkg) + return false, 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 + basedir string + VendorDir string + BuildContext build.Context + seen map[string]bool + + // Items already in the queue. + alreadyQ 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 + } + vdir := filepath.Join(basedir, "vendor") + + r := &Resolver{ + Handler: &DefaultMissingPackageHandler{Missing: []string{}, Gopath: []string{}}, + basedir: basedir, + VendorDir: vdir, + BuildContext: build.Default, + seen: map[string]bool{}, + alreadyQ: map[string]bool{}, + findCache: map[string]*PkgInfo{}, + } + + // 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, pkg) + //msg.Debug("Scanning %s", target) + l := list.New() + l.PushBack(target) + return r.resolveList(l) +} + +// 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, error) { + // We build a list of local source to walk, then send this list + // to resolveList. + l := list.New() + alreadySeen := 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 + } + 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. + p, err := r.BuildContext.ImportDir(path, 0) + if err != nil { + if strings.HasPrefix(err.Error(), "no buildable Go source") { + return nil + } + return err + } + + // We are only looking for dependencies in vendor. No root, cgo, etc. + for _, imp := range p.Imports { + if alreadySeen[imp] { + continue + } + alreadySeen[imp] = true + info := r.FindPkg(imp) + switch info.Loc { + case LocUnknown, LocVendor: + l.PushBack(filepath.Join(r.VendorDir, imp)) // Do we need a path on this? + case LocGopath: + if !strings.HasPrefix(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, imp)) + } + } + } + + return nil + }) + + if err != nil { + msg.Error("Failed to build an initial list of packages to scan: %s", err) + return []string{}, err + } + + if deep { + return r.resolveList(l) + } + + // 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)) + } + return res, 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) ([]string, error) { + queue := sliceToQueue(deps, r.VendorDir) + return r.resolveList(queue) +} + +// resolveList takes a list and resolves it. +func (r *Resolver) resolveList(queue *list.List) ([]string, error) { + + var failedDep string + for e := queue.Front(); e != nil; e = e.Next() { + dep := e.Value.(string) + //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) + if err != nil { + failedDep = path + //msg.Error("Failed to fetch dependency %s: %s", path, err) + } + return e + }) + if err != nil && err != filepath.SkipDir { + msg.Error("Dependency %s failed to resolve: %s.", failedDep, err) + return []string{}, err + } + } + + res := make([]string, 0, queue.Len()) + for e := queue.Front(); e != nil; e = e.Next() { + 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) 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) + if err != nil && !strings.HasPrefix(err.Error(), "no buildable Go source") { + msg.Error("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) ([]string, error) { + + // 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. + p, err := r.BuildContext.ImportDir(pkg, 0) + if err != nil { + return []string{}, err + } + + // 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 p.Imports { + info := r.FindPkg(imp) + switch info.Loc { + case LocUnknown: + // Do we resolve here? + found, err := r.Handler.NotFound(imp) + if err != nil { + msg.Error("Failed to fetch %s: %s", imp, err) + } + if found { + buf = append(buf, filepath.Join(r.VendorDir, imp)) + continue + } + r.seen[info.Path] = true + case LocVendor: + //msg.Debug("Vendored: %s", imp) + buf = append(buf, info.Path) + case LocGopath: + found, err := r.Handler.OnGopath(imp) + if err != nil { + msg.Error("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, imp)) + 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 { + l.PushBack(filepath.Join(basepath, 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 +) + +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 (LocUnkown) 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, + } + + // Check _only_ if this dep is in the current vendor directory. + p = filepath.Join(r.VendorDir, 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", 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 strings.Split(r.BuildContext.GOPATH, ":") { + p = filepath.Join(rr, "src", name) + if pkgExists(p) { + info.Path = p + info.Loc = LocGopath + r.findCache[name] = info + return info + } + } + + // Check $GOROOT + for _, rr := range strings.Split(r.BuildContext.GOROOT, ":") { + p = filepath.Join(rr, "src", name) + if pkgExists(p) { + info.Path = p + info.Loc = LocGoroot + r.findCache[name] = info + return info + } + } + + // Finally, if this is "C", we're dealing with cgo + if name == "C" { + info.Loc = LocCgo + 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 +} + +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 +}
diff --git a/dependency/resolver_test.go b/dependency/resolver_test.go new file mode 100644 index 0000000..46268f2 --- /dev/null +++ b/dependency/resolver_test.go
@@ -0,0 +1,99 @@ +package dependency + +import ( + "os" + "path/filepath" + "strings" + "testing" + + "github.com/Masterminds/glide/cfg" +) + +func TestResolveLocalShallow(t *testing.T) { + r, err := NewResolver("../") + if err != nil { + t.Fatal(err) + } + + l, err := r.ResolveLocal(false) + if err != nil { + t.Fatalf("Failed to resolve: %s", err) + } + + expect := []string{ + "github.com/Masterminds/cookoo", + "github.com/Masterminds/semver", + "github.com/Masterminds/vcs", + "gopkg.in/yaml.v2", + "github.com/codegangsta/cli", + } + + for _, p := range expect { + found := false + for _, li := range l { + if strings.HasSuffix(li, p) { + found = true + break + } + } + if !found { + t.Errorf("Could not find %s in resolved list.", p) + } + } +} + +func TestResolveLocalDeep(t *testing.T) { + r, err := NewResolver("../") + if err != nil { + t.Fatal(err) + } + + l, err := r.ResolveLocal(true) + if err != nil { + t.Fatalf("Failed to resolve: %s", err) + } + + if len(l) < 8 { + t.Errorf("Expected at least 8 deps, got %d: %s", len(l)) + } +} + +func TestResolve(t *testing.T) { + r, err := NewResolver("../") + if err != nil { + t.Fatal(err) + } + + base := filepath.Join(os.Getenv("GOPATH"), "src/github.com/Masterminds/glide/vendor") + l, err := r.Resolve("github.com/codegangsta/cli", base) + if err != nil { + t.Fatalf("Failed to resolve: %s", err) + } + + if len(l) != 1 { + t.Errorf("Expected 1 dep, got %d: %s", len(l), l[0]) + } +} + +func TestResolveAll(t *testing.T) { + // These are build dependencies of Glide, so we know they are here. + deps := []*cfg.Dependency{ + &cfg.Dependency{Name: "github.com/codegangsta/cli"}, + &cfg.Dependency{Name: "github.com/Masterminds/cookoo"}, + &cfg.Dependency{Name: "github.com/Masterminds/semver"}, + &cfg.Dependency{Name: "gopkg.in/yaml.v2"}, + } + + r, err := NewResolver("../") + if err != nil { + t.Fatalf("No new resolver: %s", err) + } + l, err := r.ResolveAll(deps) + if err != nil { + t.Fatalf("Failed to resolve: %s", err) + } + + if len(l) < len(deps) { + t.Errorf("Expected at least %d deps, got %d", len(deps), len(l)) + } +}
diff --git a/msg/msg.go b/msg/msg.go new file mode 100644 index 0000000..a357a39 --- /dev/null +++ b/msg/msg.go
@@ -0,0 +1,92 @@ +// +build !windows + +package msg + +import ( + "fmt" + "strings" +) + +// These contanstants map to color codes for shell scripts making them +// human readable. +const ( + Blue = "0;34" + Red = "0;31" + Green = "0;32" + Yellow = "0;33" + Cyan = "0;36" + Pink = "1;35" +) + +// Color returns a string in a certain color. The first argument is a string +// containing the color code or a constant from the table above mapped to a code. +// +// The following will print the string "Foo" in yellow: +// fmt.Print(Color(Yellow, "Foo")) +func Color(code, msg string) string { + if NoColor { + return msg + } + return fmt.Sprintf("\033[%sm%s\033[m", code, msg) +} + +// Info logs information +func Info(msg string, args ...interface{}) { + if Quiet { + return + } + fmt.Fprint(Stderr, Color(Green, "[INFO] ")) + Msg(msg, args...) +} + +// Debug logs debug information +func Debug(msg string, args ...interface{}) { + if Quiet || !IsDebugging { + return + } + fmt.Fprint(Stderr, "[DEBUG] ") + Msg(msg, args...) +} + +// Warn logs a warning +func Warn(msg string, args ...interface{}) { + fmt.Fprint(Stderr, Color(Yellow, "[WARN] ")) + ErrMsg(msg, args...) +} + +// Error logs and error. +func Error(msg string, args ...interface{}) { + fmt.Fprint(Stderr, Color(Red, "[ERROR] ")) + ErrMsg(msg, args...) +} + +// ErrMsg sends a message to Stderr +func ErrMsg(msg string, args ...interface{}) { + if len(args) == 0 { + fmt.Fprint(Stderr, msg) + } else { + fmt.Fprintf(Stderr, msg, args...) + } + + // Get rid of the annoying fact that messages need \n at the end, but do + // it in a backward compatible way. + if !strings.HasSuffix(msg, "\n") { + fmt.Fprintln(Stderr) + } +} + +// Msg prints a message with optional arguments, that can be printed, of +// varying types. +func Msg(msg string, args ...interface{}) { + if len(args) == 0 { + fmt.Fprint(Stderr, msg) + } else { + fmt.Fprintf(Stderr, msg, args...) + } + + // Get rid of the annoying fact that messages need \n at the end, but do + // it in a backward compatible way. + if !strings.HasSuffix(msg, "\n") { + fmt.Fprintln(Stderr) + } +}
diff --git a/msg/msg_windows.go b/msg/msg_windows.go new file mode 100644 index 0000000..7a715f1 --- /dev/null +++ b/msg/msg_windows.go
@@ -0,0 +1,63 @@ +// +build windows + +package msg + +import ( + "fmt" + "strings" +) + +// Info logs information +func Info(msg string, args ...interface{}) { + if Quiet { + return + } + fmt.Print("[INFO] ") + Msg(msg, args...) +} + +// Debug logs debug information +func Debug(msg string, args ...interface{}) { + if Quiet || !IsDebugging { + return + } + fmt.Print("[DEBUG] ") + Msg(msg, args...) +} + +// Warn logs a warning +func Warn(msg string, args ...interface{}) { + fmt.Fprint(Stderr, "[WARN] ") + ErrMsg(msg, args...) +} + +// Error logs and error. +func Error(msg string, args ...interface{}) { + fmt.Fprint(Stderr, "[ERROR] ") + ErrMsg(msg, args...) +} + +// ErrMsg sends a message to Stderr +func ErrMsg(msg string, args ...interface{}) { + if len(args) == 0 { + fmt.Fprint(Stderr, msg) + return + } + fmt.Fprintf(Stderr, msg, args...) +} + +// Msg prints a message with optional arguments, that can be printed, of +// varying types. +func Msg(msg string, args ...interface{}) { + if len(args) == 0 { + fmt.Print(msg) + return + } + fmt.Printf(msg, args...) + + // Get rid of the annoying fact that messages need \n at the end, but do + // it in a backward compatible way. + if !strings.HasSuffix(msg, "\n") { + fmt.Println("") + } +}
diff --git a/msg/vars.go b/msg/vars.go new file mode 100644 index 0000000..129b53c --- /dev/null +++ b/msg/vars.go
@@ -0,0 +1,31 @@ +package msg + +import ( + "fmt" + "os" +) + +// Quiet, if true, suppresses chatty levels, like Info. +var Quiet = false + +// IsDebugging, if true, shows verbose levels, like Debug. +var IsDebugging = false + +// NoColor, if true, will not use color in the output. +var NoColor = false + +// Stdout is the location where this prints output. +var Stdout = os.Stdout + +// Stderr is the location where this prints logs. +var Stderr = os.Stderr + +// Puts formats a message and then prints to Stdout. +// +// It does not prefix the message, does not color it, or otherwise decorate it. +// +// It does add a line feed. +func Puts(msg string, args ...interface{}) { + fmt.Fprintf(Stdout, msg, args...) + fmt.Fprintln(Stdout) +}