Skip to content

Commit 7dedc23

Browse files
author
Bryan C. Mills
committed
cmd/go: smooth out upgrade paths for lazy loading
This change adds two possible upgrade paths for lazy loading: 1. Run 'go mod tidy -go=1.17'. 2. Starting in a module with no existing 'go' directive, run any 'go' command that updates the go.mod file. In the latter case, commands other than 'go mod tidy' may leave the go.mod file *very* untidy if it had non-trivial dependencies. (The 'go' invocation will promote all implicit eager dependencies to explicit lazy ones, which preserves the original module graph — most of which is not actually relevant.) 'go mod tidy -go=1.17' can be used to enable lazy loading without accidentally downgrading existing transitive dependencies. 'go mod tidy -go=1.16' can be used to disable lazy loading and clear away redundant roots in a single step (if reducing the go version), or to prune away dependencies of tests-of-external-tests (if increasing the go version). 'go mod tidy -go=1.15' can be used to add dependencies of tests-of-external-tests, although there isn't much point to that. DO NOT MERGE This change still needs an explicit test and a release note. Fixes #45094 For #36460 Change-Id: I68f057e39489dfd6a667cd11dc1e320c1ee1aec1 Reviewed-on: https://go-review.googlesource.com/c/go/+/315210 Trust: Bryan C. Mills <[email protected]> Run-TryBot: Bryan C. Mills <[email protected]> TryBot-Result: Go Bot <[email protected]> Reviewed-by: Michael Matloob <[email protected]>
1 parent 0e315ad commit 7dedc23

14 files changed

+479
-105
lines changed

doc/go1.17.html

+11
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,17 @@ <h4 id="lazy-loading">Lazy module loading</h4>
5858
<!-- TODO(bcmills): replace the design-doc link with proper documentation. -->
5959
</p>
6060

61+
<p><!-- golang.org/issue/45094 --> To facilitate the upgrade to lazy loading,
62+
the <code>go</code> <code>mod</code> <code>tidy</code> subcommand now supports
63+
a <code>-go</code> flag to set or change the <code>go</code> version in
64+
the <code>go.mod</code> file. To enable lazy loading for an existing module
65+
without changing the selected versions of its dependencies, run:
66+
</p>
67+
68+
<pre>
69+
go mod tidy -go=1.17
70+
</pre>
71+
6172
<h4 id="module-deprecation-comments">Module deprecation comments</h4>
6273

6374
<p><!-- golang.org/issue/40357 -->

src/cmd/go/alldocs.go

+7-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/cmd/go/internal/modcmd/tidy.go

+21-2
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,12 @@ import (
1212
"cmd/go/internal/imports"
1313
"cmd/go/internal/modload"
1414
"context"
15+
16+
"golang.org/x/mod/modfile"
1517
)
1618

1719
var cmdTidy = &base.Command{
18-
UsageLine: "go mod tidy [-e] [-v]",
20+
UsageLine: "go mod tidy [-e] [-v] [-go=version]",
1921
Short: "add missing and remove unused modules",
2022
Long: `
2123
Tidy makes sure go.mod matches the source code in the module.
@@ -30,16 +32,26 @@ to standard error.
3032
The -e flag causes tidy to attempt to proceed despite errors
3133
encountered while loading packages.
3234
35+
The -go flag causes tidy to update the 'go' directive in the go.mod
36+
file to the given version, which may change which module dependencies
37+
are retained as explicit requirements in the go.mod file.
38+
(Go versions 1.17 and higher retain more requirements in order to
39+
support lazy module loading.)
40+
3341
See https://golang.org/ref/mod#go-mod-tidy for more about 'go mod tidy'.
3442
`,
3543
Run: runTidy,
3644
}
3745

38-
var tidyE bool // if true, report errors but proceed anyway.
46+
var (
47+
tidyE bool // if true, report errors but proceed anyway.
48+
tidyGo string // go version to write to the tidied go.mod file (toggles lazy loading)
49+
)
3950

4051
func init() {
4152
cmdTidy.Flag.BoolVar(&cfg.BuildV, "v", false, "")
4253
cmdTidy.Flag.BoolVar(&tidyE, "e", false, "")
54+
cmdTidy.Flag.StringVar(&tidyGo, "go", "", "")
4355
base.AddModCommonFlags(&cmdTidy.Flag)
4456
}
4557

