package cfg import ( "crypto/sha256" "fmt" "io/ioutil" "reflect" "sort" "strings" "github.com/Masterminds/glide/mirrors" "github.com/Masterminds/glide/util" "github.com/Masterminds/vcs" "gopkg.in/yaml.v2" ) // Config is the top-level configuration object. type Config struct { // Name is the name of the package or application. Name string `yaml:"package"` // Description is a short description for a package, application, or library. // This description is similar but different to a Go package description as // it is for marketing and presentation purposes rather than technical ones. Description string `json:"description,omitempty"` // Home is a url to a website for the package. Home string `yaml:"homepage,omitempty"` // License provides either a SPDX license or a path to a file containing // the license. For more information on SPDX see http://spdx.org/licenses/. // When more than one license an SPDX expression can be used. License string `yaml:"license,omitempty"` // Owners is an array of owners for a project. See the Owner type for // more detail. These can be one or more people, companies, or other // organizations. Owners Owners `yaml:"owners,omitempty"` // Ignore contains a list of packages to ignore fetching. This is useful // when walking the package tree (including packages of packages) to list // those to skip. Ignore []string `yaml:"ignore,omitempty"` // Exclude contains a list of directories in the local application to // exclude from scanning for dependencies. Exclude []string `yaml:"excludeDirs,omitempty"` // Imports contains a list of all non-development imports for a project. For // more detail on how these are captured see the Dependency type. Imports Dependencies `yaml:"import"` // DevImports contains the test or other development imports for a project. // See the Dependency type for more details on how this is recorded. DevImports Dependencies `yaml:"testImport,omitempty"` } // A transitive representation of a dependency for importing and exporting to yaml. type cf struct { Name string `yaml:"package"` Description string `yaml:"description,omitempty"` Home string `yaml:"homepage,omitempty"` License string `yaml:"license,omitempty"` Owners Owners `yaml:"owners,omitempty"` Ignore []string `yaml:"ignore,omitempty"` Exclude []string `yaml:"excludeDirs,omitempty"` Imports Dependencies `yaml:"import"` DevImports Dependencies `yaml:"testImport,omitempty"` } // ConfigFromYaml returns an instance of Config from YAML func ConfigFromYaml(yml []byte) (*Config, error) { cfg := &Config{} err := yaml.Unmarshal([]byte(yml), &cfg) return cfg, err } // Marshal converts a Config instance to YAML func (c *Config) Marshal() ([]byte, error) { yml, err := yaml.Marshal(&c) if err != nil { return []byte{}, err } return yml, nil } // UnmarshalYAML is a hook for gopkg.in/yaml.v2 in the unmarshalling process func (c *Config) UnmarshalYAML(unmarshal func(interface{}) error) error { newConfig := &cf{} if err := unmarshal(&newConfig); err != nil { return err } c.Name = newConfig.Name c.Description = newConfig.Description c.Home = newConfig.Home c.License = newConfig.License c.Owners = newConfig.Owners c.Ignore = newConfig.Ignore c.Exclude = newConfig.Exclude c.Imports = newConfig.Imports c.DevImports = newConfig.DevImports // Cleanup the Config object now that we have it. err := c.DeDupe() return err } // MarshalYAML is a hook for gopkg.in/yaml.v2 in the marshaling process func (c *Config) MarshalYAML() (interface{}, error) { newConfig := &cf{ Name: c.Name, Description: c.Description, Home: c.Home, License: c.License, Owners: c.Owners, Ignore: c.Ignore, Exclude: c.Exclude, } i, err := c.Imports.Clone().DeDupe() if err != nil { return newConfig, err } di, err := c.DevImports.Clone().DeDupe() if err != nil { return newConfig, err } newConfig.Imports = i newConfig.DevImports = di return newConfig, nil } // HasDependency returns true if the given name is listed as an import or dev import. func (c *Config) HasDependency(name string) bool { for _, d := range c.Imports { if d.Name == name { return true } } for _, d := range c.DevImports { if d.Name == name { return true } } return false } // HasIgnore returns true if the given name is listed on the ignore list. func (c *Config) HasIgnore(name string) bool { for _, v := range c.Ignore { // Check for both a name and to make sure sub-packages are ignored as // well. if v == name || strings.HasPrefix(name, v+"/") { return true } } return false } // HasExclude returns true if the given name is listed on the exclude list. func (c *Config) HasExclude(ex string) bool { ep := normalizeSlash(ex) for _, v := range c.Exclude { if vp := normalizeSlash(v); vp == ep { return true } } return false } // Clone performs a deep clone of the Config instance func (c *Config) Clone() *Config { n := &Config{} n.Name = c.Name n.Description = c.Description n.Home = c.Home n.License = c.License n.Owners = c.Owners.Clone() n.Ignore = c.Ignore n.Exclude = c.Exclude n.Imports = c.Imports.Clone() n.DevImports = c.DevImports.Clone() return n } // WriteFile writes a Glide YAML file. // // This is a convenience function that marshals the YAML and then writes it to // the given file. If the file exists, it will be clobbered. func (c *Config) WriteFile(glidepath string) error { o, err := c.Marshal() if err != nil { return err } return ioutil.WriteFile(glidepath, o, 0666) } // DeDupe consolidates duplicate dependencies on a Config instance func (c *Config) DeDupe() error { // Remove duplicates in the imports var err error c.Imports, err = c.Imports.DeDupe() if err != nil { return err } c.DevImports, err = c.DevImports.DeDupe() if err != nil { return err } // If the name on the config object is part of the imports remove it. found := -1 for i, dep := range c.Imports { if dep.Name == c.Name { found = i } } if found >= 0 { c.Imports = append(c.Imports[:found], c.Imports[found+1:]...) } found = -1 for i, dep := range c.DevImports { if dep.Name == c.Name { found = i } } if found >= 0 { c.DevImports = append(c.DevImports[:found], c.DevImports[found+1:]...) } // If something is on the ignore list remove it from the imports. for _, v := range c.Ignore { found = -1 for k, d := range c.Imports { if v == d.Name { found = k } } if found >= 0 { c.Imports = append(c.Imports[:found], c.Imports[found+1:]...) } found = -1 for k, d := range c.DevImports { if v == d.Name { found = k } } if found >= 0 { c.DevImports = append(c.DevImports[:found], c.DevImports[found+1:]...) } } return nil } // AddImport appends dependencies to the import list, deduplicating as we go. func (c *Config) AddImport(deps ...*Dependency) error { t := c.Imports t = append(t, deps...) t, err := t.DeDupe() if err != nil { return err } c.Imports = t return nil } // Hash generates a sha256 hash for a given Config func (c *Config) Hash() (string, error) { yml, err := c.Marshal() if err != nil { return "", err } hash := sha256.New() hash.Write(yml) return fmt.Sprintf("%x", hash.Sum(nil)), nil } // Dependencies is a collection of Dependency type Dependencies []*Dependency // Get a dependency by name func (d Dependencies) Get(name string) *Dependency { for _, dep := range d { if dep.Name == name { return dep } } return nil } // Has checks if a dependency is on a list of dependencies such as import or testImport func (d Dependencies) Has(name string) bool { for _, dep := range d { if dep.Name == name { return true } } return false } // Remove removes a dependency from a list of dependencies func (d Dependencies) Remove(name string) Dependencies { found := -1 for i, dep := range d { if dep.Name == name { found = i } } if found >= 0 { copy(d[found:], d[found+1:]) d[len(d)-1] = nil return d[:len(d)-1] } return d } // Clone performs a deep clone of Dependencies func (d Dependencies) Clone() Dependencies { n := make(Dependencies, 0, len(d)) for _, v := range d { n = append(n, v.Clone()) } return n } // DeDupe cleans up duplicates on a list of dependencies. func (d Dependencies) DeDupe() (Dependencies, error) { checked := map[string]int{} imports := make(Dependencies, 0, 1) i := 0 for _, dep := range d { // The first time we encounter a dependency add it to the list if val, ok := checked[dep.Name]; !ok { checked[dep.Name] = i imports = append(imports, dep) i++ } else { // In here we've encountered a dependency for the second time. // Make sure the details are the same or return an error. v := imports[val] if dep.Reference != v.Reference { return d, fmt.Errorf("Import %s repeated with different versions '%s' and '%s'", dep.Name, dep.Reference, v.Reference) } if dep.Repository != v.Repository || dep.VcsType != v.VcsType { return d, fmt.Errorf("Import %s repeated with different Repository details", dep.Name) } if !reflect.DeepEqual(dep.Os, v.Os) || !reflect.DeepEqual(dep.Arch, v.Arch) { return d, fmt.Errorf("Import %s repeated with different OS or Architecture filtering", dep.Name) } imports[checked[dep.Name]].Subpackages = stringArrayDeDupe(v.Subpackages, dep.Subpackages...) } } return imports, nil } // Dependency describes a package that the present package depends upon. type Dependency struct { Name string `yaml:"package"` Reference string `yaml:"version,omitempty"` Pin string `yaml:"-"` Repository string `yaml:"repo,omitempty"` VcsType string `yaml:"vcs,omitempty"` Subpackages []string `yaml:"subpackages,omitempty"` Arch []string `yaml:"arch,omitempty"` Os []string `yaml:"os,omitempty"` } // A transitive representation of a dependency for importing and exploting to yaml. type dep struct { Name string `yaml:"package"` Reference string `yaml:"version,omitempty"` Ref string `yaml:"ref,omitempty"` Repository string `yaml:"repo,omitempty"` VcsType string `yaml:"vcs,omitempty"` Subpackages []string `yaml:"subpackages,omitempty"` Arch []string `yaml:"arch,omitempty"` Os []string `yaml:"os,omitempty"` } // DependencyFromLock converts a Lock to a Dependency func DependencyFromLock(lock *Lock) *Dependency { return &Dependency{ Name: lock.Name, Reference: lock.Version, Repository: lock.Repository, VcsType: lock.VcsType, Subpackages: lock.Subpackages, Arch: lock.Arch, Os: lock.Os, } } // UnmarshalYAML is a hook for gopkg.in/yaml.v2 in the unmarshaling process func (d *Dependency) UnmarshalYAML(unmarshal func(interface{}) error) error { newDep := &dep{} err := unmarshal(&newDep) if err != nil { return err } d.Name = newDep.Name d.Reference = newDep.Reference d.Repository = newDep.Repository d.VcsType = newDep.VcsType d.Subpackages = newDep.Subpackages d.Arch = newDep.Arch d.Os = newDep.Os if d.Reference == "" && newDep.Ref != "" { d.Reference = newDep.Ref } // Make sure only legitimate VCS are listed. d.VcsType = filterVcsType(d.VcsType) // Get the root name for the package tn, subpkg := util.NormalizeName(d.Name) d.Name = tn if subpkg != "" { d.Subpackages = append(d.Subpackages, subpkg) } // Older versions of Glide had a / prefix on subpackages in some cases. // Here that's cleaned up. Someday we should be able to remove this. for k, v := range d.Subpackages { d.Subpackages[k] = strings.TrimPrefix(v, "/") } return nil } // MarshalYAML is a hook for gopkg.in/yaml.v2 in the marshaling process func (d *Dependency) MarshalYAML() (interface{}, error) { // Make sure we only write the correct vcs type to file t := filterVcsType(d.VcsType) newDep := &dep{ Name: d.Name, Reference: d.Reference, Repository: d.Repository, VcsType: t, Subpackages: d.Subpackages, Arch: d.Arch, Os: d.Os, } return newDep, nil } // Remote returns the remote location to fetch source from. This location is // the central place where mirrors can alter the location. func (d *Dependency) Remote() string { var r string if d.Repository != "" { r = d.Repository } else { r = "https://" + d.Name } f, nr, _ := mirrors.Get(r) if f { return nr } return r } // Vcs returns the VCS type to fetch source from. func (d *Dependency) Vcs() string { var r string if d.Repository != "" { r = d.Repository } else { r = "https://" + d.Name } f, _, nv := mirrors.Get(r) if f { return nv } return d.VcsType } // GetRepo retrieves a Masterminds/vcs repo object configured for the root // of the package being retrieved. func (d *Dependency) GetRepo(dest string) (vcs.Repo, error) { // The remote location is either the configured repo or the package // name as an https url. remote := d.Remote() VcsType := d.Vcs() // If the VCS type has a value we try that first. if len(VcsType) > 0 && VcsType != "None" { switch vcs.Type(VcsType) { case vcs.Git: return vcs.NewGitRepo(remote, dest) case vcs.Svn: return vcs.NewSvnRepo(remote, dest) case vcs.Hg: return vcs.NewHgRepo(remote, dest) case vcs.Bzr: return vcs.NewBzrRepo(remote, dest) default: return nil, fmt.Errorf("Unknown VCS type %s set for %s", VcsType, d.Name) } } // When no type set we try to autodetect. return vcs.NewRepo(remote, dest) } // Clone creates a clone of a Dependency func (d *Dependency) Clone() *Dependency { return &Dependency{ Name: d.Name, Reference: d.Reference, Pin: d.Pin, Repository: d.Repository, VcsType: d.VcsType, Subpackages: d.Subpackages, Arch: d.Arch, Os: d.Os, } } // HasSubpackage returns if the subpackage is present on the dependency func (d *Dependency) HasSubpackage(sub string) bool { for _, v := range d.Subpackages { if sub == v { return true } } return false } // Owners is a list of owners for a project. type Owners []*Owner // Clone performs a deep clone of Owners func (o Owners) Clone() Owners { n := make(Owners, 0, 1) for _, v := range o { n = append(n, v.Clone()) } return n } // Owner describes an owner of a package. This can be a person, company, or // other organization. This is useful if someone needs to contact the // owner of a package to address things like a security issue. type Owner struct { // Name describes the name of an organization. Name string `yaml:"name,omitempty"` // Email is an email address to reach the owner at. Email string `yaml:"email,omitempty"` // Home is a url to a website for the owner. Home string `yaml:"homepage,omitempty"` } // Clone creates a clone of a Dependency func (o *Owner) Clone() *Owner { return &Owner{ Name: o.Name, Email: o.Email, Home: o.Home, } } func stringArrayDeDupe(s []string, items ...string) []string { for _, item := range items { exists := false for _, v := range s { if v == item { exists = true } } if !exists { s = append(s, item) } } sort.Strings(s) return s } func filterVcsType(vcs string) string { switch vcs { case "git", "hg", "bzr", "svn": return vcs case "mercurial": return "hg" case "bazaar": return "bzr" case "subversion": return "svn" default: return "" } } func normalizeSlash(k string) string { return strings.Replace(k, "\\", "/", -1) }