diff --git a/cmd/flatten.go b/cmd/flatten.go new file mode 100644 index 00000000..84ed576a --- /dev/null +++ b/cmd/flatten.go @@ -0,0 +1,242 @@ +package cmd + +import ( + "os" + "path" + + "github.com/Masterminds/cookoo" + "github.com/kylelemons/go-gypsy/yaml" +) + +// 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 git updates. +// - conf (*Config): The configuration. +// +// Returns: +// +func Flatten(c cookoo.Context, p *cookoo.Params) (interface{}, cookoo.Interrupt) { + packages := p.Get("packages", []string{}).([]string) + conf := p.Get("conf", &Config{}).(*Config) + 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]*Dependency, len(conf.Imports)) + for _, imp := range conf.Imports { + deps[imp.Name] = imp + } + + f := &flattening{conf, vend, vend, deps, packages} + + a, err := recFlatten(f, force) + flattenSetRefs(f) + Info("Project relies on %d dependencies.", len(deps)) + return a, err +} + +type flattening struct { + conf *Config + // Top vendor path, e.g. project/vendor + top string + // Current path + curr string + // Built list of dependencies + deps map[string]*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 flattenUpdateCache = map[string]bool{} + +// refFlatten recursively flattens the vendor tree. +func recFlatten(f *flattening, force bool) (interface{}, 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); ok { + mod = m + } else if m, ok = mergeGodep(base, imp, f.deps); ok { + mod = m + } else if m, ok = mergeGPM(base, imp, f.deps); ok { + mod = m + } else if m, ok = mergeGb(base, imp, f.deps); ok { + mod = m + } else if m, ok = mergeGuess(base, imp, f.deps); ok { + mod = m + } + + if len(mod) > 0 { + Debug("----> Updating all dependencies for %q (%d)", imp, len(mod)) + flattenGlideUp(f, base, force) + f2 := &flattening{ + conf: f.conf, + top: f.top, + curr: base, + deps: f.deps, + scan: mod} + recFlatten(f2, force) + } + } + + // Stopped: Need to recurse down the next level. + return nil, 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 string, force bool) error { + //vdir := path.Join(base, "vendor") + for _, imp := range f.deps { + wd := path.Join(f.top, imp.Name) + if VcsExists(imp, wd) { + if flattenUpdateCache[imp.Name] { + Debug("----> Already updated %s", imp.Name) + continue + } + Debug("Updating project %s (%s)\n", imp.Name, wd) + if err := VcsUpdate(imp, f.top, force); err != nil { + // We can still go on just fine even if this fails. + Warn("Skipped update %s: %s\n", imp.Name, err) + continue + } + flattenUpdateCache[imp.Name] = true + } else { + Debug("Importing %s to project %s\n", imp.Name, wd) + if err := VcsGet(imp, wd); 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]*Dependency) ([]string, bool) { + gp := path.Join(dir, "glide.yaml") + if _, err := os.Stat(gp); err != nil { + return []string{}, false + } + f, err := yaml.ReadFile(gp) + if err != nil { + Warn("Found glide file %q, but can't parse: %s", gp, err) + return []string{}, false + } + + conf, err := FromYaml(f.Root) + 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), 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]*Dependency) ([]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), true +} + +// listGb merges GB dependencies into the deps. +func mergeGb(dir, pkg string, deps map[string]*Dependency) ([]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), true +} + +// mergeGPM merges GPM Godeps files into deps. +func mergeGPM(dir, pkg string, deps map[string]*Dependency) ([]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), true +} + +// mergeGuess guesses dependencies and merges. +func mergeGuess(dir, pkg string, deps map[string]*Dependency) ([]string, bool) { + Info("%s manages its own dependencies.", pkg) + return []string{}, false +} + +// mergeDeps merges any dependency array into deps. +func mergeDeps(orig map[string]*Dependency, add []*Dependency) []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 { + // We can detect version conflicts, but we can't really do + // anything to correct, since we don't know the intentions of the + // authors. + Warn("Conflict: %s ref is %s, but also asked for %s", existing.Name, existing.Reference, dd.Reference) + Info("Keeping %s %s", existing.Name, existing.Reference) + } + } + return mod +} diff --git a/cmd/get_imports.go b/cmd/get_imports.go index 9e173934..405cdc5e 100644 --- a/cmd/get_imports.go +++ b/cmd/get_imports.go @@ -30,6 +30,9 @@ func init() { // GetAll gets zero or more repos. // +// This takes a package name, normalizes it, finds the repo, and installs it. +// It's the workhorse behind `glide get`. +// // Params: // - packages ([]string): Package names to get. // - verbose (bool): default false diff --git a/cmd/recursive_glide.go b/cmd/recursive_glide.go deleted file mode 100644 index 717249ea..00000000 --- a/cmd/recursive_glide.go +++ /dev/null @@ -1,246 +0,0 @@ -package cmd - -import ( - "github.com/Masterminds/cookoo" - "github.com/kylelemons/go-gypsy/yaml" - "io/ioutil" - "os" - "path" - "strings" -) - -// Recurse does glide installs on dependent packages. -// -// Recurse looks in all known packages for a glide.yaml files and installs for -// each one it finds. -// -// The packages scanned can be restricted (at the top level) by providing -// a list of packages to scan in the `packages` param. -// -// Params: -// - enable (bool) -// - importGodeps (bool) -// - importGPM (bool) -// - importGb (bool) -// - deleteFlatten (bool) -// - force (bool) -// - packages ([]string): Packages to recurse through. If empty, does all of them. -func Recurse(c cookoo.Context, p *cookoo.Params) (interface{}, cookoo.Interrupt) { - if !p.Get("enable", true).(bool) { - return nil, nil - } - force := p.Get("force", true).(bool) - plist := p.Get("packages", []string{}).([]string) - pkgs := list2map(plist) - - godeps, gpm, gb, deleteFlatten := false, false, false, false - if g, ok := p.Has("importGodeps"); ok { - godeps = g.(bool) - } - if g, ok := p.Has("importGPM"); ok { - gpm = g.(bool) - } - if g, ok := p.Has("importGb"); ok { - gb = g.(bool) - } - - if g, ok := p.Has("deleteFlatten"); ok { - deleteFlatten = g.(bool) - } - - Info("Checking dependencies for updates. Godeps: %v, GPM: %v, gb: %v\n", godeps, gpm, gb) - if deleteFlatten == true { - Info("Deleting flattened dependencies enabled\n") - } - conf := p.Get("conf", &Config{}).(*Config) - vend, _ := VendorPath(c) - - return recDepResolve(conf, pkgs, vend, godeps, gpm, gb, force, deleteFlatten) -} - -func recDepResolve(conf *Config, filter map[string]bool, vend string, godeps, gpm, gb, force, deleteFlatten bool) (interface{}, error) { - - Info("Inspecting %s.\n", vend) - - if len(conf.Imports) == 0 { - Info("No imports.\n") - } - - restrict := len(filter) > 0 - - // Look in each package to see whether it has a glide.yaml, and no vendor/ - for _, imp := range conf.Imports { - if restrict && !filter[imp.Name] { - Debug("===> Skipping %q", imp.Name) - continue - } - if imp.Flattened == true { - continue - } - base := path.Join(vend, imp.Name) - Info("Looking in %s for a glide.yaml file.\n", base) - if !needsGlideUp(base) { - if godeps { - importGodep(base, imp.Name) - } - if gpm { - importGPM(base, imp.Name) - } - if gb { - importGb(base, imp.Name) - } - if !needsGlideUp(base) { - Info("Package %s manages its own dependencies.\n", imp.Name) - continue - } - } - - if err := dependencyGlideUp(conf, base, godeps, gpm, gb, force, deleteFlatten); err != nil { - Warn("Failed to update dependency %s: %s", imp.Name, err) - } - } - - return nil, nil -} - -func dependencyGlideUp(parentConf *Config, base string, godep, gpm, gb, force, deleteFlatten bool) error { - Info("Doing a glide in %s\n", base) - fname := path.Join(base, "glide.yaml") - f, err := yaml.ReadFile(fname) - if err != nil { - return err - } - - conf, err := FromYaml(f.Root) - conf.Parent = parentConf - if err != nil { - return err - } - for _, imp := range conf.Imports { - vdir := path.Join(base, "vendor") - wd := path.Join(vdir, imp.Name) - // if our root glide.yaml says to flatten this, we skip it - if dep := conf.GetRoot().Imports.Get(imp.Name); dep != nil { - flatten := conf.GetRoot().Flatten - if flatten == true && dep.Flatten == false || - flatten == false && dep.Flatten == true { - flatten = dep.Flatten - } - if flatten == true { - Info("Skipping importing %s due to flatten being set in root import glide.yaml\n", imp.Name) - imp.Flattened = true - } - - if flatten == true && imp.Reference != dep.Reference { - Warn("Flattened package %s ref (%s) is diferent from sub vendored package ref (%s)\n", imp.Name, imp.Reference, dep.Reference) - } - - if imp.Flattened == true && deleteFlatten == true { - if exists, _ := fileExist(wd); exists == true || true { - remove := wd + string(os.PathSeparator) - Warn("Removing flattened sub vendored package: %s\n", strings.TrimPrefix(remove, base)) - rerr := os.RemoveAll(remove) - if rerr != nil { - return rerr - } - } - } - if imp.Flattened == true { - continue - } - } - - // We don't use the global var to find vendor dir name because the - // user may mis-use that var to modify the local vendor dir, and - // we don't want that to break the embedded vendor dirs. - - if err := ensureDir(wd); err != nil { - Warn("Skipped getting %s (vendor/ error): %s\n", imp.Name, err) - continue - } - - if VcsExists(imp, wd) { - Info("Updating project %s (%s)\n", imp.Name, wd) - if err := VcsUpdate(imp, vdir, force); err != nil { - // We can still go on just fine even if this fails. - Warn("Skipped update %s: %s\n", imp.Name, err) - continue - } - } else { - Info("Importing %s to project %s\n", imp.Name, base) - if err := VcsGet(imp, wd); err != nil { - Warn("Skipped getting %s: %v\n", imp.Name, err) - continue - } - } - - // If a revision has been set use it. - err = VcsVersion(imp, vdir) - if err != nil { - Warn("Problem setting version on %s: %s\n", imp.Name, err) - } - - //recDepResolve(conf, path.Join(wd, "vendor")) - } - // We only filter at the top level. - e := map[string]bool{} - recDepResolve(conf, e, path.Join(base, "vendor"), godep, gpm, gb, force, deleteFlatten) - return nil -} - -func ensureDir(dirpath string) error { - if fi, err := os.Stat(dirpath); err == nil && fi.IsDir() { - return nil - } - return os.MkdirAll(dirpath, 0755) -} - -func needsGlideUp(dir string) bool { - stat, err := os.Stat(path.Join(dir, "glide.yaml")) - if err != nil || stat.IsDir() { - return false - } - - // Should probably see if vendor is there and non-empty. - - return true -} - -func importGodep(dir, pkg string) error { - Info("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 err - } - return quickDirtyYAMLWrite(dir, d, pkg) -} - -func importGPM(dir, pkg string) error { - d, err := parseGPMGodeps(dir) - if err != nil { - return err - } - return quickDirtyYAMLWrite(dir, d, pkg) -} - -func importGb(dir, pkg string) error { - Info("Looking in %s/vendor/ for a manifest file.\n", dir) - d, err := parseGbManifest(dir) - if err != nil { - return err - } - return quickDirtyYAMLWrite(dir, d, pkg) -} - -func quickDirtyYAMLWrite(dir string, d []*Dependency, pkg string) error { - if len(d) == 0 { - return nil - } - c := &Config{Name: pkg, Imports: d} - node := c.ToYaml() - data := yaml.Render(node) - f := path.Join(dir, "glide.yaml") - Info("Writing new glide.yaml file in %s\n", dir) - return ioutil.WriteFile(f, []byte(data), 0755) -} diff --git a/cmd/yaml.go b/cmd/yaml.go index f0fbbdfc..c629e1ea 100644 --- a/cmd/yaml.go +++ b/cmd/yaml.go @@ -432,6 +432,7 @@ type Dependency struct { UpdateAsVendored bool Flatten bool Flattened bool + dirty bool } // DependencyFromYaml creates a dependency from a yaml.Node. diff --git a/glide.go b/glide.go index a71c5b5b..88f62eec 100644 --- a/glide.go +++ b/glide.go @@ -416,13 +416,9 @@ func routes(reg *cookoo.Registry, cxt cookoo.Context) { Using("packages").From("cxt:packages"). Using("conf").From("cxt:cfg"). Does(cmd.MergeToYaml, "merged").Using("conf").From("cxt:cfg"). - Does(cmd.Recurse, "recurse").Using("conf").From("cxt:cfg"). - Using("enable").From("cxt:recursiveDependencies"). - Using("importGodeps").From("cxt:importGodeps"). - Using("importGPM").From("cxt:importGPM"). - Using("importGb").From("cxt:importGb"). - Using("force").From("cxt:forceUpdate").WithDefault(false). + Does(cmd.Flatten, "flatten").Using("conf").From("cxt:cfg"). Using("packages").From("cxt:packages"). + Using("force").From("cxt:forceUpdate"). Does(cmd.WriteYaml, "out"). Using("yaml.Node").From("cxt:merged"). Using("filename").WithDefault("glide.yaml").From("cxt:yaml") @@ -450,14 +446,9 @@ func routes(reg *cookoo.Registry, cxt cookoo.Context) { Using("force").From("cxt:forceUpdate"). Using("packages").From("cxt:packages"). Does(cmd.SetReference, "version").Using("conf").From("cxt:cfg"). - Does(cmd.Recurse, "recurse").Using("conf").From("cxt:cfg"). - Using("deleteFlatten").From("cxt:deleteFlatten"). - Using("importGodeps").From("cxt:importGodeps"). - Using("importGPM").From("cxt:importGPM"). - Using("importGb").From("cxt:importGb"). - Using("enable").From("cxt:recursiveDependencies"). - Using("force").From("cxt:forceUpdate"). + Does(cmd.Flatten, "flatten").Using("conf").From("cxt:cfg"). Using("packages").From("cxt:packages"). + Using("force").From("cxt:forceUpdate"). Does(cmd.VendoredCleanUp, "_"). Using("conf").From("cxt:cfg"). Using("update").From("cxt:updateVendoredDeps")