@@ -48,6 +60,12 @@ func runTidy(ctx context.Context, cmd *base.Command, args []string) {
4860
base.Fatalf("go mod tidy: no arguments allowed")
4961
}
5062

63+
if tidyGo != "" {
64+
if !modfile.GoVersionRE.MatchString(tidyGo) {
65+
base.Fatalf(`go mod: invalid -go option %q; expecting something like "-go 1.17"`, tidyGo)
66+
}
67+
}
68+
5169
// Tidy aims to make 'go test' reproducible for any package in 'all', so we
5270
// need to include test dependencies. For modules that specify go 1.15 or
5371
// earlier this is a no-op (because 'all' saturates transitive test
@@ -62,6 +80,7 @@ func runTidy(ctx context.Context, cmd *base.Command, args []string) {
6280
modload.RootMode = modload.NeedRoot
6381

6482
modload.LoadPackages(ctx, modload.PackageOpts{
83+
GoVersion: tidyGo,
6584
Tags: imports.AnyTags(),
6685
Tidy: true,
6786
VendorModulesInGOROOTSrc: true,

src/cmd/go/internal/modload/build.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -259,8 +259,8 @@ func moduleInfo(ctx context.Context, rs *Requirements, m module.Version, mode Li
259259
if m.GoVersion == "" && checksumOk("/go.mod") {
260260
// Load the go.mod file to determine the Go version, since it hasn't
261261
// already been populated from rawGoVersion.
262-
if summary, err := rawGoModSummary(mod); err == nil && summary.goVersionV != "" {
263-
m.GoVersion = summary.goVersionV[1:]
262+
if summary, err := rawGoModSummary(mod); err == nil && summary.goVersion != "" {
263+
m.GoVersion = summary.goVersion
264264
}
265265
}
266266

src/cmd/go/internal/modload/buildlist.go

+37-7
Original file line numberDiff line numberDiff line change
@@ -299,7 +299,7 @@ func readModGraph(ctx context.Context, depth modDepth, roots []module.Version) (
299299
// sufficient to build the packages it contains. We must load its full
300300
// transitive dependency graph to be sure that we see all relevant
301301
// dependencies.
302-
if depth == eager || summary.depth() == eager {
302+
if depth == eager || summary.depth == eager {
303303
for _, r := range summary.require {
304304
enqueue(r, eager)
305305
}
@@ -393,7 +393,7 @@ func LoadModGraph(ctx context.Context) *ModuleGraph {
393393
base.Fatalf("go: %v", err)
394394
}
395395

396-
commitRequirements(ctx, rs)
396+
commitRequirements(ctx, modFileGoVersion(), rs)
397397
return mg
398398
}
399399

@@ -459,7 +459,7 @@ func EditBuildList(ctx context.Context, add, mustSelect []module.Version) (chang
459459
if err != nil {
460460
return false, err
461461
}
462-
commitRequirements(ctx, rs)
462+
commitRequirements(ctx, modFileGoVersion(), rs)
463463
return changed, err
464464
}
465465

@@ -943,10 +943,40 @@ func updateEagerRoots(ctx context.Context, direct map[string]bool, rs *Requireme
943943
if err != nil {
944944
return rs, err
945945
}
946-
if reflect.DeepEqual(min, rs.rootModules) && reflect.DeepEqual(direct, rs.direct) {
947-
// The root set is unchanged, so keep rs to preserve its cached ModuleGraph
948-
// (if any).
946+
if rs.depth == eager && reflect.DeepEqual(min, rs.rootModules) && reflect.DeepEqual(direct, rs.direct) {
947+
// The root set is unchanged and rs was already eager, so keep rs to
948+
// preserve its cached ModuleGraph (if any).
949949
return rs, nil
950950
}
951-
return newRequirements(rs.depth, min, direct), nil
951+
return newRequirements(eager, min, direct), nil
952+
}
953+
954+
// convertDepth returns a version of rs with the given depth.
955+
// If rs already has the given depth, convertDepth returns rs unmodified.
956+
func convertDepth(ctx context.Context, rs *Requirements, depth modDepth) (*Requirements, error) {
957+
if rs.depth == depth {
958+
return rs, nil
959+
}
960+
961+
if depth == eager {
962+
// We are converting a lazy module to an eager one. The roots of an eager
963+
// module graph are a superset of the roots of a lazy graph, so we don't
964+
// need to add any new roots — we just need to prune away the ones that are
965+
// redundant given eager loading, which is exactly what updateEagerRoots
966+
// does.
967+
return updateEagerRoots(ctx, rs.direct, rs, nil)
968+
}
969+
970+
// We are converting an eager module to a lazy one. The module graph of an
971+
// eager module includes the transitive dependencies of every module in the
972+
// build list.
973+
//
974+
// Hey, we can express that as a lazy root set! “Include the transitive
975+
// dependencies of every module in the build list” is exactly what happens in
976+
// a lazy module if we promote every module in the build list to a root!
977+
mg, err := rs.Graph(ctx)
978+
if err != nil {
979+
return rs, err
980+
}
981+
return newRequirements(lazy, mg.BuildList()[1:], rs.direct), nil
952982
}

src/cmd/go/internal/modload/edit.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -229,7 +229,7 @@ func raiseLimitsForUpgrades(ctx context.Context, maxVersion map[string]string, d
229229
if err != nil {
230230
return err
231231
}
232-
if summary.depth() == eager {
232+
if summary.depth == eager {
233233
// For efficiency, we'll load all of the eager upgrades as one big
234234
// graph, rather than loading the (potentially-overlapping) subgraph for
235235
// each upgrade individually.
@@ -522,7 +522,7 @@ func (l *versionLimiter) check(m module.Version, depth modDepth) dqState {
522522
return l.disqualify(m, dqState{err: err})
523523
}
524524

525-
if summary.depth() == eager {
525+
if summary.depth == eager {
526526
depth = eager
527527
}
528528
for _, r := range summary.require {

src/cmd/go/internal/modload/init.go

+33-32
Original file line numberDiff line numberDiff line change
@@ -381,7 +381,7 @@ var errGoModDirty error = goModDirtyError{}
381381
func LoadModFile(ctx context.Context) *Requirements {
382382
rs, needCommit := loadModFile(ctx)
383383
if needCommit {
384-
commitRequirements(ctx, rs)
384+
commitRequirements(ctx, modFileGoVersion(), rs)
385385
}
386386
return rs
387387
}
@@ -401,8 +401,9 @@ func loadModFile(ctx context.Context) (rs *Requirements, needCommit bool) {
401401
if modRoot == "" {
402402
Target = module.Version{Path: "command-line-arguments"}
403403
targetPrefix = "command-line-arguments"
404-
rawGoVersion.Store(Target, latestGoVersion())
405-
requirements = newRequirements(index.depth(), nil, nil)
404+
goVersion := latestGoVersion()
405+
rawGoVersion.Store(Target, goVersion)
406+
requirements = newRequirements(modDepthFromGoVersion(goVersion), nil, nil)
406407
return requirements, false
407408
}
408409

@@ -432,35 +433,31 @@ func loadModFile(ctx context.Context) (rs *Requirements, needCommit bool) {
432433
}
433434

434435
setDefaultBuildMod() // possibly enable automatic vendoring
435-
rs = requirementsFromModFile(ctx, f)
436+
rs = requirementsFromModFile(ctx)
436437

437438
if cfg.BuildMod == "vendor" {
438439
readVendorList()
439440
checkVendorConsistency()
440441
rs.initVendor(vendorList)
441442
}
442443
if index.goVersionV == "" {
443-
// The main module necessarily has a go.mod file, and that file lacks a
444-
// 'go' directive. The 'go' command has been adding that directive
445-
// automatically since Go 1.12, so this module either dates to Go 1.11 or
446-
// has been erroneously hand-edited.
447-
//
448-
// The semantics of the go.mod file are more-or-less the same from Go 1.11
449-
// through Go 1.16, changing at 1.17 for lazy loading. So even though a
450-
// go.mod file without a 'go' directive is theoretically a Go 1.11 file,
451-
// scripts may assume that it ends up as a Go 1.16 module. We can't go
452-
// higher than that, because we don't know which semantics the user intends.
453-
//
454-
// (Note that 'go mod init' always adds the latest version, so scripts that
455-
// use 'go mod init' will result in current-version modules instead of Go
456-
// 1.16 modules.)
457-
//
458-
// If we are able to modify the go.mod file, we will add a 'go' directive
459-
// to at least make the situation explicit going forward.
460-
if cfg.BuildMod == "mod" {
461-
addGoStmt("1.16")
444+
// TODO(#45551): Do something more principled instead of checking
445+
// cfg.CmdName directly here.
446+
if cfg.BuildMod == "mod" && cfg.CmdName != "mod graph" && cfg.CmdName != "mod why" {
447+
addGoStmt(latestGoVersion())
448+
if go117EnableLazyLoading {
449+
// We need to add a 'go' version to the go.mod file, but we must assume
450+
// that its existing contents match something between Go 1.11 and 1.16.
451+
// Go 1.11 through 1.16 have eager requirements, but the latest Go
452+
// version uses lazy requirements instead — so we need to cnvert the
453+
// requirements to be lazy.
454+
rs, err = convertDepth(ctx, rs, lazy)
455+
if err != nil {
456+
base.Fatalf("go: %v", err)
457+
}
458+
}
462459
} else {
463-
rawGoVersion.Store(Target, "1.16")
460+
rawGoVersion.Store(Target, modFileGoVersion())
464461
}
465462
}
466463

@@ -509,7 +506,7 @@ func CreateModFile(ctx context.Context, modPath string) {
509506
base.Fatalf("go: %v", err)
510507
}
511508

512-
commitRequirements(ctx, requirementsFromModFile(ctx, modFile))
509+
commitRequirements(ctx, modFileGoVersion(), requirementsFromModFile(ctx))
513510

514511
// Suggest running 'go mod tidy' unless the project is empty. Even if we
515512
// imported all the correct requirements above, we're probably missing
@@ -661,12 +658,13 @@ func initTarget(m module.Version) {
661658
}
662659
}
663660

664-
// requirementsFromModFile returns the set of non-excluded requirements from f.
665-
func requirementsFromModFile(ctx context.Context, f *modfile.File) *Requirements {
666-
roots := make([]module.Version, 0, len(f.Require))
661+
// requirementsFromModFile returns the set of non-excluded requirements from
662+
// the global modFile.
663+
func requirementsFromModFile(ctx context.Context) *Requirements {
664+
roots := make([]module.Version, 0, len(modFile.Require))
667665
mPathCount := map[string]int{Target.Path: 1}
668666
direct := map[string]bool{}
669-
for _, r := range f.Require {
667+
for _, r := range modFile.Require {
670668
if index != nil && index.exclude[r.Mod] {
671669
if cfg.BuildMod == "mod" {
672670
fmt.Fprintf(os.Stderr, "go: dropping requirement on excluded version %s %s\n", r.Mod.Path, r.Mod.Version)
@@ -683,7 +681,7 @@ func requirementsFromModFile(ctx context.Context, f *modfile.File) *Requirements
683681
}
684682
}
685683
module.Sort(roots)
686-
rs := newRequirements(index.depth(), roots, direct)
684+
rs := newRequirements(modDepthFromGoVersion(modFileGoVersion()), roots, direct)
687685

688686
// If any module path appears more than once in the roots, we know that the
689687
// go.mod file needs to be updated even though we have not yet loaded any
@@ -988,12 +986,12 @@ func WriteGoMod(ctx context.Context) {
988986
if !allowWriteGoMod {
989987
panic("WriteGoMod called while disallowed")
990988
}
991-
commitRequirements(ctx, LoadModFile(ctx))
989+
commitRequirements(ctx, modFileGoVersion(), LoadModFile(ctx))
992990
}
993991

994992
// commitRequirements writes sets the global requirements variable to rs and
995993
// writes its contents back to the go.mod file on disk.
996-
func commitRequirements(ctx context.Context, rs *Requirements) {
994+
func commitRequirements(ctx context.Context, goVersion string, rs *Requirements) {
997995
requirements = rs
998996

999997
if !allowWriteGoMod {
@@ -1014,6 +1012,9 @@ func commitRequirements(ctx context.Context, rs *Requirements) {
10141012
})
10151013
}
10161014
modFile.SetRequire(list)
1015+
if goVersion != "" {
1016+
modFile.AddGoStmt(goVersion)
1017+
}
10171018
modFile.Cleanup()
10181019

10191020
dirty := index.modFileIsDirty(modFile)

src/cmd/go/internal/modload/list.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ func ListModules(ctx context.Context, args []string, mode ListMode) ([]*modinfo.
7272
}
7373

7474
if err == nil {
75-
commitRequirements(ctx, rs)
75+
commitRequirements(ctx, modFileGoVersion(), rs)
7676
}
7777
return mods, err
7878
}

0 commit comments

Comments
 (0)