cuelang.org/go@v0.13.0/internal/mod/modload/tidy.go (about) 1 package modload 2 3 import ( 4 "context" 5 "errors" 6 "fmt" 7 "io/fs" 8 "log" 9 "maps" 10 "path" 11 "runtime" 12 "slices" 13 "strings" 14 15 "cuelang.org/go/internal/buildattr" 16 "cuelang.org/go/internal/mod/modimports" 17 "cuelang.org/go/internal/mod/modpkgload" 18 "cuelang.org/go/internal/mod/modrequirements" 19 "cuelang.org/go/internal/mod/semver" 20 "cuelang.org/go/internal/par" 21 "cuelang.org/go/mod/modfile" 22 "cuelang.org/go/mod/module" 23 ) 24 25 const logging = false // TODO hook this up to CUE_DEBUG 26 27 // Registry is modload's view of a module registry. 28 type Registry interface { 29 modrequirements.Registry 30 modpkgload.Registry 31 // ModuleVersions returns all the versions for the module with the given path 32 // sorted in semver order. 33 // If mpath has a major version suffix, only versions with that major version will 34 // be returned. 35 ModuleVersions(ctx context.Context, mpath string) ([]string, error) 36 } 37 38 type loader struct { 39 mainModule module.Version 40 mainModuleLoc module.SourceLoc 41 registry Registry 42 checkTidy bool 43 } 44 45 // CheckTidy checks that the module file in the given main module is considered tidy. 46 // A module file is considered tidy when: 47 // - it can be parsed OK by [modfile.ParseStrict]. 48 // - it contains a language version in canonical semver form 49 // - it includes valid modules for all of its dependencies 50 // - it does not include any unnecessary dependencies. 51 func CheckTidy(ctx context.Context, fsys fs.FS, modRoot string, reg Registry) error { 52 _, err := tidy(ctx, fsys, modRoot, reg, true) 53 return err 54 } 55 56 // Tidy evaluates all the requirements of the given main module, using the given 57 // registry to download requirements and returns a resolved and tidied module file. 58 func Tidy(ctx context.Context, fsys fs.FS, modRoot string, reg Registry) (*modfile.File, error) { 59 return tidy(ctx, fsys, modRoot, reg, false) 60 } 61 62 func tidy(ctx context.Context, fsys fs.FS, modRoot string, reg Registry, checkTidy bool) (*modfile.File, error) { 63 mainModuleVersion, mf, err := readModuleFile(fsys, modRoot) 64 if err != nil { 65 return nil, err 66 } 67 // TODO check that module path is well formed etc 68 origRs := modrequirements.NewRequirements(mf.QualifiedModule(), reg, mf.DepVersions(), mf.DefaultMajorVersions()) 69 // Note: we can ignore build tags and the fact that we might 70 // have _tool.cue and _test.cue files, because we want to include 71 // all of those, but we do need to consider @ignore() attributes. 72 rootPkgPaths, err := modimports.AllImports(withoutIgnoredFiles(modimports.AllModuleFiles(fsys, modRoot))) 73 if err != nil { 74 return nil, err 75 } 76 ld := &loader{ 77 mainModule: mainModuleVersion, 78 registry: reg, 79 mainModuleLoc: module.SourceLoc{ 80 FS: fsys, 81 Dir: modRoot, 82 }, 83 checkTidy: checkTidy, 84 } 85 86 rs, pkgs, err := ld.resolveDependencies(ctx, rootPkgPaths, origRs) 87 if err != nil { 88 return nil, err 89 } 90 for _, pkg := range pkgs.All() { 91 if pkg.Error() != nil { 92 return nil, fmt.Errorf("failed to resolve %q: %v", pkg.ImportPath(), pkg.Error()) 93 } 94 } 95 // TODO check whether it's changed or not. 96 rs, err = ld.tidyRoots(ctx, rs, pkgs) 97 if err != nil { 98 return nil, fmt.Errorf("cannot tidy requirements: %v", err) 99 } 100 if ld.checkTidy && !equalRequirements(origRs, rs) { 101 // TODO: provide a reason, perhaps in structured form rather than a string 102 return nil, &ErrModuleNotTidy{} 103 } 104 return modfileFromRequirements(mf, rs), nil 105 } 106 107 // ErrModuleNotTidy is returned by CheckTidy when a module is not tidy, 108 // such as when there are missing or unnecessary dependencies listed. 109 type ErrModuleNotTidy struct { 110 // Reason summarizes why the module is not tidy. 111 Reason string 112 } 113 114 func (e ErrModuleNotTidy) Error() string { 115 if e.Reason == "" { 116 return "module is not tidy" 117 } 118 return "module is not tidy: " + e.Reason 119 } 120 121 func equalRequirements(rs0, rs1 *modrequirements.Requirements) bool { 122 // Note that rs1.RootModules may include the unversioned local module 123 // if the current module imports any packages under cue.mod/*/. 124 // In such a case we want to skip over the local module when comparing, 125 // just like modfileFromRequirements does when filling [modfile.File.Deps]. 126 // Note that we clone the slice to not modify rs1's internal slice in-place. 127 rs1RootMods := slices.DeleteFunc(slices.Clone(rs1.RootModules()), module.Version.IsLocal) 128 return slices.Equal(rs0.RootModules(), rs1RootMods) && 129 maps.Equal(rs0.DefaultMajorVersions(), rs1.DefaultMajorVersions()) 130 } 131 132 func readModuleFile(fsys fs.FS, modRoot string) (module.Version, *modfile.File, error) { 133 modFilePath := path.Join(modRoot, "cue.mod/module.cue") 134 data, err := fs.ReadFile(fsys, modFilePath) 135 if err != nil { 136 return module.Version{}, nil, fmt.Errorf("cannot read cue.mod file: %v", err) 137 } 138 mf, err := modfile.ParseNonStrict(data, modFilePath) 139 if err != nil { 140 return module.Version{}, nil, err 141 } 142 mainModuleVersion, err := module.NewVersion(mf.QualifiedModule(), "") 143 if err != nil { 144 return module.Version{}, nil, fmt.Errorf("%s: invalid module path: %v", modFilePath, err) 145 } 146 return mainModuleVersion, mf, nil 147 } 148 149 func modfileFromRequirements(old *modfile.File, rs *modrequirements.Requirements) *modfile.File { 150 // TODO it would be nice to have some way of automatically including new 151 // fields by default when they're added to modfile.File, but we don't 152 // want to just copy the entirety of old because that includes 153 // private fields too. 154 mf := &modfile.File{ 155 Module: old.Module, 156 Language: old.Language, 157 Deps: make(map[string]*modfile.Dep), 158 Source: old.Source, 159 Custom: old.Custom, 160 } 161 defaults := rs.DefaultMajorVersions() 162 for _, v := range rs.RootModules() { 163 if v.IsLocal() { 164 continue 165 } 166 mf.Deps[v.Path()] = &modfile.Dep{ 167 Version: v.Version(), 168 Default: defaults[v.BasePath()] == semver.Major(v.Version()), 169 } 170 } 171 return mf 172 } 173 174 // shouldIncludePkgFile reports whether a file from a package should be included 175 // for dependency-analysis purposes. 176 // 177 // In general a file should always be considered unless it's a _tool.cue file 178 // that's not in the main module. 179 func (ld *loader) shouldIncludePkgFile(pkgPath string, mod module.Version, fsys fs.FS, mf modimports.ModuleFile) (_ok bool) { 180 if buildattr.ShouldIgnoreFile(mf.Syntax) { 181 // The file is marked to be explicitly ignored. 182 return false 183 } 184 if mod.Path() == ld.mainModule.Path() { 185 // All files in the main module are considered. 186 return true 187 } 188 if strings.HasSuffix(mf.FilePath, "_tool.cue") || strings.HasSuffix(mf.FilePath, "_test.cue") { 189 // tool and test files are only considered when they are part of the main module. 190 return false 191 } 192 ok, _, err := buildattr.ShouldBuildFile(mf.Syntax, func(key string) bool { 193 // Keys of build attributes are considered always false when 194 // outside the main module. 195 return false 196 }) 197 if err != nil { 198 return false 199 } 200 return ok 201 } 202 203 func (ld *loader) resolveDependencies(ctx context.Context, rootPkgPaths []string, rs *modrequirements.Requirements) (*modrequirements.Requirements, *modpkgload.Packages, error) { 204 for { 205 logf("---- LOADING from requirements %q", rs.RootModules()) 206 pkgs := modpkgload.LoadPackages(ctx, ld.mainModule.Path(), ld.mainModuleLoc, rs, ld.registry, rootPkgPaths, ld.shouldIncludePkgFile) 207 if ld.checkTidy { 208 for _, pkg := range pkgs.All() { 209 err := pkg.Error() 210 if err == nil { 211 continue 212 } 213 missingErr := new(modpkgload.ImportMissingError) 214 // "cannot find module providing package P" is confusing here, 215 // as checkTidy simply points out missing dependencies without fetching them. 216 if errors.As(err, &missingErr) { 217 err = &ErrModuleNotTidy{Reason: fmt.Sprintf( 218 "missing dependency providing package %s", missingErr.Path)} 219 } 220 return nil, nil, err 221 } 222 // All packages could be loaded OK so there are no new 223 // dependencies to be resolved and nothing to do. 224 // Specifically, if there are no packages in error, then 225 // resolveMissingImports will never return any entries 226 // in modAddedBy and the default major versions won't 227 // change. 228 return rs, pkgs, nil 229 } 230 231 // TODO the original code calls updateRequirements at this point. 232 // /home/rogpeppe/go/src/cmd/go/internal/modload/load.go:1124 233 234 modAddedBy, defaultMajorVersions := ld.resolveMissingImports(ctx, pkgs, rs) 235 if !maps.Equal(defaultMajorVersions, rs.DefaultMajorVersions()) { 236 rs = rs.WithDefaultMajorVersions(defaultMajorVersions) 237 } 238 if len(modAddedBy) == 0 { 239 // The roots are stable, and we've resolved all of the missing packages 240 // that we can. 241 logf("dependencies are stable at %q", rs.RootModules()) 242 return rs, pkgs, nil 243 } 244 for m, p := range modAddedBy { 245 logf("added: %v (by %v)", m, p.ImportPath()) 246 } 247 toAdd := slices.SortedFunc(maps.Keys(modAddedBy), module.Version.Compare) // to make errors deterministic 248 oldRs := rs 249 var err error 250 rs, err = ld.updateRoots(ctx, rs, pkgs, toAdd) 251 if err != nil { 252 return nil, nil, err 253 } 254 if slices.Equal(rs.RootModules(), oldRs.RootModules()) { 255 // Something is deeply wrong. resolveMissingImports gave us a non-empty 256 // set of modules to add to the graph, but adding those modules had no 257 // effect — either they were already in the graph, or updateRoots did not 258 // add them as requested. 259 panic(fmt.Sprintf("internal error: adding %v to module graph had no effect on root requirements (%v)", toAdd, rs.RootModules())) 260 } 261 logf("after loading, requirements: %v", rs.RootModules()) 262 } 263 } 264 265 // updatePrunedRoots returns a set of root requirements that maintains the 266 // invariants of the cue.mod/module.cue file needed to support graph pruning: 267 // 268 // 1. The selected version of the module providing each package marked with 269 // either pkgInAll or pkgIsRoot is included as a root. 270 // Note that certain root patterns (such as '...') may explode the root set 271 // to contain every module that provides any package imported (or merely 272 // required) by any other module. 273 // 2. Each root appears only once, at the selected version of its path 274 // (if rs.graph is non-nil) or at the highest version otherwise present as a 275 // root (otherwise). 276 // 3. Every module path that appears as a root in rs remains a root. 277 // 4. Every version in add is selected at its given version unless upgraded by 278 // (the dependencies of) an existing root or another module in add. 279 // 280 // The packages in pkgs are assumed to have been loaded from either the roots of 281 // rs or the modules selected in the graph of rs. 282 // 283 // The above invariants together imply the graph-pruning invariants for the 284 // go.mod file: 285 // 286 // 1. (The import invariant.) Every module that provides a package transitively 287 // imported by any package or test in the main module is included as a root. 288 // This follows by induction from (1) and (3) above. Transitively-imported 289 // packages loaded during this invocation are marked with pkgInAll (1), 290 // and by hypothesis any transitively-imported packages loaded in previous 291 // invocations were already roots in rs (3). 292 // 293 // 2. (The argument invariant.) Every module that provides a package matching 294 // an explicit package pattern is included as a root. This follows directly 295 // from (1): packages matching explicit package patterns are marked with 296 // pkgIsRoot. 297 // 298 // 3. (The completeness invariant.) Every module that contributed any package 299 // to the build is required by either the main module or one of the modules 300 // it requires explicitly. This invariant is left up to the caller, who must 301 // not load packages from outside the module graph but may add roots to the 302 // graph, but is facilitated by (3). If the caller adds roots to the graph in 303 // order to resolve missing packages, then updatePrunedRoots will retain them, 304 // the selected versions of those roots cannot regress, and they will 305 // eventually be written back to the main module's go.mod file. 306 // 307 // (See https://golang.org/design/36460-lazy-module-loading#invariants for more 308 // detail.) 309 func (ld *loader) updateRoots(ctx context.Context, rs *modrequirements.Requirements, pkgs *modpkgload.Packages, add []module.Version) (*modrequirements.Requirements, error) { 310 roots := rs.RootModules() 311 rootsUpgraded := false 312 313 spotCheckRoot := map[module.Version]bool{} 314 315 // “The selected version of the module providing each package marked with 316 // either pkgInAll or pkgIsRoot is included as a root.” 317 needSort := false 318 for _, pkg := range pkgs.All() { 319 if !pkg.Mod().IsValid() || !pkg.FromExternalModule() { 320 // pkg was not loaded from a module dependency, so we don't need 321 // to do anything special to maintain that dependency. 322 continue 323 } 324 325 switch { 326 case pkg.HasFlags(modpkgload.PkgInAll): 327 // pkg is transitively imported by a package or test in the main module. 328 // We need to promote the module that maintains it to a root: if some 329 // other module depends on the main module, and that other module also 330 // uses a pruned module graph, it will expect to find all of our 331 // transitive dependencies by reading just our go.mod file, not the go.mod 332 // files of everything we depend on. 333 // 334 // (This is the “import invariant” that makes graph pruning possible.) 335 336 case pkg.HasFlags(modpkgload.PkgIsRoot): 337 // pkg is a root of the package-import graph. (Generally this means that 338 // it matches a command-line argument.) We want future invocations of the 339 // 'go' command — such as 'go test' on the same package — to continue to 340 // use the same versions of its dependencies that we are using right now. 341 // So we need to bring this package's dependencies inside the pruned 342 // module graph. 343 // 344 // Making the module containing this package a root of the module graph 345 // does exactly that: if the module containing the package supports graph 346 // pruning then it should satisfy the import invariant itself, so all of 347 // its dependencies should be in its go.mod file, and if the module 348 // containing the package does not support pruning then if we make it a 349 // root we will load all of its (unpruned) transitive dependencies into 350 // the module graph. 351 // 352 // (This is the “argument invariant”, and is important for 353 // reproducibility.) 354 355 default: 356 // pkg is a dependency of some other package outside of the main module. 357 // As far as we know it's not relevant to the main module (and thus not 358 // relevant to consumers of the main module either), and its dependencies 359 // should already be in the module graph — included in the dependencies of 360 // the package that imported it. 361 continue 362 } 363 if _, ok := rs.RootSelected(pkg.Mod().Path()); ok { 364 // It is possible that the main module's go.mod file is incomplete or 365 // otherwise erroneous — for example, perhaps the author forgot to 'git 366 // add' their updated go.mod file after adding a new package import, or 367 // perhaps they made an edit to the go.mod file using a third-party tool 368 // ('git merge'?) that doesn't maintain consistency for module 369 // dependencies. If that happens, ideally we want to detect the missing 370 // requirements and fix them up here. 371 // 372 // However, we also need to be careful not to be too aggressive. For 373 // transitive dependencies of external tests, the go.mod file for the 374 // module containing the test itself is expected to provide all of the 375 // relevant dependencies, and we explicitly don't want to pull in 376 // requirements on *irrelevant* requirements that happen to occur in the 377 // go.mod files for these transitive-test-only dependencies. (See the test 378 // in mod_lazy_test_horizon.txt for a concrete example). 379 // 380 // The “goldilocks zone” seems to be to spot-check exactly the same 381 // modules that we promote to explicit roots: namely, those that provide 382 // packages transitively imported by the main module, and those that 383 // provide roots of the package-import graph. That will catch erroneous 384 // edits to the main module's go.mod file and inconsistent requirements in 385 // dependencies that provide imported packages, but will ignore erroneous 386 // or misleading requirements in dependencies that aren't obviously 387 // relevant to the packages in the main module. 388 spotCheckRoot[pkg.Mod()] = true 389 } else { 390 roots = append(roots, pkg.Mod()) 391 rootsUpgraded = true 392 // The roots slice was initially sorted because rs.rootModules was sorted, 393 // but the root we just added could be out of order. 394 needSort = true 395 } 396 } 397 398 for _, m := range add { 399 if !m.IsValid() { 400 panic("add contains invalid module") 401 } 402 if v, ok := rs.RootSelected(m.Path()); !ok || semver.Compare(v, m.Version()) < 0 { 403 roots = append(roots, m) 404 rootsUpgraded = true 405 needSort = true 406 } 407 } 408 if needSort { 409 slices.SortFunc(roots, module.Version.Compare) 410 } 411 412 // "Each root appears only once, at the selected version of its path ….” 413 for { 414 var mg *modrequirements.ModuleGraph 415 if rootsUpgraded { 416 // We've added or upgraded one or more roots, so load the full module 417 // graph so that we can update those roots to be consistent with other 418 // requirements. 419 420 rs = modrequirements.NewRequirements(ld.mainModule.Path(), ld.registry, roots, rs.DefaultMajorVersions()) 421 var err error 422 mg, err = rs.Graph(ctx) 423 if err != nil { 424 return rs, err 425 } 426 } else { 427 // Since none of the roots have been upgraded, we have no reason to 428 // suspect that they are inconsistent with the requirements of any other 429 // roots. Only look at the full module graph if we've already loaded it; 430 // otherwise, just spot-check the explicit requirements of the roots from 431 // which we loaded packages. 432 if rs.GraphIsLoaded() { 433 // We've already loaded the full module graph, which includes the 434 // requirements of all of the root modules — even the transitive 435 // requirements, if they are unpruned! 436 mg, _ = rs.Graph(ctx) 437 } else if !ld.spotCheckRoots(ctx, rs, spotCheckRoot) { 438 // We spot-checked the explicit requirements of the roots that are 439 // relevant to the packages we've loaded. Unfortunately, they're 440 // inconsistent in some way; we need to load the full module graph 441 // so that we can fix the roots properly. 442 var err error 443 mg, err = rs.Graph(ctx) 444 if err != nil { 445 return rs, err 446 } 447 } 448 } 449 450 roots = make([]module.Version, 0, len(rs.RootModules())) 451 rootsUpgraded = false 452 inRootPaths := map[string]bool{ 453 ld.mainModule.Path(): true, 454 } 455 for _, m := range rs.RootModules() { 456 if inRootPaths[m.Path()] { 457 // This root specifies a redundant path. We already retained the 458 // selected version of this path when we saw it before, so omit the 459 // redundant copy regardless of its version. 460 // 461 // When we read the full module graph, we include the dependencies of 462 // every root even if that root is redundant. That better preserves 463 // reproducibility if, say, some automated tool adds a redundant 464 // 'require' line and then runs 'go mod tidy' to try to make everything 465 // consistent, since the requirements of the older version are carried 466 // over. 467 // 468 // So omitting a root that was previously present may *reduce* the 469 // selected versions of non-roots, but merely removing a requirement 470 // cannot *increase* the selected versions of other roots as a result — 471 // we don't need to mark this change as an upgrade. (This particular 472 // change cannot invalidate any other roots.) 473 continue 474 } 475 476 var v string 477 if mg == nil { 478 v, _ = rs.RootSelected(m.Path()) 479 } else { 480 v = mg.Selected(m.Path()) 481 } 482 mv, err := module.NewVersion(m.Path(), v) 483 if err != nil { 484 return nil, fmt.Errorf("internal error: cannot form module version from %q@%q", m.Path(), v) 485 } 486 roots = append(roots, mv) 487 inRootPaths[m.Path()] = true 488 if v != m.Version() { 489 rootsUpgraded = true 490 } 491 } 492 // Note that rs.rootModules was already sorted by module path and version, 493 // and we appended to the roots slice in the same order and guaranteed that 494 // each path has only one version, so roots is also sorted by module path 495 // and (trivially) version. 496 497 if !rootsUpgraded { 498 // The root set has converged: every root going into this iteration was 499 // already at its selected version, although we have have removed other 500 // (redundant) roots for the same path. 501 break 502 } 503 } 504 505 if slices.Equal(roots, rs.RootModules()) { 506 // The root set is unchanged and rs was already pruned, so keep rs to 507 // preserve its cached ModuleGraph (if any). 508 return rs, nil 509 } 510 return modrequirements.NewRequirements(ld.mainModule.Path(), ld.registry, roots, rs.DefaultMajorVersions()), nil 511 } 512 513 // resolveMissingImports returns a set of modules that could be added as 514 // dependencies in order to resolve missing packages from pkgs. 515 // 516 // It returns a map from each new module version to 517 // the first missing package that module would resolve. 518 func (ld *loader) resolveMissingImports(ctx context.Context, pkgs *modpkgload.Packages, rs *modrequirements.Requirements) (modAddedBy map[module.Version]*modpkgload.Package, defaultMajorVersions map[string]string) { 519 type pkgMod struct { 520 pkg *modpkgload.Package 521 needsDefault *bool 522 mods *[]module.Version 523 } 524 var pkgMods []pkgMod 525 work := par.NewQueue(runtime.GOMAXPROCS(0)) 526 for _, pkg := range pkgs.All() { 527 if pkg.Error() == nil { 528 continue 529 } 530 if !errors.As(pkg.Error(), new(*modpkgload.ImportMissingError)) { 531 // Leave other errors to be reported outside of the module resolution logic. 532 continue 533 } 534 logf("querying %q", pkg.ImportPath()) 535 var mods []module.Version // updated asynchronously. 536 var needsDefault bool 537 work.Add(func() { 538 var err error 539 mods, needsDefault, err = ld.queryImport(ctx, pkg.ImportPath(), rs) 540 if err != nil { 541 // pkg.err was already non-nil, so we can reasonably attribute the error 542 // for pkg to either the original error or the one returned by 543 // queryImport. The existing error indicates only that we couldn't find 544 // the package, whereas the query error also explains why we didn't fix 545 // the problem — so we prefer the latter. 546 pkg.SetError(err) 547 } 548 549 // err is nil, but we intentionally leave pkg.err non-nil: we still haven't satisfied other invariants of a 550 // successfully-loaded package, such as scanning and loading the imports 551 // of that package. If we succeed in resolving the new dependency graph, 552 // the caller can reload pkg and update the error at that point. 553 // 554 // Even then, the package might not be loaded from the version we've 555 // identified here. The module may be upgraded by some other dependency, 556 // or by a transitive dependency of mod itself, or — less likely — the 557 // package may be rejected by an AllowPackage hook or rendered ambiguous 558 // by some other newly-added or newly-upgraded dependency. 559 }) 560 561 pkgMods = append(pkgMods, pkgMod{pkg: pkg, mods: &mods, needsDefault: &needsDefault}) 562 } 563 <-work.Idle() 564 565 modAddedBy = map[module.Version]*modpkgload.Package{} 566 defaultMajorVersions = maps.Clone(rs.DefaultMajorVersions()) 567 for _, pm := range pkgMods { 568 pkg, mods, needsDefault := pm.pkg, *pm.mods, *pm.needsDefault 569 for _, mod := range mods { 570 // TODO support logging progress messages like this but without printing to stderr? 571 logf("cue: found potential %s in %v", pkg.ImportPath(), mod) 572 if modAddedBy[mod] == nil { 573 modAddedBy[mod] = pkg 574 } 575 if needsDefault { 576 defaultMajorVersions[mod.BasePath()] = semver.Major(mod.Version()) 577 } 578 } 579 } 580 581 return modAddedBy, defaultMajorVersions 582 } 583 584 // tidyRoots returns a minimal set of root requirements that maintains the 585 // invariants of the cue.mod/module.cue file needed to support graph pruning for the given 586 // packages: 587 // 588 // 1. For each package marked with PkgInAll, the module path that provided that 589 // package is included as a root. 590 // 2. For all packages, the module that provided that package either remains 591 // selected at the same version or is upgraded by the dependencies of a 592 // root. 593 // 594 // If any module that provided a package has been upgraded above its previous 595 // version, the caller may need to reload and recompute the package graph. 596 // 597 // To ensure that the loading process eventually converges, the caller should 598 // add any needed roots from the tidy root set (without removing existing untidy 599 // roots) until the set of roots has converged. 600 func (ld *loader) tidyRoots(ctx context.Context, old *modrequirements.Requirements, pkgs *modpkgload.Packages) (*modrequirements.Requirements, error) { 601 var ( 602 roots []module.Version 603 pathIsRoot = map[string]bool{ld.mainModule.Path(): true} 604 ) 605 // We start by adding roots for every package in "all". 606 // 607 // Once that is done, we may still need to add more roots to cover upgraded or 608 // otherwise-missing test dependencies for packages in "all". For those test 609 // dependencies, we prefer to add roots for packages with shorter import 610 // stacks first, on the theory that the module requirements for those will 611 // tend to fill in the requirements for their transitive imports (which have 612 // deeper import stacks). So we add the missing dependencies for one depth at 613 // a time, starting with the packages actually in "all" and expanding outwards 614 // until we have scanned every package that was loaded. 615 var ( 616 queue []*modpkgload.Package 617 queued = map[*modpkgload.Package]bool{} 618 ) 619 for _, pkg := range pkgs.All() { 620 if !pkg.HasFlags(modpkgload.PkgInAll) { 621 continue 622 } 623 if pkg.FromExternalModule() && !pathIsRoot[pkg.Mod().Path()] { 624 roots = append(roots, pkg.Mod()) 625 pathIsRoot[pkg.Mod().Path()] = true 626 } 627 queue = append(queue, pkg) 628 queued[pkg] = true 629 } 630 slices.SortFunc(roots, module.Version.Compare) 631 tidy := modrequirements.NewRequirements(ld.mainModule.Path(), ld.registry, roots, old.DefaultMajorVersions()) 632 633 for len(queue) > 0 { 634 roots = tidy.RootModules() 635 mg, err := tidy.Graph(ctx) 636 if err != nil { 637 return nil, err 638 } 639 640 prevQueue := queue 641 queue = nil 642 for _, pkg := range prevQueue { 643 m := pkg.Mod() 644 if m.Path() == "" { 645 continue 646 } 647 for _, dep := range pkg.Imports() { 648 if !queued[dep] { 649 queue = append(queue, dep) 650 queued[dep] = true 651 } 652 } 653 if !pathIsRoot[m.Path()] { 654 if s := mg.Selected(m.Path()); semver.Compare(s, m.Version()) < 0 { 655 roots = append(roots, m) 656 pathIsRoot[m.Path()] = true 657 } 658 } 659 } 660 661 if tidyRoots := tidy.RootModules(); len(roots) > len(tidyRoots) { 662 slices.SortFunc(roots, module.Version.Compare) 663 tidy = modrequirements.NewRequirements(ld.mainModule.Path(), ld.registry, roots, tidy.DefaultMajorVersions()) 664 } 665 } 666 667 if _, err := tidy.Graph(ctx); err != nil { 668 return nil, err 669 } 670 671 // TODO the original code had some logic I don't properly understand, 672 // related to https://go.dev/issue/60313, that _may_ be relevant only 673 // to test-only dependencies, which we don't have, so leave it out for now. 674 675 return tidy, nil 676 } 677 678 // spotCheckRoots reports whether the versions of the roots in rs satisfy the 679 // explicit requirements of the modules in mods. 680 func (ld *loader) spotCheckRoots(ctx context.Context, rs *modrequirements.Requirements, mods map[module.Version]bool) bool { 681 ctx, cancel := context.WithCancel(ctx) 682 defer cancel() 683 684 work := par.NewQueue(runtime.GOMAXPROCS(0)) 685 for m := range mods { 686 work.Add(func() { 687 if ctx.Err() != nil { 688 return 689 } 690 691 require, err := ld.registry.Requirements(ctx, m) 692 if err != nil { 693 cancel() 694 return 695 } 696 697 for _, r := range require { 698 if v, ok := rs.RootSelected(r.Path()); ok && semver.Compare(v, r.Version()) < 0 { 699 cancel() 700 return 701 } 702 } 703 }) 704 } 705 <-work.Idle() 706 707 if ctx.Err() != nil { 708 // Either we failed a spot-check, or the caller no longer cares about our 709 // answer anyway. 710 return false 711 } 712 713 return true 714 } 715 716 func withoutIgnoredFiles(iter func(func(modimports.ModuleFile, error) bool)) func(func(modimports.ModuleFile, error) bool) { 717 return func(yield func(modimports.ModuleFile, error) bool) { 718 // TODO for mf, err := range iter { 719 iter(func(mf modimports.ModuleFile, err error) bool { 720 if err == nil && buildattr.ShouldIgnoreFile(mf.Syntax) { 721 return true 722 } 723 return yield(mf, err) 724 }) 725 } 726 } 727 728 func logf(f string, a ...any) { 729 if logging { 730 log.Printf(f, a...) 731 } 732 }