Skip to content

Commit 1736f3a

Browse files
author
Bryan C. Mills
committed
cmd/go: automatically check and use vendored packages
This implements the proposal described in https://golang.org/issue/33848#issuecomment-537222782. Fixes #33848 Change-Id: Ia34d6500ca396b6aa644b920233716c6b83ef729 Reviewed-on: https://go-review.googlesource.com/c/go/+/198319 Run-TryBot: Bryan C. Mills <[email protected]> TryBot-Result: Gobot Gobot <[email protected]> Reviewed-by: Jay Conrod <[email protected]>
1 parent dae8e71 commit 1736f3a

File tree

17 files changed

+632
-86
lines changed

17 files changed

+632
-86
lines changed

doc/go1.14.html

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -51,20 +51,38 @@ <h2 id="tools">Tools</h2>
5151

5252
<h3 id="go-command">Go command</h3>
5353

54-
<p><!-- golang.org/issue/30748 -->
55-
The <code>go</code> command now includes snippets of plain-text error messages
56-
from module proxies and other HTTP servers.
57-
An error message will only be shown if it is valid UTF-8 and consists of only
58-
graphic characters and spaces.
54+
<!-- golang.org/issue/33848 -->
55+
<p>
56+
When the main module contains a top-level <code>vendor</code> directory and
57+
its <code>go.mod<code> file specifies <code>go</code> <code>1.14</code> or
58+
higher, the <code>go</code> command now defaults to <code>-mod=vendor</code>
59+
for operations that accept that flag. A new value for that flag,
60+
<code>-mod=mod</code>, causes the <code>go</code> command to instead load
61+
modules from the module cache (as when no <code>vendor<code> directory is
62+
present).
63+
</p>
64+
65+
<p>
66+
When <code>-mod=vendor</code> is set (explicitly or by default), the
67+
<code>go</code> command now verifies that the main module's
68+
<code>vendor/modules.txt</code> file is consistent with its
69+
<code>go.mod</code> file.
5970
</p>
6071

6172
<p><!-- golang.org/issue/32502, golang.org/issue/30345 -->
62-
The <code>go</code> <code>get</code> subcommand no longer accepts
73+
The <code>go</code> <code>get</code> command no longer accepts
6374
the <code>-mod</code> flag. Previously, the flag's setting either
6475
<a href="https://golang.org/issue/30345">was ignored</a> or
6576
<a href="https://golang.org/issue/32502">caused the build to fail</a>.
6677
</p>
6778

79+
<p><!-- golang.org/issue/30748 -->
80+
The <code>go</code> command now includes snippets of plain-text error messages
81+
from module proxies and other HTTP servers.
82+
An error message will only be shown if it is valid UTF-8 and consists of only
83+
graphic characters and spaces.
84+
</p>
85+
6886
<h2 id="runtime">Runtime</h2>
6987

7088
<p>

src/cmd/go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
module cmd
22

3-
go 1.12
3+
go 1.14
44

