golang.org/x/exp@v0.0.0-20240506185415-9bf2ced13842/cmd/gorelease/gorelease.go (about) 1 // Copyright 2019 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 // gorelease is an experimental tool that helps module authors avoid common 6 // problems before releasing a new version of a module. 7 // 8 // Usage: 9 // 10 // gorelease [-base={version|none}] [-version=version] 11 // 12 // Examples: 13 // 14 // # Compare with the latest version and suggest a new version. 15 // gorelease 16 // 17 // # Compare with a specific version and suggest a new version. 18 // gorelease -base=v1.2.3 19 // 20 // # Compare with the latest version and check a specific new version for compatibility. 21 // gorelease -version=v1.3.0 22 // 23 // # Compare with a specific version and check a specific new version for compatibility. 24 // gorelease -base=v1.2.3 -version=v1.3.0 25 // 26 // gorelease analyzes changes in the public API and dependencies of the main 27 // module. It compares a base version (set with -base) with the currently 28 // checked out revision. Given a proposed version to release (set with 29 // -version), gorelease reports whether the changes are consistent with 30 // semantic versioning. If no version is proposed with -version, gorelease 31 // suggests the lowest version consistent with semantic versioning. 32 // 33 // If there are no visible changes in the module's public API, gorelease 34 // accepts versions that increment the minor or patch version numbers. For 35 // example, if the base version is "v2.3.1", gorelease would accept "v2.3.2" or 36 // "v2.4.0" or any prerelease of those versions, like "v2.4.0-beta". If no 37 // version is proposed, gorelease would suggest "v2.3.2". 38 // 39 // If there are only backward compatible differences in the module's public 40 // API, gorelease only accepts versions that increment the minor version. For 41 // example, if the base version is "v2.3.1", gorelease would accept "v2.4.0" 42 // but not "v2.3.2". 43 // 44 // If there are incompatible API differences for a proposed version with 45 // major version 1 or higher, gorelease will exit with a non-zero status. 46 // Incompatible differences may only be released in a new major version, which 47 // requires creating a module with a different path. For example, if 48 // incompatible changes are made in the module "example.com/mod", a 49 // new major version must be released as a new module, "example.com/mod/v2". 50 // For a proposed version with major version 0, which allows incompatible 51 // changes, gorelease will describe all changes, but incompatible changes 52 // will not affect its exit status. 53 // 54 // For more information on semantic versioning, see https://semver.org. 55 // 56 // Note: gorelease does not accept build metadata in releases (like 57 // v1.0.0+debug). Although it is valid semver, the Go tool and other tools in 58 // the ecosystem do not support it, so its use is not recommended. 59 // 60 // gorelease accepts the following flags: 61 // 62 // -base=version: The version that the current version of the module will be 63 // compared against. This may be a version like "v1.5.2", a version query like 64 // "latest", or "none". If the version is "none", gorelease will not compare the 65 // current version against any previous version; it will only validate the 66 // current version. This is useful for checking the first release of a new major 67 // version. The version may be preceded by a different module path and an '@', 68 // like -base=example.com/mod/v2@v2.5.2. This is useful to compare against 69 // an earlier major version or a fork. If -base is not specified, gorelease will 70 // attempt to infer a base version from the -version flag and available released 71 // versions. 72 // 73 // -version=version: The proposed version to be released. If specified, 74 // gorelease will confirm whether this version is consistent with changes made 75 // to the module's public API. gorelease will exit with a non-zero status if the 76 // version is not valid. 77 // 78 // gorelease is eventually intended to be merged into the go command 79 // as "go release". See golang.org/issues/26420. 80 package main 81 82 import ( 83 "bytes" 84 "context" 85 "encoding/json" 86 "errors" 87 "flag" 88 "fmt" 89 "go/build" 90 "io" 91 "log" 92 "os" 93 "os/exec" 94 "path" 95 "path/filepath" 96 "sort" 97 "strings" 98 "unicode" 99 100 "golang.org/x/exp/apidiff" 101 "golang.org/x/mod/modfile" 102 "golang.org/x/mod/module" 103 "golang.org/x/mod/semver" 104 "golang.org/x/mod/zip" 105 "golang.org/x/tools/go/packages" 106 ) 107 108 // IDEAS: 109 // * Should we suggest versions at all or should -version be mandatory? 110 // * Verify downstream modules have licenses. May need an API or library 111 // for this. Be clear that we can't provide legal advice. 112 // * Internal packages may be relevant to submodules (for example, 113 // golang.org/x/tools/internal/lsp is imported by golang.org/x/tools). 114 // gorelease should detect whether this is the case and include internal 115 // directories in comparison. It should be possible to opt out or specify 116 // a different list of submodules. 117 // * Decide what to do about build constraints, particularly GOOS and GOARCH. 118 // The API may be different on some platforms (e.g., x/sys). 119 // Should gorelease load packages in multiple configurations in the same run? 120 // Is it a compatible change if the same API is available for more platforms? 121 // Is it an incompatible change for fewer? 122 // How about cgo? Is adding a new cgo dependency an incompatible change? 123 // * Support splits and joins of nested modules. For example, if we are 124 // proposing to tag a particular commit as both cloud.google.com/go v0.46.2 125 // and cloud.google.com/go/storage v1.0.0, we should ensure that the sets of 126 // packages provided by those modules are disjoint, and we should not report 127 // the packages moved from one to the other as an incompatible change (since 128 // the APIs are still compatible, just with a different module split). 129 130 // TODO(jayconrod): 131 // * Clean up overuse of fmt.Errorf. 132 // * Support migration to modules after v2.x.y+incompatible. Requires comparing 133 // packages with different module paths. 134 // * Error when packages import from earlier major version of same module. 135 // (this may be intentional; look for real examples first). 136 // * Mechanism to suppress error messages. 137 138 func main() { 139 log.SetFlags(0) 140 log.SetPrefix("gorelease: ") 141 wd, err := os.Getwd() 142 if err != nil { 143 log.Fatal(err) 144 } 145 ctx := context.WithValue(context.Background(), "env", append(os.Environ(), "GO111MODULE=on")) 146 success, err := runRelease(ctx, os.Stdout, wd, os.Args[1:]) 147 if err != nil { 148 if _, ok := err.(*usageError); ok { 149 fmt.Fprintln(os.Stderr, err) 150 os.Exit(2) 151 } else { 152 log.Fatal(err) 153 } 154 } 155 if !success { 156 os.Exit(1) 157 } 158 } 159 160 // runRelease is the main function of gorelease. It's called by tests, so 161 // it writes to w instead of os.Stdout and returns an error instead of 162 // exiting. 163 func runRelease(ctx context.Context, w io.Writer, dir string, args []string) (success bool, err error) { 164 // Validate arguments and flags. We'll print our own errors, since we want to 165 // test without printing to stderr. 166 fs := flag.NewFlagSet("gorelease", flag.ContinueOnError) 167 fs.Usage = func() {} 168 fs.SetOutput(io.Discard) 169 var baseOpt, releaseVersion string 170 fs.StringVar(&baseOpt, "base", "", "previous version to compare against") 171 fs.StringVar(&releaseVersion, "version", "", "proposed version to be released") 172 if err := fs.Parse(args); err != nil { 173 return false, &usageError{err: err} 174 } 175 176 if len(fs.Args()) > 0 { 177 return false, usageErrorf("no arguments allowed") 178 } 179 180 if releaseVersion != "" { 181 if semver.Build(releaseVersion) != "" { 182 return false, usageErrorf("release version %q is not a canonical semantic version: build metadata is not supported", releaseVersion) 183 } 184 if c := semver.Canonical(releaseVersion); c != releaseVersion { 185 return false, usageErrorf("release version %q is not a canonical semantic version", releaseVersion) 186 } 187 } 188 189 var baseModPath, baseVersion string 190 if at := strings.Index(baseOpt, "@"); at >= 0 { 191 baseModPath = baseOpt[:at] 192 baseVersion = baseOpt[at+1:] 193 } else if dot, slash := strings.Index(baseOpt, "."), strings.Index(baseOpt, "/"); dot >= 0 && slash >= 0 && dot < slash { 194 baseModPath = baseOpt 195 } else { 196 baseVersion = baseOpt 197 } 198 if baseModPath == "" { 199 if baseVersion != "" && semver.Canonical(baseVersion) == baseVersion && releaseVersion != "" { 200 if cmp := semver.Compare(baseOpt, releaseVersion); cmp == 0 { 201 return false, usageErrorf("-base and -version must be different") 202 } else if cmp > 0 { 203 return false, usageErrorf("base version (%q) must be lower than release version (%q)", baseVersion, releaseVersion) 204 } 205 } 206 } else if baseModPath != "" && baseVersion == "none" { 207 return false, usageErrorf(`base version (%q) cannot have version "none" with explicit module path`, baseOpt) 208 } 209 210 // Find the local module and repository root directories. 211 modRoot, err := findModuleRoot(dir) 212 if err != nil { 213 return false, err 214 } 215 repoRoot := findRepoRoot(modRoot) 216 217 // Load packages for the version to be released from the local directory. 218 release, err := loadLocalModule(ctx, modRoot, repoRoot, releaseVersion) 219 if err != nil { 220 return false, err 221 } 222 223 // Find the base version if there is one, download it, and load packages from 224 // the module cache. 225 var max string 226 if baseModPath == "" { 227 if baseVersion != "" && semver.Canonical(baseVersion) == baseVersion && module.Check(release.modPath, baseVersion) != nil { 228 // Base version was specified, but it's not consistent with the release 229 // module path, for example, the module path is example.com/m/v2, but 230 // the user said -base=v1.0.0. Instead of making the user explicitly 231 // specify the base module path, we'll adjust the major version suffix. 232 prefix, _, _ := module.SplitPathVersion(release.modPath) 233 major := semver.Major(baseVersion) 234 if strings.HasPrefix(prefix, "gopkg.in/") { 235 baseModPath = prefix + "." + semver.Major(baseVersion) 236 } else if major >= "v2" { 237 baseModPath = prefix + "/" + major 238 } else { 239 baseModPath = prefix 240 } 241 } else { 242 baseModPath = release.modPath 243 max = releaseVersion 244 } 245 } 246 base, err := loadDownloadedModule(ctx, baseModPath, baseVersion, max) 247 if err != nil { 248 return false, err 249 } 250 251 // Compare packages and check for other issues. 252 report, err := makeReleaseReport(ctx, base, release) 253 if err != nil { 254 return false, err 255 } 256 if _, err := fmt.Fprint(w, report.String()); err != nil { 257 return false, err 258 } 259 return report.isSuccessful(), nil 260 } 261 262 type moduleInfo struct { 263 modRoot string // module root directory 264 repoRoot string // repository root directory (may be "") 265 modPath string // module path in go.mod 266 version string // resolved version or "none" 267 versionQuery string // a query like "latest" or "dev-branch", if specified 268 versionInferred bool // true if the version was unspecified and inferred 269 highestTransitiveVersion string // version of the highest transitive self-dependency (cycle) 270 modPathMajor string // major version suffix like "/v3" or ".v2" 271 tagPrefix string // prefix for version tags if module not in repo root 272 273 goModPath string // file path to go.mod 274 goModData []byte // content of go.mod 275 goSumData []byte // content of go.sum 276 goModFile *modfile.File // parsed go.mod file 277 278 diagnostics []string // problems not related to loading specific packages 279 pkgs []*packages.Package // loaded packages with type information 280 281 // Versions of this module which already exist. Only loaded for release 282 // (not base). 283 existingVersions []string 284 } 285 286 // loadLocalModule loads information about a module and its packages from a 287 // local directory. 288 // 289 // modRoot is the directory containing the module's go.mod file. 290 // 291 // repoRoot is the root directory of the repository containing the module or "". 292 // 293 // version is a proposed version for the module or "". 294 func loadLocalModule(ctx context.Context, modRoot, repoRoot, version string) (m moduleInfo, err error) { 295 if repoRoot != "" && !hasFilePathPrefix(modRoot, repoRoot) { 296 return moduleInfo{}, fmt.Errorf("module root %q is not in repository root %q", modRoot, repoRoot) 297 } 298 299 // Load the go.mod file and check the module path and go version. 300 m = moduleInfo{ 301 modRoot: modRoot, 302 repoRoot: repoRoot, 303 version: version, 304 goModPath: filepath.Join(modRoot, "go.mod"), 305 } 306 307 if version != "" && semver.Compare(version, "v0.0.0-99999999999999-zzzzzzzzzzzz") < 0 { 308 m.diagnostics = append(m.diagnostics, fmt.Sprintf("Version %s is lower than most pseudo-versions. Consider releasing v0.1.0-0 instead.", version)) 309 } 310 311 m.goModData, err = os.ReadFile(m.goModPath) 312 if err != nil { 313 return moduleInfo{}, err 314 } 315 m.goModFile, err = modfile.ParseLax(m.goModPath, m.goModData, nil) 316 if err != nil { 317 return moduleInfo{}, err 318 } 319 if m.goModFile.Module == nil { 320 return moduleInfo{}, fmt.Errorf("%s: module directive is missing", m.goModPath) 321 } 322 m.modPath = m.goModFile.Module.Mod.Path 323 if err := checkModPath(m.modPath); err != nil { 324 return moduleInfo{}, err 325 } 326 var ok bool 327 _, m.modPathMajor, ok = module.SplitPathVersion(m.modPath) 328 if !ok { 329 // we just validated the path above. 330 panic(fmt.Sprintf("could not find version suffix in module path %q", m.modPath)) 331 } 332 if m.goModFile.Go == nil { 333 m.diagnostics = append(m.diagnostics, "go.mod: go directive is missing") 334 } 335 336 // Determine the version tag prefix for the module within the repository. 337 if repoRoot != "" && modRoot != repoRoot { 338 if strings.HasPrefix(m.modPathMajor, ".") { 339 m.diagnostics = append(m.diagnostics, fmt.Sprintf("%s: module path starts with gopkg.in and must be declared in the root directory of the repository", m.modPath)) 340 } else { 341 codeDir := filepath.ToSlash(modRoot[len(repoRoot)+1:]) 342 var altGoModPath string 343 if m.modPathMajor == "" { 344 // module has no major version suffix. 345 // codeDir must be a suffix of modPath. 346 // tagPrefix is codeDir with a trailing slash. 347 if strings.HasSuffix(m.modPath, "/"+codeDir) { 348 m.tagPrefix = codeDir + "/" 349 } else { 350 m.diagnostics = append(m.diagnostics, fmt.Sprintf("%s: module path must end with %[2]q, since it is in subdirectory %[2]q", m.modPath, codeDir)) 351 } 352 } else { 353 if strings.HasSuffix(m.modPath, "/"+codeDir) { 354 // module has a major version suffix and is in a major version subdirectory. 355 // codeDir must be a suffix of modPath. 356 // tagPrefix must not include the major version. 357 m.tagPrefix = codeDir[:len(codeDir)-len(m.modPathMajor)+1] 358 altGoModPath = modRoot[:len(modRoot)-len(m.modPathMajor)+1] + "go.mod" 359 } else if strings.HasSuffix(m.modPath, "/"+codeDir+m.modPathMajor) { 360 // module has a major version suffix and is not in a major version subdirectory. 361 // codeDir + modPathMajor is a suffix of modPath. 362 // tagPrefix is codeDir with a trailing slash. 363 m.tagPrefix = codeDir + "/" 364 altGoModPath = filepath.Join(modRoot, m.modPathMajor[1:], "go.mod") 365 } else { 366 m.diagnostics = append(m.diagnostics, fmt.Sprintf("%s: module path must end with %[2]q or %q, since it is in subdirectory %[2]q", m.modPath, codeDir, codeDir+m.modPathMajor)) 367 } 368 } 369 370 // Modules with major version suffixes can be defined in two places 371 // (e.g., sub/go.mod and sub/v2/go.mod). They must not be defined in both. 372 if altGoModPath != "" { 373 if data, err := os.ReadFile(altGoModPath); err == nil { 374 if altModPath := modfile.ModulePath(data); m.modPath == altModPath { 375 goModRel, _ := filepath.Rel(repoRoot, m.goModPath) 376 altGoModRel, _ := filepath.Rel(repoRoot, altGoModPath) 377 m.diagnostics = append(m.diagnostics, fmt.Sprintf("module is defined in two locations:\n\t%s\n\t%s", goModRel, altGoModRel)) 378 } 379 } 380 } 381 } 382 } 383 384 // Load the module's packages. 385 // We pack the module into a zip file and extract it to a temporary directory 386 // as if it were published and downloaded. We'll detect any errors that would 387 // occur (for example, invalid file names). We avoid loading it as the 388 // main module. 389 tmpModRoot, err := copyModuleToTempDir(repoRoot, m.modPath, m.modRoot) 390 if err != nil { 391 return moduleInfo{}, err 392 } 393 defer func() { 394 if rerr := os.RemoveAll(tmpModRoot); err == nil && rerr != nil { 395 err = fmt.Errorf("removing temporary module directory: %v", rerr) 396 } 397 }() 398 tmpLoadDir, tmpGoModData, tmpGoSumData, pkgPaths, prepareDiagnostics, err := prepareLoadDir(ctx, m.goModFile, m.modPath, tmpModRoot, version, false) 399 if err != nil { 400 return moduleInfo{}, err 401 } 402 defer func() { 403 if rerr := os.RemoveAll(tmpLoadDir); err == nil && rerr != nil { 404 err = fmt.Errorf("removing temporary load directory: %v", rerr) 405 } 406 }() 407 408 var loadDiagnostics []string 409 m.pkgs, loadDiagnostics, err = loadPackages(ctx, m.modPath, tmpModRoot, tmpLoadDir, tmpGoModData, tmpGoSumData, pkgPaths) 410 if err != nil { 411 return moduleInfo{}, err 412 } 413 414 m.diagnostics = append(m.diagnostics, prepareDiagnostics...) 415 m.diagnostics = append(m.diagnostics, loadDiagnostics...) 416 417 highestVersion, err := findSelectedVersion(ctx, tmpLoadDir, m.modPath) 418 if err != nil { 419 return moduleInfo{}, err 420 } 421 422 if highestVersion != "" { 423 // A version of the module is included in the transitive dependencies. 424 // Add it to the moduleInfo so that the release report stage can use it 425 // in verifying the version or suggestion a new version, depending on 426 // whether the user provided a version already. 427 m.highestTransitiveVersion = highestVersion 428 } 429 430 retracted, err := loadRetractions(ctx, tmpLoadDir) 431 if err != nil { 432 return moduleInfo{}, err 433 } 434 m.diagnostics = append(m.diagnostics, retracted...) 435 436 return m, nil 437 } 438 439 // loadDownloadedModule downloads a module and loads information about it and 440 // its packages from the module cache. 441 // 442 // modPath is the module path used to fetch the module. The module's path in 443 // go.mod (m.modPath) may be different, for example in a soft fork intended as 444 // a replacement. 445 // 446 // version is the version to load. It may be "none" (indicating nothing should 447 // be loaded), "" (the highest available version below max should be used), a 448 // version query (to be resolved with 'go list'), or a canonical version. 449 // 450 // If version is "" and max is not "", available versions greater than or equal 451 // to max will not be considered. Typically, loadDownloadedModule is used to 452 // load the base version, and max is the release version. 453 func loadDownloadedModule(ctx context.Context, modPath, version, max string) (m moduleInfo, err error) { 454 // Check the module path and version. 455 // If the version is a query, resolve it to a canonical version. 456 m = moduleInfo{modPath: modPath} 457 if err := checkModPath(modPath); err != nil { 458 return moduleInfo{}, err 459 } 460 461 var ok bool 462 _, m.modPathMajor, ok = module.SplitPathVersion(modPath) 463 if !ok { 464 // we just validated the path above. 465 panic(fmt.Sprintf("could not find version suffix in module path %q", modPath)) 466 } 467 468 if version == "none" { 469 // We don't have a base version to compare against. 470 m.version = "none" 471 return m, nil 472 } 473 if version == "" { 474 // Unspecified version: use the highest version below max. 475 m.versionInferred = true 476 if m.version, err = inferBaseVersion(ctx, modPath, max); err != nil { 477 return moduleInfo{}, err 478 } 479 if m.version == "none" { 480 return m, nil 481 } 482 } else if version != module.CanonicalVersion(version) { 483 // Version query: find the real version. 484 m.versionQuery = version 485 if m.version, err = queryVersion(ctx, modPath, version); err != nil { 486 return moduleInfo{}, err 487 } 488 if m.version != "none" && max != "" && semver.Compare(m.version, max) >= 0 { 489 // TODO(jayconrod): reconsider this comparison for pseudo-versions in 490 // general. A query might match different pseudo-versions over time, 491 // depending on ancestor versions, so this might start failing with 492 // no local change. 493 return moduleInfo{}, fmt.Errorf("base version %s (%s) must be lower than release version %s", m.version, m.versionQuery, max) 494 } 495 } else { 496 // Canonical version: make sure it matches the module path. 497 if err := module.CheckPathMajor(version, m.modPathMajor); err != nil { 498 // TODO(golang.org/issue/39666): don't assume this is the base version 499 // or that we're comparing across major versions. 500 return moduleInfo{}, fmt.Errorf("can't compare major versions: base version %s does not belong to module %s", version, modPath) 501 } 502 m.version = version 503 } 504 505 // Download the module into the cache and load the mod file. 506 // Note that goModPath is $GOMODCACHE/cache/download/$modPath/@v/$version.mod, 507 // which is not inside modRoot. This is what the go command uses. Even if 508 // the module didn't have a go.mod file, one will be synthesized there. 509 v := module.Version{Path: modPath, Version: m.version} 510 if m.modRoot, m.goModPath, err = downloadModule(ctx, v); err != nil { 511 return moduleInfo{}, err 512 } 513 if m.goModData, err = os.ReadFile(m.goModPath); err != nil { 514 return moduleInfo{}, err 515 } 516 if m.goModFile, err = modfile.ParseLax(m.goModPath, m.goModData, nil); err != nil { 517 return moduleInfo{}, err 518 } 519 if m.goModFile.Module == nil { 520 return moduleInfo{}, fmt.Errorf("%s: missing module directive", m.goModPath) 521 } 522 m.modPath = m.goModFile.Module.Mod.Path 523 524 // Load packages. 525 tmpLoadDir, tmpGoModData, tmpGoSumData, pkgPaths, _, err := prepareLoadDir(ctx, nil, m.modPath, m.modRoot, m.version, true) 526 if err != nil { 527 return moduleInfo{}, err 528 } 529 defer func() { 530 if rerr := os.RemoveAll(tmpLoadDir); err == nil && rerr != nil { 531 err = fmt.Errorf("removing temporary load directory: %v", err) 532 } 533 }() 534 535 if m.pkgs, _, err = loadPackages(ctx, m.modPath, m.modRoot, tmpLoadDir, tmpGoModData, tmpGoSumData, pkgPaths); err != nil { 536 return moduleInfo{}, err 537 } 538 539 // Calculate the existing versions. 540 ev, err := existingVersions(ctx, m.modPath, tmpLoadDir) 541 if err != nil { 542 return moduleInfo{}, err 543 } 544 m.existingVersions = ev 545 546 return m, nil 547 } 548 549 // makeReleaseReport returns a report comparing the current version of a 550 // module with a previously released version. The report notes any backward 551 // compatible and incompatible changes in the module's public API. It also 552 // diagnoses common problems, such as go.mod or go.sum being incomplete. 553 // The report recommends or validates a release version and indicates a 554 // version control tag to use (with an appropriate prefix, for modules not 555 // in the repository root directory). 556 func makeReleaseReport(ctx context.Context, base, release moduleInfo) (report, error) { 557 // TODO: use apidiff.ModuleChanges. 558 // Compare each pair of packages. 559 // Ignore internal packages. 560 // If we don't have a base version to compare against just check the new 561 // packages for errors. 562 shouldCompare := base.version != "none" 563 isInternal := func(modPath, pkgPath string) bool { 564 if !hasPathPrefix(pkgPath, modPath) { 565 panic(fmt.Sprintf("package %s not in module %s", pkgPath, modPath)) 566 } 567 for pkgPath != modPath { 568 if path.Base(pkgPath) == "internal" { 569 return true 570 } 571 pkgPath = path.Dir(pkgPath) 572 } 573 return false 574 } 575 r := report{ 576 base: base, 577 release: release, 578 } 579 for _, pair := range zipPackages(base.modPath, base.pkgs, release.modPath, release.pkgs) { 580 basePkg, releasePkg := pair.base, pair.release 581 switch { 582 case releasePkg == nil: 583 // Package removed 584 if internal := isInternal(base.modPath, basePkg.PkgPath); !internal || len(basePkg.Errors) > 0 { 585 pr := packageReport{ 586 path: basePkg.PkgPath, 587 baseErrors: basePkg.Errors, 588 } 589 if !internal { 590 pr.Report = apidiff.Report{ 591 Changes: []apidiff.Change{{ 592 Message: "package removed", 593 Compatible: false, 594 }}, 595 } 596 } 597 r.addPackage(pr) 598 } 599 600 case basePkg == nil: 601 // Package added 602 if internal := isInternal(release.modPath, releasePkg.PkgPath); !internal && shouldCompare || len(releasePkg.Errors) > 0 { 603 pr := packageReport{ 604 path: releasePkg.PkgPath, 605 releaseErrors: releasePkg.Errors, 606 } 607 if !internal && shouldCompare { 608 // If we aren't comparing against a base version, don't say 609 // "package added". Only report packages with errors. 610 pr.Report = apidiff.Report{ 611 Changes: []apidiff.Change{{ 612 Message: "package added", 613 Compatible: true, 614 }}, 615 } 616 } 617 r.addPackage(pr) 618 } 619 620 default: 621 // Matched packages 622 // Both packages are internal or neither; we only consider path components 623 // after the module path. 624 internal := isInternal(release.modPath, releasePkg.PkgPath) 625 if !internal && basePkg.Name != "main" && releasePkg.Name != "main" { 626 pr := packageReport{ 627 path: basePkg.PkgPath, 628 baseErrors: basePkg.Errors, 629 releaseErrors: releasePkg.Errors, 630 Report: apidiff.Changes(basePkg.Types, releasePkg.Types), 631 } 632 r.addPackage(pr) 633 } 634 } 635 } 636 637 if r.canVerifyReleaseVersion() { 638 if release.version == "" { 639 r.suggestReleaseVersion() 640 } else { 641 r.validateReleaseVersion() 642 } 643 } 644 645 return r, nil 646 } 647 648 // existingVersions returns the versions that already exist for the given 649 // modPath. 650 func existingVersions(ctx context.Context, modPath, modRoot string) (versions []string, err error) { 651 defer func() { 652 if err != nil { 653 err = fmt.Errorf("listing versions of %s: %w", modPath, err) 654 } 655 }() 656 657 type listVersions struct { 658 Versions []string 659 } 660 cmd := exec.CommandContext(ctx, "go", "list", "-json", "-m", "-versions", modPath) 661 cmd.Env = copyEnv(ctx, cmd.Env) 662 cmd.Dir = modRoot 663 out, err := cmd.Output() 664 if err != nil { 665 return nil, cleanCmdError(err) 666 } 667 if len(out) == 0 { 668 return nil, nil 669 } 670 671 var lv listVersions 672 if err := json.Unmarshal(out, &lv); err != nil { 673 return nil, err 674 } 675 return lv.Versions, nil 676 } 677 678 // findRepoRoot finds the root directory of the repository that contains dir. 679 // findRepoRoot returns "" if it can't find the repository root. 680 func findRepoRoot(dir string) string { 681 vcsDirs := []string{".git", ".hg", ".svn", ".bzr"} 682 d := filepath.Clean(dir) 683 for { 684 for _, vcsDir := range vcsDirs { 685 if _, err := os.Stat(filepath.Join(d, vcsDir)); err == nil { 686 return d 687 } 688 } 689 parent := filepath.Dir(d) 690 if parent == d { 691 return "" 692 } 693 d = parent 694 } 695 } 696 697 // findModuleRoot finds the root directory of the module that contains dir. 698 func findModuleRoot(dir string) (string, error) { 699 d := filepath.Clean(dir) 700 for { 701 if fi, err := os.Stat(filepath.Join(d, "go.mod")); err == nil && !fi.IsDir() { 702 return dir, nil 703 } 704 parent := filepath.Dir(d) 705 if parent == d { 706 break 707 } 708 d = parent 709 } 710 return "", fmt.Errorf("%s: cannot find go.mod file", dir) 711 } 712 713 // checkModPath is like golang.org/x/mod/module.CheckPath, but it returns 714 // friendlier error messages for common mistakes. 715 // 716 // TODO(jayconrod): update module.CheckPath and delete this function. 717 func checkModPath(modPath string) error { 718 if path.IsAbs(modPath) || filepath.IsAbs(modPath) { 719 // TODO(jayconrod): improve error message in x/mod instead of checking here. 720 return fmt.Errorf("module path %q must not be an absolute path.\nIt must be an address where your module may be found.", modPath) 721 } 722 if suffix := dirMajorSuffix(modPath); suffix == "v0" || suffix == "v1" { 723 return fmt.Errorf("module path %q has major version suffix %q.\nA major version suffix is only allowed for v2 or later.", modPath, suffix) 724 } else if strings.HasPrefix(suffix, "v0") { 725 return fmt.Errorf("module path %q has major version suffix %q.\nA major version may not have a leading zero.", modPath, suffix) 726 } else if strings.ContainsRune(suffix, '.') { 727 return fmt.Errorf("module path %q has major version suffix %q.\nA major version may not contain dots.", modPath, suffix) 728 } 729 return module.CheckPath(modPath) 730 } 731 732 // inferBaseVersion returns an appropriate base version if one was not specified 733 // explicitly. 734 // 735 // If max is not "", inferBaseVersion returns the highest available release 736 // version of the module lower than max. Otherwise, inferBaseVersion returns the 737 // highest available release version. Pre-release versions are not considered. 738 // If there is no available version, and max appears to be the first release 739 // version (for example, "v0.1.0", "v2.0.0"), "none" is returned. 740 func inferBaseVersion(ctx context.Context, modPath, max string) (baseVersion string, err error) { 741 defer func() { 742 if err != nil { 743 err = &baseVersionError{err: err, modPath: modPath} 744 } 745 }() 746 747 versions, err := loadVersions(ctx, modPath) 748 if err != nil { 749 return "", err 750 } 751 752 for i := len(versions) - 1; i >= 0; i-- { 753 v := versions[i] 754 if semver.Prerelease(v) == "" && 755 (max == "" || semver.Compare(v, max) < 0) { 756 return v, nil 757 } 758 } 759 760 if max == "" || maybeFirstVersion(max) { 761 return "none", nil 762 } 763 return "", fmt.Errorf("no versions found lower than %s", max) 764 } 765 766 // queryVersion returns the canonical version for a given module version query. 767 func queryVersion(ctx context.Context, modPath, query string) (resolved string, err error) { 768 defer func() { 769 if err != nil { 770 err = fmt.Errorf("could not resolve version %s@%s: %w", modPath, query, err) 771 } 772 }() 773 if query == "upgrade" || query == "patch" { 774 return "", errors.New("query is based on requirements in main go.mod file") 775 } 776 777 tmpDir, err := os.MkdirTemp("", "") 778 if err != nil { 779 return "", err 780 } 781 defer func() { 782 if rerr := os.Remove(tmpDir); rerr != nil && err == nil { 783 err = rerr 784 } 785 }() 786 arg := modPath + "@" + query 787 cmd := exec.CommandContext(ctx, "go", "list", "-m", "-f", "{{.Version}}", "--", arg) 788 cmd.Env = copyEnv(ctx, cmd.Env) 789 cmd.Dir = tmpDir 790 cmd.Env = append(cmd.Env, "GO111MODULE=on") 791 out, err := cmd.Output() 792 if err != nil { 793 return "", cleanCmdError(err) 794 } 795 return strings.TrimSpace(string(out)), nil 796 } 797 798 // loadVersions loads the list of versions for the given module using 799 // 'go list -m -versions'. The returned versions are sorted in ascending 800 // semver order. 801 func loadVersions(ctx context.Context, modPath string) (versions []string, err error) { 802 defer func() { 803 if err != nil { 804 err = fmt.Errorf("could not load versions for %s: %v", modPath, err) 805 } 806 }() 807 808 tmpDir, err := os.MkdirTemp("", "") 809 if err != nil { 810 return nil, err 811 } 812 defer func() { 813 if rerr := os.Remove(tmpDir); rerr != nil && err == nil { 814 err = rerr 815 } 816 }() 817 cmd := exec.CommandContext(ctx, "go", "list", "-m", "-versions", "--", modPath) 818 cmd.Env = copyEnv(ctx, cmd.Env) 819 cmd.Dir = tmpDir 820 out, err := cmd.Output() 821 if err != nil { 822 return nil, cleanCmdError(err) 823 } 824 versions = strings.Fields(string(out)) 825 if len(versions) > 0 { 826 versions = versions[1:] // skip module path 827 } 828 829 // Sort versions defensively. 'go list -m -versions' should always returns 830 // a sorted list of versions, but it's fast and easy to sort them here, too. 831 sort.Slice(versions, func(i, j int) bool { 832 return semver.Compare(versions[i], versions[j]) < 0 833 }) 834 return versions, nil 835 } 836 837 // maybeFirstVersion returns whether v appears to be the first version 838 // of a module. 839 func maybeFirstVersion(v string) bool { 840 major, minor, patch, _, _, err := parseVersion(v) 841 if err != nil { 842 return false 843 } 844 if major == "0" { 845 return minor == "0" && patch == "0" || 846 minor == "0" && patch == "1" || 847 minor == "1" && patch == "0" 848 } 849 return minor == "0" && patch == "0" 850 } 851 852 // dirMajorSuffix returns a major version suffix for a slash-separated path. 853 // For example, for the path "foo/bar/v2", dirMajorSuffix would return "v2". 854 // If no major version suffix is found, "" is returned. 855 // 856 // dirMajorSuffix is less strict than module.SplitPathVersion so that incorrect 857 // suffixes like "v0", "v02", "v1.2" can be detected. It doesn't handle 858 // special cases for gopkg.in paths. 859 func dirMajorSuffix(path string) string { 860 i := len(path) 861 for i > 0 && ('0' <= path[i-1] && path[i-1] <= '9') || path[i-1] == '.' { 862 i-- 863 } 864 if i <= 1 || i == len(path) || path[i-1] != 'v' || (i > 1 && path[i-2] != '/') { 865 return "" 866 } 867 return path[i-1:] 868 } 869 870 // copyModuleToTempDir copies module files from modRoot to a subdirectory of 871 // scratchDir. Submodules, vendor directories, and irregular files are excluded. 872 // An error is returned if the module contains any files or directories that 873 // can't be included in a module zip file (due to special characters, 874 // excessive sizes, etc.). 875 func copyModuleToTempDir(repoRoot, modPath, modRoot string) (dir string, err error) { 876 // Generate a fake version consistent with modPath. We need a canonical 877 // version to create a zip file. 878 version := "v0.0.0-gorelease" 879 _, majorPathSuffix, _ := module.SplitPathVersion(modPath) 880 if majorPathSuffix != "" { 881 version = majorPathSuffix[1:] + ".0.0-gorelease" 882 } 883 m := module.Version{Path: modPath, Version: version} 884 885 zipFile, err := os.CreateTemp("", "gorelease-*.zip") 886 if err != nil { 887 return "", err 888 } 889 defer func() { 890 zipFile.Close() 891 os.Remove(zipFile.Name()) 892 }() 893 894 dir, err = os.MkdirTemp("", "gorelease") 895 if err != nil { 896 return "", err 897 } 898 defer func() { 899 if err != nil { 900 os.RemoveAll(dir) 901 dir = "" 902 } 903 }() 904 905 var fallbackToDir bool 906 if repoRoot != "" { 907 var err error 908 fallbackToDir, err = tryCreateFromVCS(zipFile, m, modRoot, repoRoot) 909 if err != nil { 910 return "", err 911 } 912 } 913 914 if repoRoot == "" || fallbackToDir { 915 // Not a recognised repo: fall back to creating from dir. 916 if err := zip.CreateFromDir(zipFile, m, modRoot); err != nil { 917 var e zip.FileErrorList 918 if errors.As(err, &e) { 919 return "", e 920 } 921 return "", err 922 } 923 } 924 925 if err := zipFile.Close(); err != nil { 926 return "", err 927 } 928 if err := zip.Unzip(dir, m, zipFile.Name()); err != nil { 929 return "", err 930 } 931 return dir, nil 932 } 933 934 // tryCreateFromVCS tries to create a module zip file from VCS. If it succeeds, 935 // it returns fallBackToDir false and a nil err. If it fails in a recoverable 936 // way, it returns fallBackToDir true and a nil err. If it fails in an 937 // unrecoverable way, it returns a non-nil err. 938 func tryCreateFromVCS(zipFile io.Writer, m module.Version, modRoot, repoRoot string) (fallbackToDir bool, _ error) { 939 // We recognised a repo: create from VCS. 940 if !hasFilePathPrefix(modRoot, repoRoot) { 941 panic(fmt.Sprintf("repo root %q is not a prefix of mod root %q", repoRoot, modRoot)) 942 } 943 hasUncommitted, err := hasGitUncommittedChanges(repoRoot) 944 if err != nil { 945 // Fallback to CreateFromDir. 946 return true, nil 947 } 948 if hasUncommitted { 949 return false, fmt.Errorf("repo %s has uncommitted changes", repoRoot) 950 } 951 modRel := filepath.ToSlash(trimFilePathPrefix(modRoot, repoRoot)) 952 if err := zip.CreateFromVCS(zipFile, m, repoRoot, "HEAD", modRel); err != nil { 953 var fel zip.FileErrorList 954 if errors.As(err, &fel) { 955 return false, fel 956 } 957 var uve *zip.UnrecognizedVCSError 958 if errors.As(err, &uve) { 959 // Fallback to CreateFromDir. 960 return true, nil 961 } 962 return false, err 963 } 964 // Success! 965 return false, nil 966 } 967 968 // downloadModule downloads a specific version of a module to the 969 // module cache using 'go mod download'. 970 func downloadModule(ctx context.Context, m module.Version) (modRoot, goModPath string, err error) { 971 defer func() { 972 if err != nil { 973 err = &downloadError{m: m, err: cleanCmdError(err)} 974 } 975 }() 976 977 // Run 'go mod download' from a temporary directory to avoid needing to load 978 // go.mod from gorelease's working directory (or a parent). 979 // go.mod may be broken, and we don't need it. 980 // TODO(golang.org/issue/36812): 'go mod download' reads go.mod even though 981 // we don't need information about the main module or the build list. 982 // If it didn't read go.mod in this case, we wouldn't need a temp directory. 983 tmpDir, err := os.MkdirTemp("", "gorelease-download") 984 if err != nil { 985 return "", "", err 986 } 987 defer os.Remove(tmpDir) 988 cmd := exec.CommandContext(ctx, "go", "mod", "download", "-json", "--", m.Path+"@"+m.Version) 989 cmd.Env = copyEnv(ctx, cmd.Env) 990 cmd.Dir = tmpDir 991 out, err := cmd.Output() 992 var xerr *exec.ExitError 993 if err != nil { 994 var ok bool 995 if xerr, ok = err.(*exec.ExitError); !ok { 996 return "", "", err 997 } 998 } 999 1000 // If 'go mod download' exited unsuccessfully but printed well-formed JSON 1001 // with an error, return that error. 1002 parsed := struct{ Dir, GoMod, Error string }{} 1003 if jsonErr := json.Unmarshal(out, &parsed); jsonErr != nil { 1004 if xerr != nil { 1005 return "", "", cleanCmdError(xerr) 1006 } 1007 return "", "", jsonErr 1008 } 1009 if parsed.Error != "" { 1010 return "", "", errors.New(parsed.Error) 1011 } 1012 if xerr != nil { 1013 return "", "", cleanCmdError(xerr) 1014 } 1015 return parsed.Dir, parsed.GoMod, nil 1016 } 1017 1018 // prepareLoadDir creates a temporary directory and a go.mod file that requires 1019 // the module being loaded. go.sum is copied if present. It also creates a .go 1020 // file that imports every package in the given modPath. This temporary module 1021 // is useful for two reasons. First, replace and exclude directives from the 1022 // target module aren't applied, so we have the same view as a dependent module. 1023 // Second, we can run commands like 'go get' without modifying the original 1024 // go.mod and go.sum files. 1025 // 1026 // modFile is the pre-parsed go.mod file. If non-nil, its requirements and 1027 // go version will be copied so that incomplete and out-of-date requirements 1028 // may be reported later. 1029 // 1030 // modPath is the module's path. 1031 // 1032 // modRoot is the module's root directory. 1033 // 1034 // version is the version of the module being loaded. If must be canonical 1035 // for modules loaded from the cache. Otherwise, it may be empty (for example, 1036 // when no release version is proposed). 1037 // 1038 // cached indicates whether the module is being loaded from the module cache. 1039 // If cached is true, then the module lives in the cache at 1040 // $GOMODCACHE/$modPath@$version/. Its go.mod file is at 1041 // $GOMODCACHE/cache/download/$modPath/@v/$version.mod. It must be referenced 1042 // with a simple require. A replace directive won't work because it may not have 1043 // a go.mod file in modRoot. 1044 // If cached is false, then modRoot is somewhere outside the module cache 1045 // (ex /tmp). We'll reference it with a local replace directive. It must have a 1046 // go.mod file in modRoot. 1047 // 1048 // dir is the location of the temporary directory. 1049 // 1050 // goModData and goSumData are the contents of the go.mod and go.sum files, 1051 // respectively. 1052 // 1053 // pkgPaths are the import paths of the module being loaded, including the path 1054 // to any main packages (as if they were importable). 1055 func prepareLoadDir(ctx context.Context, modFile *modfile.File, modPath, modRoot, version string, cached bool) (dir string, goModData, goSumData []byte, pkgPaths []string, diagnostics []string, err error) { 1056 defer func() { 1057 if err != nil { 1058 if cached { 1059 err = fmt.Errorf("preparing to load packages for %s@%s: %w", modPath, version, err) 1060 } else { 1061 err = fmt.Errorf("preparing to load packages for %s: %w", modPath, err) 1062 } 1063 } 1064 }() 1065 1066 if module.Check(modPath, version) != nil { 1067 // If no version is proposed or if the version isn't valid, use a fake 1068 // version that matches the module's major version suffix. If the version 1069 // is invalid, that will be reported elsewhere. 1070 version = "v0.0.0-gorelease" 1071 if _, pathMajor, _ := module.SplitPathVersion(modPath); pathMajor != "" { 1072 version = pathMajor[1:] + ".0.0-gorelease" 1073 } 1074 } 1075 1076 dir, err = os.MkdirTemp("", "gorelease-load") 1077 if err != nil { 1078 return "", nil, nil, nil, nil, err 1079 } 1080 1081 f := &modfile.File{} 1082 f.AddModuleStmt("gorelease-load-module") 1083 f.AddRequire(modPath, version) 1084 if !cached { 1085 f.AddReplace(modPath, version, modRoot, "") 1086 } 1087 if modFile != nil { 1088 if modFile.Go != nil { 1089 f.AddGoStmt(modFile.Go.Version) 1090 } 1091 for _, r := range modFile.Require { 1092 f.AddRequire(r.Mod.Path, r.Mod.Version) 1093 } 1094 } 1095 goModData, err = f.Format() 1096 if err != nil { 1097 return "", nil, nil, nil, nil, err 1098 } 1099 if err := os.WriteFile(filepath.Join(dir, "go.mod"), goModData, 0666); err != nil { 1100 return "", nil, nil, nil, nil, err 1101 } 1102 1103 goSumData, err = os.ReadFile(filepath.Join(modRoot, "go.sum")) 1104 if err != nil && !os.IsNotExist(err) { 1105 return "", nil, nil, nil, nil, err 1106 } 1107 if err := os.WriteFile(filepath.Join(dir, "go.sum"), goSumData, 0666); err != nil { 1108 return "", nil, nil, nil, nil, err 1109 } 1110 1111 // Add a .go file with requirements, so that `go get` won't blat 1112 // requirements. 1113 fakeImports := &strings.Builder{} 1114 fmt.Fprint(fakeImports, "package tmp\n") 1115 imps, err := collectImportPaths(modPath, modRoot) 1116 if err != nil { 1117 return "", nil, nil, nil, nil, err 1118 } 1119 for _, imp := range imps { 1120 fmt.Fprintf(fakeImports, "import _ %q\n", imp) 1121 } 1122 if err := os.WriteFile(filepath.Join(dir, "tmp.go"), []byte(fakeImports.String()), 0666); err != nil { 1123 return "", nil, nil, nil, nil, err 1124 } 1125 1126 // Add missing requirements. 1127 cmd := exec.CommandContext(ctx, "go", "get", "-d", ".") 1128 cmd.Env = copyEnv(ctx, cmd.Env) 1129 cmd.Dir = dir 1130 if _, err := cmd.Output(); err != nil { 1131 return "", nil, nil, nil, nil, fmt.Errorf("looking for missing dependencies: %w", cleanCmdError(err)) 1132 } 1133 1134 // Report new requirements in go.mod. 1135 goModPath := filepath.Join(dir, "go.mod") 1136 loadReqs := func(data []byte) (reqs []module.Version, err error) { 1137 modFile, err := modfile.ParseLax(goModPath, data, nil) 1138 if err != nil { 1139 return nil, err 1140 } 1141 for _, r := range modFile.Require { 1142 reqs = append(reqs, r.Mod) 1143 } 1144 return reqs, nil 1145 } 1146 1147 oldReqs, err := loadReqs(goModData) 1148 if err != nil { 1149 return "", nil, nil, nil, nil, err 1150 } 1151 newGoModData, err := os.ReadFile(goModPath) 1152 if err != nil { 1153 return "", nil, nil, nil, nil, err 1154 } 1155 newReqs, err := loadReqs(newGoModData) 1156 if err != nil { 1157 return "", nil, nil, nil, nil, err 1158 } 1159 1160 oldMap := make(map[module.Version]bool) 1161 for _, req := range oldReqs { 1162 oldMap[req] = true 1163 } 1164 var missing []module.Version 1165 for _, req := range newReqs { 1166 // Ignore cyclic imports, since a module never needs to require itself. 1167 if req.Path == modPath { 1168 continue 1169 } 1170 if !oldMap[req] { 1171 missing = append(missing, req) 1172 } 1173 } 1174 1175 if len(missing) > 0 { 1176 var missingReqs []string 1177 for _, m := range missing { 1178 missingReqs = append(missingReqs, m.String()) 1179 } 1180 diagnostics = append(diagnostics, fmt.Sprintf("go.mod: the following requirements are needed\n\t%s\nRun 'go mod tidy' to add missing requirements.", strings.Join(missingReqs, "\n\t"))) 1181 return dir, goModData, goSumData, imps, diagnostics, nil 1182 } 1183 1184 // Cached modules may have no go.sum. 1185 // We skip comparison because a downloaded module is outside the user's 1186 // control. 1187 if !cached { 1188 // Check if 'go get' added new hashes to go.sum. 1189 goSumPath := filepath.Join(dir, "go.sum") 1190 newGoSumData, err := os.ReadFile(goSumPath) 1191 if err != nil { 1192 if !os.IsNotExist(err) { 1193 return "", nil, nil, nil, nil, err 1194 } 1195 // If the sum doesn't exist, that's ok: we'll treat "no go.sum" like 1196 // "empty go.sum". 1197 } 1198 1199 if !sumsMatchIgnoringPath(string(goSumData), string(newGoSumData), modPath) { 1200 diagnostics = append(diagnostics, "go.sum: one or more sums are missing. Run 'go mod tidy' to add missing sums.") 1201 } 1202 } 1203 1204 return dir, goModData, goSumData, imps, diagnostics, nil 1205 } 1206 1207 // sumsMatchIgnoringPath checks whether the two sums match. It ignores any lines 1208 // which contains the given modPath. 1209 func sumsMatchIgnoringPath(sum1, sum2, modPathToIgnore string) bool { 1210 lines1 := make(map[string]bool) 1211 for _, line := range strings.Split(string(sum1), "\n") { 1212 if line == "" { 1213 continue 1214 } 1215 lines1[line] = true 1216 } 1217 for _, line := range strings.Split(string(sum2), "\n") { 1218 if line == "" { 1219 continue 1220 } 1221 parts := strings.Fields(line) 1222 if len(parts) < 1 { 1223 panic(fmt.Sprintf("go.sum malformed: unexpected line %s", line)) 1224 } 1225 if parts[0] == modPathToIgnore { 1226 continue 1227 } 1228 1229 if !lines1[line] { 1230 return false 1231 } 1232 } 1233 1234 lines2 := make(map[string]bool) 1235 for _, line := range strings.Split(string(sum2), "\n") { 1236 if line == "" { 1237 continue 1238 } 1239 lines2[line] = true 1240 } 1241 for _, line := range strings.Split(string(sum1), "\n") { 1242 if line == "" { 1243 continue 1244 } 1245 parts := strings.Fields(line) 1246 if len(parts) < 1 { 1247 panic(fmt.Sprintf("go.sum malformed: unexpected line %s", line)) 1248 } 1249 if parts[0] == modPathToIgnore { 1250 continue 1251 } 1252 1253 if !lines2[line] { 1254 return false 1255 } 1256 } 1257 1258 return true 1259 } 1260 1261 // collectImportPaths visits the given root and traverses its directories 1262 // recursively, collecting the import paths of all importable packages in each 1263 // directory along the way. 1264 // 1265 // modPath is the module path. 1266 // root is the root directory of the module to collect imports for (the root 1267 // of the modPath module). 1268 // 1269 // Note: the returned importPaths will include main if it exists in root. 1270 func collectImportPaths(modPath, root string) (importPaths []string, _ error) { 1271 err := filepath.Walk(root, func(walkPath string, fi os.FileInfo, err error) error { 1272 if err != nil { 1273 return err 1274 } 1275 1276 // Avoid .foo, _foo, and testdata subdirectory trees. 1277 if !fi.IsDir() { 1278 return nil 1279 } 1280 base := filepath.Base(walkPath) 1281 if strings.HasPrefix(base, ".") || strings.HasPrefix(base, "_") || base == "testdata" || base == "internal" { 1282 return filepath.SkipDir 1283 } 1284 1285 p, err := build.Default.ImportDir(walkPath, 0) 1286 if err != nil { 1287 if nogoErr := (*build.NoGoError)(nil); errors.As(err, &nogoErr) { 1288 // No .go files found in directory. That's ok, we'll keep 1289 // searching. 1290 return nil 1291 } 1292 return err 1293 } 1294 1295 // Construct the import path. 1296 importPath := path.Join(modPath, filepath.ToSlash(trimFilePathPrefix(p.Dir, root))) 1297 importPaths = append(importPaths, importPath) 1298 1299 return nil 1300 }) 1301 if err != nil { 1302 return nil, fmt.Errorf("listing packages in %s: %v", root, err) 1303 } 1304 1305 return importPaths, nil 1306 } 1307 1308 // loadPackages returns a list of all packages in the module modPath, sorted by 1309 // package path. modRoot is the module root directory, but packages are loaded 1310 // from loadDir, which must contain go.mod and go.sum containing goModData and 1311 // goSumData. 1312 // 1313 // We load packages from a temporary external module so that replace and exclude 1314 // directives are not applied. The loading process may also modify go.mod and 1315 // go.sum, and we want to detect and report differences. 1316 // 1317 // Package loading errors will be returned in the Errors field of each package. 1318 // Other diagnostics (such as the go.sum file being incomplete) will be 1319 // returned through diagnostics. 1320 // err will be non-nil in case of a fatal error that prevented packages 1321 // from being loaded. 1322 func loadPackages(ctx context.Context, modPath, modRoot, loadDir string, goModData, goSumData []byte, pkgPaths []string) (pkgs []*packages.Package, diagnostics []string, err error) { 1323 // Load packages. 1324 // TODO(jayconrod): if there are errors loading packages in the release 1325 // version, try loading in the release directory. Errors there would imply 1326 // that packages don't load without replace / exclude directives. 1327 cfg := &packages.Config{ 1328 Mode: packages.NeedName | packages.NeedTypes | packages.NeedImports | packages.NeedDeps, 1329 Dir: loadDir, 1330 Context: ctx, 1331 } 1332 cfg.Env = copyEnv(ctx, cfg.Env) 1333 if len(pkgPaths) > 0 { 1334 pkgs, err = packages.Load(cfg, pkgPaths...) 1335 if err != nil { 1336 return nil, nil, err 1337 } 1338 } 1339 1340 // Sort the returned packages by path. 1341 // packages.Load makes no guarantee about the order of returned packages. 1342 sort.Slice(pkgs, func(i, j int) bool { 1343 return pkgs[i].PkgPath < pkgs[j].PkgPath 1344 }) 1345 1346 // Trim modRoot from file paths in errors. 1347 prefix := modRoot + string(os.PathSeparator) 1348 for _, pkg := range pkgs { 1349 for i := range pkg.Errors { 1350 pkg.Errors[i].Pos = strings.TrimPrefix(pkg.Errors[i].Pos, prefix) 1351 } 1352 } 1353 1354 return pkgs, diagnostics, nil 1355 } 1356 1357 type packagePair struct { 1358 base, release *packages.Package 1359 } 1360 1361 // zipPackages combines two lists of packages, sorted by package path, 1362 // and returns a sorted list of pairs of packages with matching paths. 1363 // If a package is in one list but not the other (because it was added or 1364 // removed between releases), a pair will be returned with a nil 1365 // base or release field. 1366 func zipPackages(baseModPath string, basePkgs []*packages.Package, releaseModPath string, releasePkgs []*packages.Package) []packagePair { 1367 baseIndex, releaseIndex := 0, 0 1368 var pairs []packagePair 1369 for baseIndex < len(basePkgs) || releaseIndex < len(releasePkgs) { 1370 var basePkg, releasePkg *packages.Package 1371 var baseSuffix, releaseSuffix string 1372 if baseIndex < len(basePkgs) { 1373 basePkg = basePkgs[baseIndex] 1374 baseSuffix = trimPathPrefix(basePkg.PkgPath, baseModPath) 1375 } 1376 if releaseIndex < len(releasePkgs) { 1377 releasePkg = releasePkgs[releaseIndex] 1378 releaseSuffix = trimPathPrefix(releasePkg.PkgPath, releaseModPath) 1379 } 1380 1381 var pair packagePair 1382 if basePkg != nil && (releasePkg == nil || baseSuffix < releaseSuffix) { 1383 // Package removed 1384 pair = packagePair{basePkg, nil} 1385 baseIndex++ 1386 } else if releasePkg != nil && (basePkg == nil || releaseSuffix < baseSuffix) { 1387 // Package added 1388 pair = packagePair{nil, releasePkg} 1389 releaseIndex++ 1390 } else { 1391 // Matched packages. 1392 pair = packagePair{basePkg, releasePkg} 1393 baseIndex++ 1394 releaseIndex++ 1395 } 1396 pairs = append(pairs, pair) 1397 } 1398 return pairs 1399 } 1400 1401 // findSelectedVersion returns the highest version of the given modPath at 1402 // modDir, if a module cycle exists. modDir should be a writable directory 1403 // containing the go.mod for modPath. 1404 // 1405 // If no module cycle exists, it returns empty string. 1406 func findSelectedVersion(ctx context.Context, modDir, modPath string) (latestVersion string, err error) { 1407 defer func() { 1408 if err != nil { 1409 err = fmt.Errorf("could not find selected version for %s: %v", modPath, err) 1410 } 1411 }() 1412 1413 cmd := exec.CommandContext(ctx, "go", "list", "-m", "-f", "{{.Version}}", "--", modPath) 1414 cmd.Env = copyEnv(ctx, cmd.Env) 1415 cmd.Dir = modDir 1416 out, err := cmd.Output() 1417 if err != nil { 1418 return "", cleanCmdError(err) 1419 } 1420 return strings.TrimSpace(string(out)), nil 1421 } 1422 1423 func copyEnv(ctx context.Context, current []string) []string { 1424 env, ok := ctx.Value("env").([]string) 1425 if !ok { 1426 return current 1427 } 1428 clone := make([]string, len(env)) 1429 copy(clone, env) 1430 return clone 1431 } 1432 1433 // loadRetractions lists all retracted deps found at the modRoot. 1434 func loadRetractions(ctx context.Context, modRoot string) ([]string, error) { 1435 cmd := exec.CommandContext(ctx, "go", "list", "-json", "-m", "-u", "all") 1436 if env, ok := ctx.Value("env").([]string); ok { 1437 cmd.Env = env 1438 } 1439 cmd.Dir = modRoot 1440 out, err := cmd.Output() 1441 if err != nil { 1442 return nil, cleanCmdError(err) 1443 } 1444 1445 var retracted []string 1446 type message struct { 1447 Path string 1448 Version string 1449 Retracted []string 1450 } 1451 1452 dec := json.NewDecoder(bytes.NewBuffer(out)) 1453 for { 1454 var m message 1455 if err := dec.Decode(&m); err == io.EOF { 1456 break 1457 } else if err != nil { 1458 return nil, err 1459 } 1460 if len(m.Retracted) == 0 { 1461 continue 1462 } 1463 rationale, ok := shortRetractionRationale(m.Retracted) 1464 if ok { 1465 retracted = append(retracted, fmt.Sprintf("required module %s@%s retracted by module author: %s", m.Path, m.Version, rationale)) 1466 } else { 1467 retracted = append(retracted, fmt.Sprintf("required module %s@%s retracted by module author", m.Path, m.Version)) 1468 } 1469 } 1470 1471 return retracted, nil 1472 } 1473 1474 // shortRetractionRationale returns a retraction rationale string that is safe 1475 // to print in a terminal. It returns hard-coded strings if the rationale 1476 // is empty, too long, or contains non-printable characters. 1477 // 1478 // It returns true if the rationale was printable, and false if it was not (too 1479 // long, contains graphics, etc). 1480 func shortRetractionRationale(rationales []string) (string, bool) { 1481 if len(rationales) == 0 { 1482 return "", false 1483 } 1484 rationale := rationales[0] 1485 1486 const maxRationaleBytes = 500 1487 if i := strings.Index(rationale, "\n"); i >= 0 { 1488 rationale = rationale[:i] 1489 } 1490 rationale = strings.TrimSpace(rationale) 1491 if rationale == "" || rationale == "retracted by module author" { 1492 return "", false 1493 } 1494 if len(rationale) > maxRationaleBytes { 1495 return "", false 1496 } 1497 for _, r := range rationale { 1498 if !unicode.IsGraphic(r) && !unicode.IsSpace(r) { 1499 return "", false 1500 } 1501 } 1502 // NOTE: the go.mod parser rejects invalid UTF-8, so we don't check that here. 1503 return rationale, true 1504 } 1505 1506 // hasGitUncommittedChanges checks if the given directory has uncommitteed git 1507 // changes. 1508 func hasGitUncommittedChanges(dir string) (bool, error) { 1509 stdout := &bytes.Buffer{} 1510 cmd := exec.Command("git", "status", "--porcelain") 1511 cmd.Dir = dir 1512 cmd.Stdout = stdout 1513 if err := cmd.Run(); err != nil { 1514 return false, cleanCmdError(err) 1515 } 1516 return stdout.Len() != 0, nil 1517 }