| package cmd |
| |
| import ( |
| "io/ioutil" |
| "os" |
| "path" |
| "strings" |
| |
| "github.com/Masterminds/cookoo" |
| "github.com/Masterminds/glide/util" |
| "github.com/Masterminds/glide/yaml" |
| "github.com/Masterminds/semver" |
| ) |
| |
| // Flatten recurses through all dependent packages and flattens to a top level. |
| // |
| // Flattening involves determining a tree's dependencies and flattening them |
| // into a single large list. |
| // |
| // Params: |
| // - packages ([]string): The packages to read. If this is empty, it reads all |
| // packages. |
| // - force (bool): force vcs updates. |
| // - conf (*yaml.Config): The configuration. |
| // |
| // Returns: |
| // |
| func Flatten(c cookoo.Context, p *cookoo.Params) (interface{}, cookoo.Interrupt) { |
| conf := p.Get("conf", &yaml.Config{}).(*yaml.Config) |
| skip := p.Get("skip", false).(bool) |
| home := p.Get("home", "").(string) |
| cache := p.Get("cache", false).(bool) |
| cacheGopath := p.Get("cacheGopath", false).(bool) |
| skipGopath := p.Get("skipGopath", false).(bool) |
| |
| if skip { |
| return conf, nil |
| } |
| packages := p.Get("packages", []string{}).([]string) |
| force := p.Get("force", true).(bool) |
| vend, _ := VendorPath(c) |
| |
| // If no packages are supplied, we do them all. |
| if len(packages) == 0 { |
| packages = make([]string, len(conf.Imports)) |
| for i, v := range conf.Imports { |
| packages[i] = v.Name |
| } |
| } |
| |
| // Build an initial dependency map. |
| deps := make(map[string]*yaml.Dependency, len(conf.Imports)) |
| for _, imp := range conf.Imports { |
| deps[imp.Name] = imp |
| } |
| |
| f := &flattening{conf, vend, vend, deps, packages} |
| |
| // 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 |
| } |
| err = conf.DeDupe() |
| if err != nil { |
| return conf, err |
| } |
| flattenSetRefs(f) |
| Info("Project relies on %d dependencies.", len(deps)) |
| |
| // A shallow copy should be all that's needed. |
| confcopy := conf.Clone() |
| exportFlattenedDeps(confcopy, deps) |
| |
| return confcopy, err |
| } |
| |
| func exportFlattenedDeps(conf *yaml.Config, in map[string]*yaml.Dependency) { |
| out := make([]*yaml.Dependency, len(in)) |
| i := 0 |
| for _, v := range in { |
| out[i] = v |
| i++ |
| } |
| conf.Imports = out |
| } |
| |
| type flattening struct { |
| conf *yaml.Config |
| // Top vendor path, e.g. project/vendor |
| top string |
| // Current path |
| curr string |
| // Built list of dependencies |
| deps map[string]*yaml.Dependency |
| // Dependencies that need to be scanned. |
| scan []string |
| } |
| |
| // Hack: Cache record of updates so we don't have to keep doing git pulls. |
| var updateCache = map[string]bool{} |
| |
| // refFlatten recursively flattens the vendor tree. |
| 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) |
| base := path.Join(f.top, imp) |
| mod := []string{} |
| if m, ok := mergeGlide(base, imp, f.deps, f.top); ok { |
| mod = m |
| } else if m, ok = mergeGodep(base, imp, f.deps, f.top); ok { |
| mod = m |
| } else if m, ok = mergeGPM(base, imp, f.deps, f.top); ok { |
| 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, scanned); ok { |
| mod = m |
| } |
| //mod, _ = mergeGuess(base, imp, f.deps, f.top, scanned) |
| //Info("Scanned: %v", scanned) |
| |
| if len(mod) > 0 { |
| Debug("----> Updating all dependencies for %q (%d)", imp, len(mod)) |
| flattenGlideUp(f, base, home, force, cache, cacheGopath, skipGopath) |
| f2 := &flattening{ |
| conf: f.conf, |
| top: f.top, |
| curr: base, |
| deps: f.deps, |
| scan: mod} |
| recFlatten(f2, force, home, cache, cacheGopath, skipGopath, scanned) |
| } |
| } |
| |
| return nil |
| } |
| |
| // flattenGlideUp does a glide update in the middle of a flatten operation. |
| // |
| // While this is expensive, it is also necessary to make sure we have the |
| // correct version of all dependencies. We might be able to simplify by |
| // marking packages dirty when they are added. |
| func flattenGlideUp(f *flattening, base, home string, force, cache, cacheGopath, skipGopath bool) error { |
| //vdir := path.Join(base, "vendor") |
| for _, imp := range f.deps { |
| // If the top package name in the glide.yaml file is present in the deps |
| // skip it because we already have it. |
| if imp.Name == f.conf.Name { |
| continue |
| } |
| wd := path.Join(f.top, imp.Name) |
| if VcsExists(imp, wd) { |
| if updateCache[imp.Name] { |
| Debug("----> Already updated %s", imp.Name) |
| continue |
| } |
| Debug("Updating project %s (%s)\n", imp.Name, wd) |
| if err := VcsUpdate(imp, f.top, home, force, cache, cacheGopath, skipGopath); err != nil { |
| // We can still go on just fine even if this fails. |
| Warn("Skipped update %s: %s\n", imp.Name, err) |
| continue |
| } |
| updateCache[imp.Name] = true |
| } else { |
| Debug("Importing %s to project %s\n", imp.Name, wd) |
| if err := VcsGet(imp, wd, home, cache, cacheGopath, skipGopath); err != nil { |
| Warn("Skipped getting %s: %v\n", imp.Name, err) |
| continue |
| } |
| } |
| |
| // If a revision has been set use it. |
| err := VcsVersion(imp, f.top) |
| if err != nil { |
| Warn("Problem setting version on %s: %s\n", imp.Name, err) |
| } |
| } |
| |
| return nil |
| } |
| |
| // Set the references for all packages after a flatten is completed. |
| func flattenSetRefs(f *flattening) { |
| Debug("Setting final version for %d dependencies.", len(f.deps)) |
| for _, imp := range f.deps { |
| if err := VcsVersion(imp, f.top); err != nil { |
| Warn("Problem setting version on %s: %s (flatten)\n", imp.Name, err) |
| } |
| } |
| } |
| |
| func mergeGlide(dir, name string, deps map[string]*yaml.Dependency, vend string) ([]string, bool) { |
| gp := path.Join(dir, "glide.yaml") |
| if _, err := os.Stat(gp); err != nil { |
| return []string{}, false |
| } |
| |
| yml, err := ioutil.ReadFile(gp) |
| if err != nil { |
| Warn("Found glide file %q, but can't read: %s", gp, err) |
| return []string{}, false |
| } |
| |
| conf, err := yaml.FromYaml(string(yml)) |
| if err != nil { |
| Warn("Found glide file %q, but can't use it: %s", gp, err) |
| return []string{}, false |
| } |
| |
| Info("Found glide.yaml in %s", gp) |
| |
| return mergeDeps(deps, conf.Imports, vend), true |
| } |
| |
| // listGodep appends Godeps entries to the deps. |
| // |
| // It returns true if any dependencies were found (even if not added because |
| // they are duplicates). |
| func mergeGodep(dir, name string, deps map[string]*yaml.Dependency, vend string) ([]string, bool) { |
| Debug("Looking in %s/Godeps/ for a Godeps.json file.\n", dir) |
| d, err := parseGodepGodeps(dir) |
| if err != nil { |
| Warn("Looking for Godeps: %s\n", err) |
| return []string{}, false |
| } else if len(d) == 0 { |
| return []string{}, false |
| } |
| |
| Info("Found Godeps.json file for %q", name) |
| return mergeDeps(deps, d, vend), true |
| } |
| |
| // listGb merges GB dependencies into the deps. |
| func mergeGb(dir, pkg string, deps map[string]*yaml.Dependency, vend string) ([]string, bool) { |
| Debug("Looking in %s/vendor/ for a manifest file.\n", dir) |
| d, err := parseGbManifest(dir) |
| if err != nil || len(d) == 0 { |
| return []string{}, false |
| } |
| Info("Found gb manifest file for %q", pkg) |
| return mergeDeps(deps, d, vend), true |
| } |
| |
| // mergeGPM merges GPM Godeps files into deps. |
| func mergeGPM(dir, pkg string, deps map[string]*yaml.Dependency, vend string) ([]string, bool) { |
| d, err := parseGPMGodeps(dir) |
| if err != nil || len(d) == 0 { |
| return []string{}, false |
| } |
| Info("Found GPM file for %q", pkg) |
| return mergeDeps(deps, d, vend), true |
| } |
| |
| // mergeGuess guesses dependencies and merges. |
| // |
| // 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]*yaml.Dependency, vend string, scanned map[string]bool) ([]string, bool) { |
| Info("Scanning %s for dependencies.", pkg) |
| buildContext, err := GetBuildContext() |
| if err != nil { |
| Warn("Could not scan package %q: %s", pkg, err) |
| return []string{}, false |
| } |
| |
| res := []string{} |
| |
| if _, err := os.Stat(dir); err != nil { |
| Warn("Directory is missing: %s", dir) |
| return res, true |
| } |
| |
| d := walkDeps(buildContext, dir, pkg) |
| 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) |
| nd := &yaml.Dependency{ |
| Name: name, |
| Repository: "https://" + repo, |
| } |
| 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 |
| // but not be on vendor. We add any that are on $GOPATH. |
| if _, ok := deps[name]; !ok { |
| Debug("✨☆ GOPATH dependency: %s", name) |
| nd := &yaml.Dependency{Name: name} |
| deps[name] = nd |
| res = append(res, name) |
| } |
| scanned[oname] = true |
| } |
| } |
| |
| return res, true |
| //Info("Package %s manages its own dependencies", pkg) |
| //return []string{}, true |
| } |
| |
| // mergeDeps merges any dependency array into deps. |
| func mergeDeps(orig map[string]*yaml.Dependency, add []*yaml.Dependency, vend string) []string { |
| mod := []string{} |
| for _, dd := range add { |
| // Add it unless it's already there. |
| if existing, ok := orig[dd.Name]; !ok { |
| orig[dd.Name] = dd |
| Debug("Adding %s to the scan list", dd.Name) |
| mod = append(mod, dd.Name) |
| } else if existing.Reference == "" && dd.Reference != "" { |
| // If a nested dep has finer dependency references than outside, |
| // set the reference. |
| existing.Reference = dd.Reference |
| mod = append(mod, dd.Name) |
| } else if dd.Reference != "" && existing.Reference != "" && dd.Reference != existing.Reference { |
| // Check if one is a version and the other is a constraint. If the |
| // version is in the constraint use that. |
| dest := path.Join(vend, dd.Name) |
| repo, err := existing.GetRepo(dest) |
| if err != nil { |
| Warn("Unable to access repo for %s\n", existing.Name) |
| Info("Keeping %s %s", existing.Name, existing.Reference) |
| continue |
| } |
| |
| eIsRef := repo.IsReference(existing.Reference) |
| ddIsRef := repo.IsReference(dd.Reference) |
| |
| // Both are references and different ones. |
| if eIsRef && ddIsRef { |
| Warn("Conflict: %s ref is %s, but also asked for %s\n", existing.Name, existing.Reference, dd.Reference) |
| Info("Keeping %s %s", existing.Name, existing.Reference) |
| } else if eIsRef { |
| // Test ddIsRef is a constraint and if eIsRef is a semver |
| // within that |
| con, err := semver.NewConstraint(dd.Reference) |
| if err != nil { |
| Warn("Version issue for %s: '%s' is neither a reference or semantic version constraint\n", dd.Name, dd.Reference) |
| Info("Keeping %s %s", existing.Name, existing.Reference) |
| continue |
| } |
| |
| ver, err := semver.NewVersion(existing.Reference) |
| if err != nil { |
| // The existing version is not a semantic version. |
| Warn("Conflict: %s version is %s, but also asked for %s\n", existing.Name, existing.Reference, dd.Reference) |
| Info("Keeping %s %s", existing.Name, existing.Reference) |
| continue |
| } |
| |
| if con.Check(ver) { |
| Info("Keeping %s %s because it fits constraint '%s'", existing.Name, existing.Reference, dd.Reference) |
| } else { |
| Warn("Conflict: %s version is %s but does not meet constraint '%s'\n", existing.Name, existing.Reference, dd.Reference) |
| Info("Keeping %s %s", existing.Name, existing.Reference) |
| } |
| |
| } else if ddIsRef { |
| // Test eIsRef is a constraint and if ddIsRef is a semver |
| // within that |
| con, err := semver.NewConstraint(existing.Reference) |
| if err != nil { |
| Warn("Version issue for %s: '%s' is neither a reference or semantic version constraint\n", existing.Name, existing.Reference) |
| Info("Keeping %s %s", existing.Name, existing.Reference) |
| continue |
| } |
| |
| ver, err := semver.NewVersion(dd.Reference) |
| if err != nil { |
| // The dd version is not a semantic version. |
| Warn("Conflict: %s version is %s, but also asked for %s\n", existing.Name, existing.Reference, dd.Reference) |
| Info("Keeping %s %s", existing.Name, existing.Reference) |
| continue |
| } |
| |
| if con.Check(ver) { |
| // Use the specific version if noted instead of the existing |
| // constraint. |
| existing.Reference = dd.Reference |
| mod = append(mod, dd.Name) |
| Info("Using %s %s because it fits constraint '%s'", existing.Name, dd.Reference, existing.Reference) |
| } else { |
| Warn("Conflict: %s semantic version constraint is %s but '%s' does not meet the constraint\n", existing.Name, existing.Reference, dd.Reference) |
| Info("Keeping %s %s", existing.Name, existing.Reference) |
| } |
| } else { |
| // Neither is a vcs reference and both could be semantic version |
| // constraints that are different. |
| |
| _, err := semver.NewConstraint(dd.Reference) |
| if err != nil { |
| // dd.Reference is not a reference or a valid constraint. |
| Warn("Version %s %s is not a reference or valid semantic version constraint\n", dd.Name, dd.Reference) |
| Info("Keeping %s %s", existing.Name, existing.Reference) |
| continue |
| } |
| |
| _, err = semver.NewConstraint(existing.Reference) |
| if err != nil { |
| // existing.Reference is not a reference or a valid constraint. |
| // We really should never end up here. |
| Warn("Version %s %s is not a reference or valid semantic version constraint\n", existing.Name, existing.Reference) |
| |
| existing.Reference = dd.Reference |
| mod = append(mod, dd.Name) |
| Info("Using %s %s because it is a valid version", existing.Name, existing.Reference) |
| continue |
| } |
| |
| // Both versions are constraints. Try to merge them. |
| // If either comparison has an || skip merging. That's complicated. |
| ddor := strings.Index(dd.Reference, "||") |
| eor := strings.Index(existing.Reference, "||") |
| if ddor == -1 && eor == -1 { |
| // Add the comparisons together. |
| newRef := existing.Reference + ", " + dd.Reference |
| existing.Reference = newRef |
| mod = append(mod, dd.Name) |
| Info("Combining %s semantic version constraints %s and %s", existing.Name, existing.Reference, dd.Reference) |
| } else { |
| Warn("Conflict: %s version is %s, but also asked for %s\n", existing.Name, existing.Reference, dd.Reference) |
| Info("Keeping %s %s", existing.Name, existing.Reference) |
| } |
| } |
| } |
| } |
| return mod |
| } |