55
require (
66
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f

src/cmd/go/internal/modcmd/vendor.go

Lines changed: 53 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -59,19 +59,24 @@ func runVendor(cmd *base.Command, args []string) {
5959
modpkgs[m] = append(modpkgs[m], pkg)
6060
}
6161

62+
isExplicit := map[module.Version]bool{}
63+
for _, r := range modload.ModFile().Require {
64+
isExplicit[r.Mod] = true
65+
}
66+
6267
var buf bytes.Buffer
6368
for _, m := range modload.BuildList()[1:] {
64-
if pkgs := modpkgs[m]; len(pkgs) > 0 {
65-
repl := ""
66-
if r := modload.Replacement(m); r.Path != "" {
67-
repl = " => " + r.Path
68-
if r.Version != "" {
69-
repl += " " + r.Version
70-
}
71-
}
72-
fmt.Fprintf(&buf, "# %s %s%s\n", m.Path, m.Version, repl)
69+
if pkgs := modpkgs[m]; len(pkgs) > 0 || isExplicit[m] {
70+
line := moduleLine(m, modload.Replacement(m))
71+
buf.WriteString(line)
7372
if cfg.BuildV {
74-
fmt.Fprintf(os.Stderr, "# %s %s%s\n", m.Path, m.Version, repl)
73+
os.Stderr.WriteString(line)
74+
}
75+
if isExplicit[m] {
76+
buf.WriteString("## explicit\n")
77+
if cfg.BuildV {
78+
os.Stderr.WriteString("## explicit\n")
79+
}
7580
}
7681
sort.Strings(pkgs)
7782
for _, pkg := range pkgs {
@@ -83,6 +88,24 @@ func runVendor(cmd *base.Command, args []string) {
8388
}
8489
}
8590
}
91+
92+
// Record unused and wildcard replacements at the end of the modules.txt file:
93+
// without access to the complete build list, the consumer of the vendor
94+
// directory can't otherwise determine that those replacements had no effect.
95+
for _, r := range modload.ModFile().Replace {
96+
if len(modpkgs[r.Old]) > 0 {
97+
// We we already recorded this replacement in the entry for the replaced
98+
// module with the packages it provides.
99+
continue
100+
}
101+
102+
line := moduleLine(r.Old, r.New)
103+
buf.WriteString(line)
104+
if cfg.BuildV {
105+
os.Stderr.WriteString(line)
106+
}
107+
}
108+
86109
if buf.Len() == 0 {
87110
fmt.Fprintf(os.Stderr, "go: no dependencies to vendor\n")
88111
return
@@ -92,6 +115,26 @@ func runVendor(cmd *base.Command, args []string) {
92115
}
93116
}
94117

118+
func moduleLine(m, r module.Version) string {
119+
b := new(strings.Builder)
120+
b.WriteString("# ")
121+
b.WriteString(m.Path)
122+
if m.Version != "" {
123+
b.WriteString(" ")
124+
b.WriteString(m.Version)
125+
}
126+
if r.Path != "" {
127+
b.WriteString(" => ")
128+
b.WriteString(r.Path)
129+
if r.Version != "" {
130+
b.WriteString(" ")
131+
b.WriteString(r.Version)
132+
}
133+
}
134+
b.WriteString("\n")
135+
return b.String()
136+
}
137+
95138
func vendorPkg(vdir, pkg string) {
96139
realPath := modload.ImportMap(pkg)
97140
if realPath != pkg && modload.ImportMap(realPath) != "" {

src/cmd/go/internal/modload/import.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@ func Import(path string) (m module.Version, dir string, err error) {
139139
return Target, mainDir, nil
140140
}
141141
readVendorList()
142-
return vendorMap[path], vendorDir, nil
142+
return vendorPkgModule[path], vendorDir, nil
143143
}
144144

145145
// Check each module on the build list.

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

Lines changed: 124 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import (
3030
"cmd/go/internal/mvs"
3131
"cmd/go/internal/renameio"
3232
"cmd/go/internal/search"
33+
"cmd/go/internal/semver"
3334
)
3435

3536
var (
@@ -349,8 +350,14 @@ func InitMod() {
349350
excluded[x.Mod] = true
350351
}
351352
modFileToBuildList()
352-
stdVendorMode()
353-
WriteGoMod()
353+
setDefaultBuildMod()
354+
if cfg.BuildMod == "vendor" {
355+
readVendorList()
356+
checkVendorConsistency()
357+
} else {
358+
// TODO(golang.org/issue/33326): if cfg.BuildMod != "readonly"?
359+
WriteGoMod()
360+
}
354361
}
355362

356363
// modFileToBuildList initializes buildList from the modFile.
@@ -371,40 +378,133 @@ func modFileToBuildList() {
371378
buildList = list
372379
}
373380

374-
// stdVendorMode applies inside $GOROOT/src.
375-
// It checks that the go.mod matches vendor/modules.txt
376-
// and then sets -mod=vendor unless this is a command
377-
// that has to do explicitly with modules.
378-
func stdVendorMode() {
379-
if !targetInGorootSrc {
381+
// setDefaultBuildMod sets a default value for cfg.BuildMod
382+
// if it is currently empty.
383+
func setDefaultBuildMod() {
384+
if cfg.BuildMod != "" {
385+
// Don't override an explicit '-mod=' argument.
380386
return
381387
}
388+
cfg.BuildMod = "mod"
382389
if cfg.CmdName == "get" || strings.HasPrefix(cfg.CmdName, "mod ") {
390+
// Don't set -mod implicitly for commands whose purpose is to
391+
// manipulate the build list.
383392
return
384393
}
394+
if modRoot != "" {
395+
if fi, err := os.Stat(filepath.Join(modRoot, "vendor")); err == nil && fi.IsDir() {
396+
modGo := "unspecified"
397+
if modFile.Go != nil {
398+
if semver.Compare("v"+modFile.Go.Version, "v1.14") >= 0 {
399+
// The Go version is at least 1.14, and a vendor directory exists.
400+
// Set -mod=vendor by default.
401+
cfg.BuildMod = "vendor"
402+
return
403+
} else {
404+
modGo = modFile.Go.Version
405+
}
406+
}
407+
fmt.Fprintf(os.Stderr, "go: not defaulting to -mod=vendor because go.mod 'go' version is %s\n", modGo)
408+
}
409+
}
385410

411+
// TODO(golang.org/issue/33326): set -mod=readonly implicitly if the go.mod
412+
// file is itself read-only?
413+
}
414+
415+
// checkVendorConsistency verifies that the vendor/modules.txt file matches (if
416+
// go 1.14) or at least does not contradict (go 1.13 or earlier) the
417+
// requirements and replacements listed in the main module's go.mod file.
418+
func checkVendorConsistency() {
386419
readVendorList()
387-
BuildList:
388-
for _, m := range buildList {
389-
if m.Path == "cmd" || m.Path == "std" {
390-
continue
420+
421+
pre114 := false
422+
if modFile.Go == nil || semver.Compare("v"+modFile.Go.Version, "v1.14") < 0 {
423+
// Go versions before 1.14 did not include enough information in
424+
// vendor/modules.txt to check for consistency.
425+
// If we know that we're on an earlier version, relax the consistency check.
426+
pre114 = true
427+
}
428+
429+
vendErrors := new(strings.Builder)
430+
vendErrorf := func(mod module.Version, format string, args ...interface{}) {
431+
detail := fmt.Sprintf(format, args...)
432+
if mod.Version == "" {
433+
fmt.Fprintf(vendErrors, "\n\t%s: %s", mod.Path, detail)
434+
} else {
435+
fmt.Fprintf(vendErrors, "\n\t%s@%s: %s", mod.Path, mod.Version, detail)
391436
}
392-
for _, v := range vendorList {
393-
if m.Path == v.Path {
394-
if m.Version != v.Version {
395-
base.Fatalf("go: inconsistent vendoring in %s:\n"+
396-
"\tgo.mod requires %s %s but vendor/modules.txt has %s.\n"+
397-
"\trun 'go mod tidy; go mod vendor' to sync",
398-
modRoot, m.Path, m.Version, v.Version)
437+
}
438+
439+
explicitInGoMod := make(map[module.Version]bool, len(modFile.Require))
440+
for _, r := range modFile.Require {
441+
explicitInGoMod[r.Mod] = true
442+
if !vendorMeta[r.Mod].Explicit {
443+
if pre114 {
444+
// Before 1.14, modules.txt did not indicate whether modules were listed
445+
// explicitly in the main module's go.mod file.
446+
// However, we can at least detect a version mismatch if packages were
447+
// vendored from a non-matching version.
448+
if vv, ok := vendorVersion[r.Mod.Path]; ok && vv != r.Mod.Version {
449+
vendErrorf(r.Mod, fmt.Sprintf("is explicitly required in go.mod, but vendor/modules.txt indicates %s@%s", r.Mod.Path, vv))
399450
}
400-
continue BuildList
451+
} else {
452+
vendErrorf(r.Mod, "is explicitly required in go.mod, but not marked as explicit in vendor/modules.txt")
453+
}
454+
}
455+
}
456+
457+
describe := func(m module.Version) string {
458+
if m.Version == "" {
459+
return m.Path
460+
}
461+
return m.Path + "@" + m.Version
462+
}
463+
464+
// We need to verify *all* replacements that occur in modfile: even if they
465+
// don't directly apply to any module in the vendor list, the replacement
466+
// go.mod file can affect the selected versions of other (transitive)
467+
// dependencies
468+
goModReplacement := make(map[module.Version]module.Version, len(modFile.Replace))
469+
for _, r := range modFile.Replace {
470+
goModReplacement[r.Old] = r.New
471+
vr := vendorMeta[r.Old].Replacement
472+
if vr == (module.Version{}) {
473+
if pre114 && (r.Old.Version == "" || vendorVersion[r.Old.Path] != r.Old.Version) {
474+
// Before 1.14, modules.txt omitted wildcard replacements and
475+
// replacements for modules that did not have any packages to vendor.
476+
} else {
477+
vendErrorf(r.Old, "is replaced in go.mod, but not marked as replaced in vendor/modules.txt")
401478
}
479+
} else if vr != r.New {
480+
vendErrorf(r.Old, "is replaced by %s in go.mod, but marked as replaced by %s in vendor/modules.txt", describe(r.New), describe(vr))
402481
}
403-
base.Fatalf("go: inconsistent vendoring in %s:\n"+
404-
"\tgo.mod requires %s %s but vendor/modules.txt does not include it.\n"+
405-
"\trun 'go mod tidy; go mod vendor' to sync", modRoot, m.Path, m.Version)
406482
}
407-
cfg.BuildMod = "vendor"
483+
484+
for _, mod := range vendorList {
485+
meta := vendorMeta[mod]
486+
if meta.Explicit && !explicitInGoMod[mod] {
487+
vendErrorf(mod, "is marked as explicit in vendor/modules.txt, but not explicitly required in go.mod")
488+
}
489+
}
490+
491+
for _, mod := range vendorReplaced {
492+
r, ok := goModReplacement[mod]
493+
if !ok {
494+
r, ok = goModReplacement[module.Version{Path: mod.Path}]
495+
}
496+
if !ok {
497+
vendErrorf(mod, "is marked as replaced in vendor/modules.txt, but not replaced in go.mod")
498+
continue
499+
}
500+
if meta := vendorMeta[mod]; r != meta.Replacement {
501+
vendErrorf(mod, "is marked as replaced by %s in vendor/modules.txt, but replaced by %s in go.mod", describe(meta.Replacement), describe(r))
502+
}
503+
}
504+
505+
if vendErrors.Len() > 0 {
506+
base.Fatalf("go: inconsistent vendoring in %s:%s\n\nrun 'go mod vendor' to sync, or use -mod=mod or -mod=readonly to ignore the vendor directory", modRoot, vendErrors)
507+
}
408508
}
409509

410510
// Allowed reports whether module m is allowed (not excluded) by the main module's go.mod.

0 commit comments

Comments
 (0)