| // Copyright 2016 The Go Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style |
| // license that can be found in the LICENSE file. |
| |
| package dep |
| |
| import ( |
| "bytes" |
| "fmt" |
| "io" |
| "reflect" |
| "regexp" |
| "sort" |
| "sync" |
| |
| "github.com/golang/dep/gps" |
| "github.com/golang/dep/gps/pkgtree" |
| "github.com/pelletier/go-toml" |
| "github.com/pkg/errors" |
| ) |
| |
| // ManifestName is the manifest file name used by dep. |
| const ManifestName = "Gopkg.toml" |
| |
| // Errors |
| var ( |
| errInvalidConstraint = errors.Errorf("%q must be a TOML array of tables", "constraint") |
| errInvalidOverride = errors.Errorf("%q must be a TOML array of tables", "override") |
| errInvalidRequired = errors.Errorf("%q must be a TOML list of strings", "required") |
| errInvalidIgnored = errors.Errorf("%q must be a TOML list of strings", "ignored") |
| errInvalidNoVerify = errors.Errorf("%q must be a TOML list of strings", "noverify") |
| errInvalidPrune = errors.Errorf("%q must be a TOML table of booleans", "prune") |
| errInvalidPruneProject = errors.Errorf("%q must be a TOML array of tables", "prune.project") |
| errInvalidMetadata = errors.New("metadata should be a TOML table") |
| |
| errInvalidProjectRoot = errors.New("ProjectRoot name validation failed") |
| |
| errInvalidPruneValue = errors.New("prune options values must be booleans") |
| errPruneSubProject = errors.New("prune projects should not contain sub projects") |
| |
| errRootPruneContainsName = errors.Errorf("%q should not include a name", "prune") |
| errInvalidRootPruneValue = errors.New("root prune options must be omitted instead of being set to false") |
| errInvalidPruneProjectName = errors.Errorf("%q in %q must be a string", "name", "prune.project") |
| errNoName = errors.New("no name provided") |
| ) |
| |
| // Manifest holds manifest file data and implements gps.RootManifest. |
| type Manifest struct { |
| Constraints gps.ProjectConstraints |
| Ovr gps.ProjectConstraints |
| |
| Ignored []string |
| Required []string |
| |
| NoVerify []string |
| |
| PruneOptions gps.CascadingPruneOptions |
| } |
| |
| type rawManifest struct { |
| Constraints []rawProject `toml:"constraint,omitempty"` |
| Overrides []rawProject `toml:"override,omitempty"` |
| Ignored []string `toml:"ignored,omitempty"` |
| Required []string `toml:"required,omitempty"` |
| NoVerify []string `toml:"noverify,omitempty"` |
| PruneOptions rawPruneOptions `toml:"prune,omitempty"` |
| } |
| |
| type rawProject struct { |
| Name string `toml:"name"` |
| Branch string `toml:"branch,omitempty"` |
| Revision string `toml:"revision,omitempty"` |
| Version string `toml:"version,omitempty"` |
| Source string `toml:"source,omitempty"` |
| } |
| |
| type rawPruneOptions struct { |
| UnusedPackages bool `toml:"unused-packages,omitempty"` |
| NonGoFiles bool `toml:"non-go,omitempty"` |
| GoTests bool `toml:"go-tests,omitempty"` |
| |
| //Projects []map[string]interface{} `toml:"project,omitempty"` |
| Projects []map[string]interface{} |
| } |
| |
| const ( |
| pruneOptionUnusedPackages = "unused-packages" |
| pruneOptionGoTests = "go-tests" |
| pruneOptionNonGo = "non-go" |
| ) |
| |
| // Constants representing per-project prune uint8 values. |
| const ( |
| pvnone uint8 = 0 // No per-project prune value was set in Gopkg.toml. |
| pvtrue uint8 = 1 // Per-project prune value was explicitly set to true. |
| pvfalse uint8 = 2 // Per-project prune value was explicitly set to false. |
| ) |
| |
| // NewManifest instantites a new manifest. |
| func NewManifest() *Manifest { |
| return &Manifest{ |
| Constraints: make(gps.ProjectConstraints), |
| Ovr: make(gps.ProjectConstraints), |
| PruneOptions: gps.CascadingPruneOptions{ |
| DefaultOptions: gps.PruneNestedVendorDirs, |
| PerProjectOptions: map[gps.ProjectRoot]gps.PruneOptionSet{}, |
| }, |
| } |
| } |
| |
| func validateManifest(s string) ([]error, error) { |
| var warns []error |
| // Load the TomlTree from string |
| tree, err := toml.Load(s) |
| if err != nil { |
| return warns, errors.Wrap(err, "unable to load TomlTree from string") |
| } |
| // Convert tree to a map |
| manifest := tree.ToMap() |
| |
| // match abbreviated git hash (7chars) or hg hash (12chars) |
| abbrevRevHash := regexp.MustCompile("^[a-f0-9]{7}([a-f0-9]{5})?$") |
| // Look for unknown fields and collect errors |
| for prop, val := range manifest { |
| switch prop { |
| case "metadata": |
| // Check if metadata is of Map type |
| if reflect.TypeOf(val).Kind() != reflect.Map { |
| warns = append(warns, errInvalidMetadata) |
| } |
| case "constraint", "override": |
| valid := true |
| // Invalid if type assertion fails. Not a TOML array of tables. |
| if rawProj, ok := val.([]interface{}); ok { |
| // Check element type. Must be a map. Checking one element would be |
| // enough because TOML doesn't allow mixing of types. |
| if reflect.TypeOf(rawProj[0]).Kind() != reflect.Map { |
| valid = false |
| } |
| |
| if valid { |
| // Iterate through each array of tables |
| for _, v := range rawProj { |
| ruleProvided := false |
| props := v.(map[string]interface{}) |
| // Check the individual field's key to be valid |
| for key, value := range props { |
| // Check if the key is valid |
| switch key { |
| case "name": |
| case "branch", "version", "source": |
| ruleProvided = true |
| case "revision": |
| ruleProvided = true |
| if valueStr, ok := value.(string); ok { |
| if abbrevRevHash.MatchString(valueStr) { |
| warns = append(warns, fmt.Errorf("revision %q should not be in abbreviated form", valueStr)) |
| } |
| } |
| case "metadata": |
| // Check if metadata is of Map type |
| if reflect.TypeOf(value).Kind() != reflect.Map { |
| warns = append(warns, fmt.Errorf("metadata in %q should be a TOML table", prop)) |
| } |
| default: |
| // unknown/invalid key |
| warns = append(warns, fmt.Errorf("invalid key %q in %q", key, prop)) |
| } |
| } |
| if _, ok := props["name"]; !ok { |
| warns = append(warns, errNoName) |
| } else if !ruleProvided && prop == "constraint" { |
| warns = append(warns, fmt.Errorf("branch, version, revision, or source should be provided for %q", props["name"])) |
| } |
| } |
| } |
| } else { |
| valid = false |
| } |
| |
| if !valid { |
| if prop == "constraint" { |
| return warns, errInvalidConstraint |
| } |
| if prop == "override" { |
| return warns, errInvalidOverride |
| } |
| } |
| case "ignored", "required", "noverify": |
| valid := true |
| if rawList, ok := val.([]interface{}); ok { |
| // Check element type of the array. TOML doesn't let mixing of types in |
| // array. Checking one element would be enough. Empty array is valid. |
| if len(rawList) > 0 && reflect.TypeOf(rawList[0]).Kind() != reflect.String { |
| valid = false |
| } |
| } else { |
| valid = false |
| } |
| |
| if !valid { |
| if prop == "ignored" { |
| return warns, errInvalidIgnored |
| } |
| if prop == "required" { |
| return warns, errInvalidRequired |
| } |
| if prop == "noverify" { |
| return warns, errInvalidNoVerify |
| } |
| } |
| case "prune": |
| pruneWarns, err := validatePruneOptions(val, true) |
| warns = append(warns, pruneWarns...) |
| if err != nil { |
| return warns, err |
| } |
| default: |
| warns = append(warns, fmt.Errorf("unknown field in manifest: %v", prop)) |
| } |
| } |
| |
| return warns, nil |
| } |
| |
| func validatePruneOptions(val interface{}, root bool) (warns []error, err error) { |
| if reflect.TypeOf(val).Kind() != reflect.Map { |
| return warns, errInvalidPrune |
| } |
| |
| for key, value := range val.(map[string]interface{}) { |
| switch key { |
| case pruneOptionNonGo, pruneOptionGoTests, pruneOptionUnusedPackages: |
| if option, ok := value.(bool); !ok { |
| return warns, errInvalidPruneValue |
| } else if root && !option { |
| return warns, errInvalidRootPruneValue |
| } |
| case "name": |
| if root { |
| warns = append(warns, errRootPruneContainsName) |
| } else if _, ok := value.(string); !ok { |
| return warns, errInvalidPruneProjectName |
| } |
| case "project": |
| if !root { |
| return warns, errPruneSubProject |
| } |
| if reflect.TypeOf(value).Kind() != reflect.Slice { |
| return warns, errInvalidPruneProject |
| } |
| |
| for _, project := range value.([]interface{}) { |
| projectWarns, err := validatePruneOptions(project, false) |
| warns = append(warns, projectWarns...) |
| if err != nil { |
| return nil, err |
| } |
| } |
| |
| default: |
| if root { |
| warns = append(warns, errors.Errorf("unknown field %q in %q", key, "prune")) |
| } else { |
| warns = append(warns, errors.Errorf("unknown field %q in %q", key, "prune.project")) |
| } |
| } |
| } |
| |
| return warns, err |
| } |
| |
| func checkRedundantPruneOptions(co gps.CascadingPruneOptions) (warns []error) { |
| for name, project := range co.PerProjectOptions { |
| if project.UnusedPackages != pvnone { |
| if (co.DefaultOptions&gps.PruneUnusedPackages != 0) == (project.UnusedPackages == pvtrue) { |
| warns = append(warns, errors.Errorf("redundant prune option %q set for %q", pruneOptionUnusedPackages, name)) |
| } |
| } |
| |
| if project.NonGoFiles != pvnone { |
| if (co.DefaultOptions&gps.PruneNonGoFiles != 0) == (project.NonGoFiles == pvtrue) { |
| warns = append(warns, errors.Errorf("redundant prune option %q set for %q", pruneOptionNonGo, name)) |
| } |
| } |
| |
| if project.GoTests != pvnone { |
| if (co.DefaultOptions&gps.PruneGoTestFiles != 0) == (project.GoTests == pvtrue) { |
| warns = append(warns, errors.Errorf("redundant prune option %q set for %q", pruneOptionGoTests, name)) |
| } |
| } |
| } |
| |
| return warns |
| } |
| |
| // ValidateProjectRoots validates the project roots present in manifest. |
| func ValidateProjectRoots(c *Ctx, m *Manifest, sm gps.SourceManager) error { |
| // Channel to receive all the errors |
| errorCh := make(chan error, len(m.Constraints)+len(m.Ovr)) |
| |
| var wg sync.WaitGroup |
| |
| validate := func(pr gps.ProjectRoot) { |
| defer wg.Done() |
| origPR, err := sm.DeduceProjectRoot(string(pr)) |
| if err != nil { |
| errorCh <- err |
| } else if origPR != pr { |
| errorCh <- fmt.Errorf("the name for %q should be changed to %q", pr, origPR) |
| } |
| } |
| |
| for pr := range m.Constraints { |
| wg.Add(1) |
| go validate(pr) |
| } |
| for pr := range m.Ovr { |
| wg.Add(1) |
| go validate(pr) |
| } |
| for pr := range m.PruneOptions.PerProjectOptions { |
| wg.Add(1) |
| go validate(pr) |
| } |
| |
| wg.Wait() |
| close(errorCh) |
| |
| var valErr error |
| if len(errorCh) > 0 { |
| valErr = errInvalidProjectRoot |
| c.Err.Printf("The following issues were found in Gopkg.toml:\n\n") |
| for err := range errorCh { |
| c.Err.Println(" ✗", err.Error()) |
| } |
| c.Err.Println() |
| } |
| |
| return valErr |
| } |
| |
| // readManifest returns a Manifest read from r and a slice of validation warnings. |
| func readManifest(r io.Reader) (*Manifest, []error, error) { |
| buf := &bytes.Buffer{} |
| _, err := buf.ReadFrom(r) |
| if err != nil { |
| return nil, nil, errors.Wrap(err, "unable to read byte stream") |
| } |
| |
| warns, err := validateManifest(buf.String()) |
| if err != nil { |
| return nil, warns, errors.Wrap(err, "manifest validation failed") |
| } |
| |
| raw := rawManifest{} |
| err = toml.Unmarshal(buf.Bytes(), &raw) |
| if err != nil { |
| return nil, warns, errors.Wrap(err, "unable to parse the manifest as TOML") |
| } |
| |
| m, err := fromRawManifest(raw, buf) |
| if err != nil { |
| return nil, warns, err |
| } |
| |
| warns = append(warns, checkRedundantPruneOptions(m.PruneOptions)...) |
| return m, warns, nil |
| } |
| |
| func fromRawManifest(raw rawManifest, buf *bytes.Buffer) (*Manifest, error) { |
| m := NewManifest() |
| |
| m.Constraints = make(gps.ProjectConstraints, len(raw.Constraints)) |
| m.Ovr = make(gps.ProjectConstraints, len(raw.Overrides)) |
| m.Ignored = raw.Ignored |
| m.Required = raw.Required |
| m.NoVerify = raw.NoVerify |
| |
| for i := 0; i < len(raw.Constraints); i++ { |
| name, prj, err := toProject(raw.Constraints[i]) |
| if err != nil { |
| return nil, err |
| } |
| if _, exists := m.Constraints[name]; exists { |
| return nil, errors.Errorf("multiple dependencies specified for %s, can only specify one", name) |
| } |
| m.Constraints[name] = prj |
| } |
| |
| for i := 0; i < len(raw.Overrides); i++ { |
| name, prj, err := toProject(raw.Overrides[i]) |
| if err != nil { |
| return nil, err |
| } |
| if _, exists := m.Ovr[name]; exists { |
| return nil, errors.Errorf("multiple overrides specified for %s, can only specify one", name) |
| } |
| m.Ovr[name] = prj |
| } |
| |
| // TODO(sdboyer) it is awful that we have to do this manual extraction |
| tree, err := toml.Load(buf.String()) |
| if err != nil { |
| return nil, errors.Wrap(err, "unable to load TomlTree from string") |
| } |
| |
| iprunemap := tree.Get("prune") |
| if iprunemap == nil { |
| return m, nil |
| } |
| // Previous validation already guaranteed that, if it exists, it's this map |
| // type. |
| m.PruneOptions = fromRawPruneOptions(iprunemap.(*toml.Tree).ToMap()) |
| |
| return m, nil |
| } |
| |
| func fromRawPruneOptions(prunemap map[string]interface{}) gps.CascadingPruneOptions { |
| opts := gps.CascadingPruneOptions{ |
| DefaultOptions: gps.PruneNestedVendorDirs, |
| PerProjectOptions: make(map[gps.ProjectRoot]gps.PruneOptionSet), |
| } |
| |
| if val, has := prunemap[pruneOptionUnusedPackages]; has && val.(bool) { |
| opts.DefaultOptions |= gps.PruneUnusedPackages |
| } |
| if val, has := prunemap[pruneOptionNonGo]; has && val.(bool) { |
| opts.DefaultOptions |= gps.PruneNonGoFiles |
| } |
| if val, has := prunemap[pruneOptionGoTests]; has && val.(bool) { |
| opts.DefaultOptions |= gps.PruneGoTestFiles |
| } |
| |
| trinary := func(v interface{}) uint8 { |
| b := v.(bool) |
| if b { |
| return pvtrue |
| } |
| return pvfalse |
| } |
| |
| if projprunes, has := prunemap["project"]; has { |
| for _, proj := range projprunes.([]interface{}) { |
| var pr gps.ProjectRoot |
| // This should be redundant, but being explicit doesn't hurt. |
| pos := gps.PruneOptionSet{NestedVendor: pvtrue} |
| |
| for key, val := range proj.(map[string]interface{}) { |
| switch key { |
| case "name": |
| pr = gps.ProjectRoot(val.(string)) |
| case pruneOptionNonGo: |
| pos.NonGoFiles = trinary(val) |
| case pruneOptionGoTests: |
| pos.GoTests = trinary(val) |
| case pruneOptionUnusedPackages: |
| pos.UnusedPackages = trinary(val) |
| } |
| } |
| opts.PerProjectOptions[pr] = pos |
| } |
| } |
| |
| return opts |
| } |
| |
| // toRawPruneOptions converts a gps.RootPruneOption's PruneOptions to rawPruneOptions |
| // |
| // Will panic if gps.RootPruneOption includes ProjectPruneOptions |
| // See https://github.com/golang/dep/pull/1460#discussion_r158128740 for more information |
| func toRawPruneOptions(co gps.CascadingPruneOptions) rawPruneOptions { |
| if len(co.PerProjectOptions) != 0 { |
| panic("toRawPruneOptions cannot convert ProjectOptions to rawPruneOptions") |
| } |
| raw := rawPruneOptions{} |
| |
| if (co.DefaultOptions & gps.PruneUnusedPackages) != 0 { |
| raw.UnusedPackages = true |
| } |
| |
| if (co.DefaultOptions & gps.PruneNonGoFiles) != 0 { |
| raw.NonGoFiles = true |
| } |
| |
| if (co.DefaultOptions & gps.PruneGoTestFiles) != 0 { |
| raw.GoTests = true |
| } |
| return raw |
| } |
| |
| // toProject interprets the string representations of project information held in |
| // a rawProject, converting them into a proper gps.ProjectProperties. An |
| // error is returned if the rawProject contains some invalid combination - |
| // for example, if both a branch and version constraint are specified. |
| func toProject(raw rawProject) (n gps.ProjectRoot, pp gps.ProjectProperties, err error) { |
| n = gps.ProjectRoot(raw.Name) |
| if raw.Branch != "" { |
| if raw.Version != "" || raw.Revision != "" { |
| return n, pp, errors.Errorf("multiple constraints specified for %s, can only specify one", n) |
| } |
| pp.Constraint = gps.NewBranch(raw.Branch) |
| } else if raw.Version != "" { |
| if raw.Revision != "" { |
| return n, pp, errors.Errorf("multiple constraints specified for %s, can only specify one", n) |
| } |
| |
| // always semver if we can |
| pp.Constraint, err = gps.NewSemverConstraintIC(raw.Version) |
| if err != nil { |
| // but if not, fall back on plain versions |
| pp.Constraint = gps.NewVersion(raw.Version) |
| } |
| } else if raw.Revision != "" { |
| pp.Constraint = gps.Revision(raw.Revision) |
| } else { |
| // If the user specifies nothing, it means an open constraint (accept |
| // anything). |
| pp.Constraint = gps.Any() |
| } |
| |
| pp.Source = raw.Source |
| |
| return n, pp, nil |
| } |
| |
| // MarshalTOML serializes this manifest into TOML via an intermediate raw form. |
| func (m *Manifest) MarshalTOML() ([]byte, error) { |
| raw := m.toRaw() |
| var buf bytes.Buffer |
| enc := toml.NewEncoder(&buf).ArraysWithOneElementPerLine(true) |
| err := enc.Encode(raw) |
| return buf.Bytes(), errors.Wrap(err, "unable to marshal the lock to a TOML string") |
| } |
| |
| // toRaw converts the manifest into a representation suitable to write to the manifest file |
| func (m *Manifest) toRaw() rawManifest { |
| raw := rawManifest{ |
| Constraints: make([]rawProject, 0, len(m.Constraints)), |
| Overrides: make([]rawProject, 0, len(m.Ovr)), |
| Ignored: m.Ignored, |
| Required: m.Required, |
| NoVerify: m.NoVerify, |
| } |
| |
| for n, prj := range m.Constraints { |
| raw.Constraints = append(raw.Constraints, toRawProject(n, prj)) |
| } |
| sort.Sort(sortedRawProjects(raw.Constraints)) |
| |
| for n, prj := range m.Ovr { |
| raw.Overrides = append(raw.Overrides, toRawProject(n, prj)) |
| } |
| sort.Sort(sortedRawProjects(raw.Overrides)) |
| |
| raw.PruneOptions = toRawPruneOptions(m.PruneOptions) |
| |
| return raw |
| } |
| |
| type sortedRawProjects []rawProject |
| |
| func (s sortedRawProjects) Len() int { return len(s) } |
| func (s sortedRawProjects) Swap(i, j int) { s[i], s[j] = s[j], s[i] } |
| func (s sortedRawProjects) Less(i, j int) bool { |
| l, r := s[i], s[j] |
| |
| if l.Name < r.Name { |
| return true |
| } |
| if r.Name < l.Name { |
| return false |
| } |
| |
| return l.Source < r.Source |
| } |
| |
| func toRawProject(name gps.ProjectRoot, project gps.ProjectProperties) rawProject { |
| raw := rawProject{ |
| Name: string(name), |
| Source: project.Source, |
| } |
| |
| if v, ok := project.Constraint.(gps.Version); ok { |
| switch v.Type() { |
| case gps.IsRevision: |
| raw.Revision = v.String() |
| case gps.IsBranch: |
| raw.Branch = v.String() |
| case gps.IsSemver, gps.IsVersion: |
| raw.Version = v.ImpliedCaretString() |
| } |
| return raw |
| } |
| |
| // We simply don't allow for a case where the user could directly |
| // express a 'none' constraint, so we can ignore it here. We also ignore |
| // the 'any' case, because that's the other possibility, and it's what |
| // we interpret not having any constraint expressions at all to mean. |
| // if !gps.IsAny(pp.Constraint) && !gps.IsNone(pp.Constraint) { |
| if !gps.IsAny(project.Constraint) && project.Constraint != nil { |
| // Has to be a semver range. |
| raw.Version = project.Constraint.ImpliedCaretString() |
| } |
| |
| return raw |
| } |
| |
| // DependencyConstraints returns a list of project-level constraints. |
| func (m *Manifest) DependencyConstraints() gps.ProjectConstraints { |
| return m.Constraints |
| } |
| |
| // Overrides returns a list of project-level override constraints. |
| func (m *Manifest) Overrides() gps.ProjectConstraints { |
| return m.Ovr |
| } |
| |
| // IgnoredPackages returns a set of import paths to ignore. |
| func (m *Manifest) IgnoredPackages() *pkgtree.IgnoredRuleset { |
| if m == nil { |
| return pkgtree.NewIgnoredRuleset(nil) |
| } |
| return pkgtree.NewIgnoredRuleset(m.Ignored) |
| } |
| |
| // HasConstraintsOn checks if the manifest contains either constraints or |
| // overrides on the provided ProjectRoot. |
| func (m *Manifest) HasConstraintsOn(root gps.ProjectRoot) bool { |
| if _, has := m.Constraints[root]; has { |
| return true |
| } |
| if _, has := m.Ovr[root]; has { |
| return true |
| } |
| |
| return false |
| } |
| |
| // RequiredPackages returns a set of import paths to require. |
| func (m *Manifest) RequiredPackages() map[string]bool { |
| if m == nil || m == (*Manifest)(nil) { |
| return map[string]bool{} |
| } |
| |
| if len(m.Required) == 0 { |
| return nil |
| } |
| |
| mp := make(map[string]bool, len(m.Required)) |
| for _, i := range m.Required { |
| mp[i] = true |
| } |
| |
| return mp |
| } |