github.com/golang/dep@v0.5.4/manifest.go (about) 1 // Copyright 2016 The Go Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 package dep 6 7 import ( 8 "bytes" 9 "fmt" 10 "io" 11 "reflect" 12 "regexp" 13 "sort" 14 "sync" 15 16 "github.com/golang/dep/gps" 17 "github.com/golang/dep/gps/pkgtree" 18 "github.com/pelletier/go-toml" 19 "github.com/pkg/errors" 20 ) 21 22 // ManifestName is the manifest file name used by dep. 23 const ManifestName = "Gopkg.toml" 24 25 // Errors 26 var ( 27 errInvalidConstraint = errors.Errorf("%q must be a TOML array of tables", "constraint") 28 errInvalidOverride = errors.Errorf("%q must be a TOML array of tables", "override") 29 errInvalidRequired = errors.Errorf("%q must be a TOML list of strings", "required") 30 errInvalidIgnored = errors.Errorf("%q must be a TOML list of strings", "ignored") 31 errInvalidNoVerify = errors.Errorf("%q must be a TOML list of strings", "noverify") 32 errInvalidPrune = errors.Errorf("%q must be a TOML table of booleans", "prune") 33 errInvalidPruneProject = errors.Errorf("%q must be a TOML array of tables", "prune.project") 34 errInvalidMetadata = errors.New("metadata should be a TOML table") 35 36 errInvalidProjectRoot = errors.New("ProjectRoot name validation failed") 37 38 errInvalidPruneValue = errors.New("prune options values must be booleans") 39 errPruneSubProject = errors.New("prune projects should not contain sub projects") 40 41 errRootPruneContainsName = errors.Errorf("%q should not include a name", "prune") 42 errInvalidRootPruneValue = errors.New("root prune options must be omitted instead of being set to false") 43 errInvalidPruneProjectName = errors.Errorf("%q in %q must be a string", "name", "prune.project") 44 errNoName = errors.New("no name provided") 45 ) 46 47 // Manifest holds manifest file data and implements gps.RootManifest. 48 type Manifest struct { 49 Constraints gps.ProjectConstraints 50 Ovr gps.ProjectConstraints 51 52 Ignored []string 53 Required []string 54 55 NoVerify []string 56 57 PruneOptions gps.CascadingPruneOptions 58 } 59 60 type rawManifest struct { 61 Constraints []rawProject `toml:"constraint,omitempty"` 62 Overrides []rawProject `toml:"override,omitempty"` 63 Ignored []string `toml:"ignored,omitempty"` 64 Required []string `toml:"required,omitempty"` 65 NoVerify []string `toml:"noverify,omitempty"` 66 PruneOptions rawPruneOptions `toml:"prune,omitempty"` 67 } 68 69 type rawProject struct { 70 Name string `toml:"name"` 71 Branch string `toml:"branch,omitempty"` 72 Revision string `toml:"revision,omitempty"` 73 Version string `toml:"version,omitempty"` 74 Source string `toml:"source,omitempty"` 75 } 76 77 type rawPruneOptions struct { 78 UnusedPackages bool `toml:"unused-packages,omitempty"` 79 NonGoFiles bool `toml:"non-go,omitempty"` 80 GoTests bool `toml:"go-tests,omitempty"` 81 82 //Projects []map[string]interface{} `toml:"project,omitempty"` 83 Projects []map[string]interface{} 84 } 85 86 const ( 87 pruneOptionUnusedPackages = "unused-packages" 88 pruneOptionGoTests = "go-tests" 89 pruneOptionNonGo = "non-go" 90 ) 91 92 // Constants representing per-project prune uint8 values. 93 const ( 94 pvnone uint8 = 0 // No per-project prune value was set in Gopkg.toml. 95 pvtrue uint8 = 1 // Per-project prune value was explicitly set to true. 96 pvfalse uint8 = 2 // Per-project prune value was explicitly set to false. 97 ) 98 99 // NewManifest instantites a new manifest. 100 func NewManifest() *Manifest { 101 return &Manifest{ 102 Constraints: make(gps.ProjectConstraints), 103 Ovr: make(gps.ProjectConstraints), 104 PruneOptions: gps.CascadingPruneOptions{ 105 DefaultOptions: gps.PruneNestedVendorDirs, 106 PerProjectOptions: map[gps.ProjectRoot]gps.PruneOptionSet{}, 107 }, 108 } 109 } 110 111 func validateManifest(s string) ([]error, error) { 112 var warns []error 113 // Load the TomlTree from string 114 tree, err := toml.Load(s) 115 if err != nil { 116 return warns, errors.Wrap(err, "unable to load TomlTree from string") 117 } 118 // Convert tree to a map 119 manifest := tree.ToMap() 120 121 // match abbreviated git hash (7chars) or hg hash (12chars) 122 abbrevRevHash := regexp.MustCompile("^[a-f0-9]{7}([a-f0-9]{5})?$") 123 // Look for unknown fields and collect errors 124 for prop, val := range manifest { 125 switch prop { 126 case "metadata": 127 // Check if metadata is of Map type 128 if reflect.TypeOf(val).Kind() != reflect.Map { 129 warns = append(warns, errInvalidMetadata) 130 } 131 case "constraint", "override": 132 valid := true 133 // Invalid if type assertion fails. Not a TOML array of tables. 134 if rawProj, ok := val.([]interface{}); ok { 135 // Check element type. Must be a map. Checking one element would be 136 // enough because TOML doesn't allow mixing of types. 137 if reflect.TypeOf(rawProj[0]).Kind() != reflect.Map { 138 valid = false 139 } 140 141 if valid { 142 // Iterate through each array of tables 143 for _, v := range rawProj { 144 ruleProvided := false 145 props := v.(map[string]interface{}) 146 // Check the individual field's key to be valid 147 for key, value := range props { 148 // Check if the key is valid 149 switch key { 150 case "name": 151 case "branch", "version", "source": 152 ruleProvided = true 153 case "revision": 154 ruleProvided = true 155 if valueStr, ok := value.(string); ok { 156 if abbrevRevHash.MatchString(valueStr) { 157 warns = append(warns, fmt.Errorf("revision %q should not be in abbreviated form", valueStr)) 158 } 159 } 160 case "metadata": 161 // Check if metadata is of Map type 162 if reflect.TypeOf(value).Kind() != reflect.Map { 163 warns = append(warns, fmt.Errorf("metadata in %q should be a TOML table", prop)) 164 } 165 default: 166 // unknown/invalid key 167 warns = append(warns, fmt.Errorf("invalid key %q in %q", key, prop)) 168 } 169 } 170 if _, ok := props["name"]; !ok { 171 warns = append(warns, errNoName) 172 } else if !ruleProvided && prop == "constraint" { 173 warns = append(warns, fmt.Errorf("branch, version, revision, or source should be provided for %q", props["name"])) 174 } 175 } 176 } 177 } else { 178 valid = false 179 } 180 181 if !valid { 182 if prop == "constraint" { 183 return warns, errInvalidConstraint 184 } 185 if prop == "override" { 186 return warns, errInvalidOverride 187 } 188 } 189 case "ignored", "required", "noverify": 190 valid := true 191 if rawList, ok := val.([]interface{}); ok { 192 // Check element type of the array. TOML doesn't let mixing of types in 193 // array. Checking one element would be enough. Empty array is valid. 194 if len(rawList) > 0 && reflect.TypeOf(rawList[0]).Kind() != reflect.String { 195 valid = false 196 } 197 } else { 198 valid = false 199 } 200 201 if !valid { 202 if prop == "ignored" { 203 return warns, errInvalidIgnored 204 } 205 if prop == "required" { 206 return warns, errInvalidRequired 207 } 208 if prop == "noverify" { 209 return warns, errInvalidNoVerify 210 } 211 } 212 case "prune": 213 pruneWarns, err := validatePruneOptions(val, true) 214 warns = append(warns, pruneWarns...) 215 if err != nil { 216 return warns, err 217 } 218 default: 219 warns = append(warns, fmt.Errorf("unknown field in manifest: %v", prop)) 220 } 221 } 222 223 return warns, nil 224 } 225 226 func validatePruneOptions(val interface{}, root bool) (warns []error, err error) { 227 if reflect.TypeOf(val).Kind() != reflect.Map { 228 return warns, errInvalidPrune 229 } 230 231 for key, value := range val.(map[string]interface{}) { 232 switch key { 233 case pruneOptionNonGo, pruneOptionGoTests, pruneOptionUnusedPackages: 234 if option, ok := value.(bool); !ok { 235 return warns, errInvalidPruneValue 236 } else if root && !option { 237 return warns, errInvalidRootPruneValue 238 } 239 case "name": 240 if root { 241 warns = append(warns, errRootPruneContainsName) 242 } else if _, ok := value.(string); !ok { 243 return warns, errInvalidPruneProjectName 244 } 245 case "project": 246 if !root { 247 return warns, errPruneSubProject 248 } 249 if reflect.TypeOf(value).Kind() != reflect.Slice { 250 return warns, errInvalidPruneProject 251 } 252 253 for _, project := range value.([]interface{}) { 254 projectWarns, err := validatePruneOptions(project, false) 255 warns = append(warns, projectWarns...) 256 if err != nil { 257 return nil, err 258 } 259 } 260 261 default: 262 if root { 263 warns = append(warns, errors.Errorf("unknown field %q in %q", key, "prune")) 264 } else { 265 warns = append(warns, errors.Errorf("unknown field %q in %q", key, "prune.project")) 266 } 267 } 268 } 269 270 return warns, err 271 } 272 273 func checkRedundantPruneOptions(co gps.CascadingPruneOptions) (warns []error) { 274 for name, project := range co.PerProjectOptions { 275 if project.UnusedPackages != pvnone { 276 if (co.DefaultOptions&gps.PruneUnusedPackages != 0) == (project.UnusedPackages == pvtrue) { 277 warns = append(warns, errors.Errorf("redundant prune option %q set for %q", pruneOptionUnusedPackages, name)) 278 } 279 } 280 281 if project.NonGoFiles != pvnone { 282 if (co.DefaultOptions&gps.PruneNonGoFiles != 0) == (project.NonGoFiles == pvtrue) { 283 warns = append(warns, errors.Errorf("redundant prune option %q set for %q", pruneOptionNonGo, name)) 284 } 285 } 286 287 if project.GoTests != pvnone { 288 if (co.DefaultOptions&gps.PruneGoTestFiles != 0) == (project.GoTests == pvtrue) { 289 warns = append(warns, errors.Errorf("redundant prune option %q set for %q", pruneOptionGoTests, name)) 290 } 291 } 292 } 293 294 return warns 295 } 296 297 // ValidateProjectRoots validates the project roots present in manifest. 298 func ValidateProjectRoots(c *Ctx, m *Manifest, sm gps.SourceManager) error { 299 // Channel to receive all the errors 300 errorCh := make(chan error, len(m.Constraints)+len(m.Ovr)) 301 302 var wg sync.WaitGroup 303 304 validate := func(pr gps.ProjectRoot) { 305 defer wg.Done() 306 origPR, err := sm.DeduceProjectRoot(string(pr)) 307 if err != nil { 308 errorCh <- err 309 } else if origPR != pr { 310 errorCh <- fmt.Errorf("the name for %q should be changed to %q", pr, origPR) 311 } 312 } 313 314 for pr := range m.Constraints { 315 wg.Add(1) 316 go validate(pr) 317 } 318 for pr := range m.Ovr { 319 wg.Add(1) 320 go validate(pr) 321 } 322 for pr := range m.PruneOptions.PerProjectOptions { 323 wg.Add(1) 324 go validate(pr) 325 } 326 327 wg.Wait() 328 close(errorCh) 329 330 var valErr error 331 if len(errorCh) > 0 { 332 valErr = errInvalidProjectRoot 333 c.Err.Printf("The following issues were found in Gopkg.toml:\n\n") 334 for err := range errorCh { 335 c.Err.Println(" ✗", err.Error()) 336 } 337 c.Err.Println() 338 } 339 340 return valErr 341 } 342 343 // readManifest returns a Manifest read from r and a slice of validation warnings. 344 func readManifest(r io.Reader) (*Manifest, []error, error) { 345 buf := &bytes.Buffer{} 346 _, err := buf.ReadFrom(r) 347 if err != nil { 348 return nil, nil, errors.Wrap(err, "unable to read byte stream") 349 } 350 351 warns, err := validateManifest(buf.String()) 352 if err != nil { 353 return nil, warns, errors.Wrap(err, "manifest validation failed") 354 } 355 356 raw := rawManifest{} 357 err = toml.Unmarshal(buf.Bytes(), &raw) 358 if err != nil { 359 return nil, warns, errors.Wrap(err, "unable to parse the manifest as TOML") 360 } 361 362 m, err := fromRawManifest(raw, buf) 363 if err != nil { 364 return nil, warns, err 365 } 366 367 warns = append(warns, checkRedundantPruneOptions(m.PruneOptions)...) 368 return m, warns, nil 369 } 370 371 func fromRawManifest(raw rawManifest, buf *bytes.Buffer) (*Manifest, error) { 372 m := NewManifest() 373 374 m.Constraints = make(gps.ProjectConstraints, len(raw.Constraints)) 375 m.Ovr = make(gps.ProjectConstraints, len(raw.Overrides)) 376 m.Ignored = raw.Ignored 377 m.Required = raw.Required 378 m.NoVerify = raw.NoVerify 379 380 for i := 0; i < len(raw.Constraints); i++ { 381 name, prj, err := toProject(raw.Constraints[i]) 382 if err != nil { 383 return nil, err 384 } 385 if _, exists := m.Constraints[name]; exists { 386 return nil, errors.Errorf("multiple dependencies specified for %s, can only specify one", name) 387 } 388 m.Constraints[name] = prj 389 } 390 391 for i := 0; i < len(raw.Overrides); i++ { 392 name, prj, err := toProject(raw.Overrides[i]) 393 if err != nil { 394 return nil, err 395 } 396 if _, exists := m.Ovr[name]; exists { 397 return nil, errors.Errorf("multiple overrides specified for %s, can only specify one", name) 398 } 399 m.Ovr[name] = prj 400 } 401 402 // TODO(sdboyer) it is awful that we have to do this manual extraction 403 tree, err := toml.Load(buf.String()) 404 if err != nil { 405 return nil, errors.Wrap(err, "unable to load TomlTree from string") 406 } 407 408 iprunemap := tree.Get("prune") 409 if iprunemap == nil { 410 return m, nil 411 } 412 // Previous validation already guaranteed that, if it exists, it's this map 413 // type. 414 m.PruneOptions = fromRawPruneOptions(iprunemap.(*toml.Tree).ToMap()) 415 416 return m, nil 417 } 418 419 func fromRawPruneOptions(prunemap map[string]interface{}) gps.CascadingPruneOptions { 420 opts := gps.CascadingPruneOptions{ 421 DefaultOptions: gps.PruneNestedVendorDirs, 422 PerProjectOptions: make(map[gps.ProjectRoot]gps.PruneOptionSet), 423 } 424 425 if val, has := prunemap[pruneOptionUnusedPackages]; has && val.(bool) { 426 opts.DefaultOptions |= gps.PruneUnusedPackages 427 } 428 if val, has := prunemap[pruneOptionNonGo]; has && val.(bool) { 429 opts.DefaultOptions |= gps.PruneNonGoFiles 430 } 431 if val, has := prunemap[pruneOptionGoTests]; has && val.(bool) { 432 opts.DefaultOptions |= gps.PruneGoTestFiles 433 } 434 435 trinary := func(v interface{}) uint8 { 436 b := v.(bool) 437 if b { 438 return pvtrue 439 } 440 return pvfalse 441 } 442 443 if projprunes, has := prunemap["project"]; has { 444 for _, proj := range projprunes.([]interface{}) { 445 var pr gps.ProjectRoot 446 // This should be redundant, but being explicit doesn't hurt. 447 pos := gps.PruneOptionSet{NestedVendor: pvtrue} 448 449 for key, val := range proj.(map[string]interface{}) { 450 switch key { 451 case "name": 452 pr = gps.ProjectRoot(val.(string)) 453 case pruneOptionNonGo: 454 pos.NonGoFiles = trinary(val) 455 case pruneOptionGoTests: 456 pos.GoTests = trinary(val) 457 case pruneOptionUnusedPackages: 458 pos.UnusedPackages = trinary(val) 459 } 460 } 461 opts.PerProjectOptions[pr] = pos 462 } 463 } 464 465 return opts 466 } 467 468 // toRawPruneOptions converts a gps.RootPruneOption's PruneOptions to rawPruneOptions 469 // 470 // Will panic if gps.RootPruneOption includes ProjectPruneOptions 471 // See https://github.com/golang/dep/pull/1460#discussion_r158128740 for more information 472 func toRawPruneOptions(co gps.CascadingPruneOptions) rawPruneOptions { 473 if len(co.PerProjectOptions) != 0 { 474 panic("toRawPruneOptions cannot convert ProjectOptions to rawPruneOptions") 475 } 476 raw := rawPruneOptions{} 477 478 if (co.DefaultOptions & gps.PruneUnusedPackages) != 0 { 479 raw.UnusedPackages = true 480 } 481 482 if (co.DefaultOptions & gps.PruneNonGoFiles) != 0 { 483 raw.NonGoFiles = true 484 } 485 486 if (co.DefaultOptions & gps.PruneGoTestFiles) != 0 { 487 raw.GoTests = true 488 } 489 return raw 490 } 491 492 // toProject interprets the string representations of project information held in 493 // a rawProject, converting them into a proper gps.ProjectProperties. An 494 // error is returned if the rawProject contains some invalid combination - 495 // for example, if both a branch and version constraint are specified. 496 func toProject(raw rawProject) (n gps.ProjectRoot, pp gps.ProjectProperties, err error) { 497 n = gps.ProjectRoot(raw.Name) 498 if raw.Branch != "" { 499 if raw.Version != "" || raw.Revision != "" { 500 return n, pp, errors.Errorf("multiple constraints specified for %s, can only specify one", n) 501 } 502 pp.Constraint = gps.NewBranch(raw.Branch) 503 } else if raw.Version != "" { 504 if raw.Revision != "" { 505 return n, pp, errors.Errorf("multiple constraints specified for %s, can only specify one", n) 506 } 507 508 // always semver if we can 509 pp.Constraint, err = gps.NewSemverConstraintIC(raw.Version) 510 if err != nil { 511 // but if not, fall back on plain versions 512 pp.Constraint = gps.NewVersion(raw.Version) 513 } 514 } else if raw.Revision != "" { 515 pp.Constraint = gps.Revision(raw.Revision) 516 } else { 517 // If the user specifies nothing, it means an open constraint (accept 518 // anything). 519 pp.Constraint = gps.Any() 520 } 521 522 pp.Source = raw.Source 523 524 return n, pp, nil 525 } 526 527 // MarshalTOML serializes this manifest into TOML via an intermediate raw form. 528 func (m *Manifest) MarshalTOML() ([]byte, error) { 529 raw := m.toRaw() 530 var buf bytes.Buffer 531 enc := toml.NewEncoder(&buf).ArraysWithOneElementPerLine(true) 532 err := enc.Encode(raw) 533 return buf.Bytes(), errors.Wrap(err, "unable to marshal the lock to a TOML string") 534 } 535 536 // toRaw converts the manifest into a representation suitable to write to the manifest file 537 func (m *Manifest) toRaw() rawManifest { 538 raw := rawManifest{ 539 Constraints: make([]rawProject, 0, len(m.Constraints)), 540 Overrides: make([]rawProject, 0, len(m.Ovr)), 541 Ignored: m.Ignored, 542 Required: m.Required, 543 NoVerify: m.NoVerify, 544 } 545 546 for n, prj := range m.Constraints { 547 raw.Constraints = append(raw.Constraints, toRawProject(n, prj)) 548 } 549 sort.Sort(sortedRawProjects(raw.Constraints)) 550 551 for n, prj := range m.Ovr { 552 raw.Overrides = append(raw.Overrides, toRawProject(n, prj)) 553 } 554 sort.Sort(sortedRawProjects(raw.Overrides)) 555 556 raw.PruneOptions = toRawPruneOptions(m.PruneOptions) 557 558 return raw 559 } 560 561 type sortedRawProjects []rawProject 562 563 func (s sortedRawProjects) Len() int { return len(s) } 564 func (s sortedRawProjects) Swap(i, j int) { s[i], s[j] = s[j], s[i] } 565 func (s sortedRawProjects) Less(i, j int) bool { 566 l, r := s[i], s[j] 567 568 if l.Name < r.Name { 569 return true 570 } 571 if r.Name < l.Name { 572 return false 573 } 574 575 return l.Source < r.Source 576 } 577 578 func toRawProject(name gps.ProjectRoot, project gps.ProjectProperties) rawProject { 579 raw := rawProject{ 580 Name: string(name), 581 Source: project.Source, 582 } 583 584 if v, ok := project.Constraint.(gps.Version); ok { 585 switch v.Type() { 586 case gps.IsRevision: 587 raw.Revision = v.String() 588 case gps.IsBranch: 589 raw.Branch = v.String() 590 case gps.IsSemver, gps.IsVersion: 591 raw.Version = v.ImpliedCaretString() 592 } 593 return raw 594 } 595 596 // We simply don't allow for a case where the user could directly 597 // express a 'none' constraint, so we can ignore it here. We also ignore 598 // the 'any' case, because that's the other possibility, and it's what 599 // we interpret not having any constraint expressions at all to mean. 600 // if !gps.IsAny(pp.Constraint) && !gps.IsNone(pp.Constraint) { 601 if !gps.IsAny(project.Constraint) && project.Constraint != nil { 602 // Has to be a semver range. 603 raw.Version = project.Constraint.ImpliedCaretString() 604 } 605 606 return raw 607 } 608 609 // DependencyConstraints returns a list of project-level constraints. 610 func (m *Manifest) DependencyConstraints() gps.ProjectConstraints { 611 return m.Constraints 612 } 613 614 // Overrides returns a list of project-level override constraints. 615 func (m *Manifest) Overrides() gps.ProjectConstraints { 616 return m.Ovr 617 } 618 619 // IgnoredPackages returns a set of import paths to ignore. 620 func (m *Manifest) IgnoredPackages() *pkgtree.IgnoredRuleset { 621 if m == nil { 622 return pkgtree.NewIgnoredRuleset(nil) 623 } 624 return pkgtree.NewIgnoredRuleset(m.Ignored) 625 } 626 627 // HasConstraintsOn checks if the manifest contains either constraints or 628 // overrides on the provided ProjectRoot. 629 func (m *Manifest) HasConstraintsOn(root gps.ProjectRoot) bool { 630 if _, has := m.Constraints[root]; has { 631 return true 632 } 633 if _, has := m.Ovr[root]; has { 634 return true 635 } 636 637 return false 638 } 639 640 // RequiredPackages returns a set of import paths to require. 641 func (m *Manifest) RequiredPackages() map[string]bool { 642 if m == nil || m == (*Manifest)(nil) { 643 return map[string]bool{} 644 } 645 646 if len(m.Required) == 0 { 647 return nil 648 } 649 650 mp := make(map[string]bool, len(m.Required)) 651 for _, i := range m.Required { 652 mp[i] = true 653 } 654 655 return mp 656 }