Skip to content

Commit 3c29796

Browse files
author
Bryan C. Mills
committed
cmd/doc: understand vendor directories in module mode
This change employs the same strategy as in CL 203017 to detect when vendoring is in use, and if so treats the vendor directory as a (non-module, prefixless) root. The integration test also verifies that the 'std' and 'cmd' modules are included and their vendored dependencies are visible (as they are with 'go list') even when outside of those modules. Fixes #35224 Change-Id: I18cd01218e9eb97c1fc6e2401c1907536b0b95f7 Reviewed-on: https://go-review.googlesource.com/c/go/+/205577 Run-TryBot: Bryan C. Mills <[email protected]> Reviewed-by: Jay Conrod <[email protected]>
1 parent a2b1dc8 commit 3c29796

File tree

3 files changed

+136
-9
lines changed

3 files changed

+136
-9
lines changed

src/cmd/doc/dirs.go

+108-8
Original file line numberDiff line numberDiff line change
@@ -6,19 +6,24 @@ package main
66

77
import (
88
"bytes"
9+
"fmt"
910
"log"
1011
"os"
1112
"os/exec"
1213
"path/filepath"
14+
"regexp"
1315
"strings"
1416
"sync"
17+
18+
"golang.org/x/mod/semver"
1519
)
1620

1721
// A Dir describes a directory holding code by specifying
1822
// the expected import path and the file system directory.
1923
type Dir struct {
2024
importPath string // import path for that dir
2125
dir string // file system directory
26+
inModule bool
2227
}
2328

2429
// Dirs is a structure for scanning the directory tree.
@@ -113,9 +118,14 @@ func (d *Dirs) bfsWalkRoot(root Dir) {
113118
if name[0] == '.' || name[0] == '_' || name == "testdata" {
114119
continue
115120
}
116-
// Ignore vendor when using modules.
117-
if usingModules && name == "vendor" {
118-
continue
121+
// When in a module, ignore vendor directories and stop at module boundaries.
122+
if root.inModule {
123+
if name == "vendor" {
124+
continue
125+
}
126+
if fi, err := os.Stat(filepath.Join(dir, name, "go.mod")); err == nil && !fi.IsDir() {
127+
continue
128+
}
119129
}
120130
// Remember this (fully qualified) directory for the next pass.
121131
next = append(next, filepath.Join(dir, name))
@@ -129,7 +139,7 @@ func (d *Dirs) bfsWalkRoot(root Dir) {
129139
}
130140
importPath += filepath.ToSlash(dir[len(root.dir)+1:])
131141
}
132-
d.scan <- Dir{importPath, dir}
142+
d.scan <- Dir{importPath, dir, root.inModule}
133143
}
134144
}
135145

@@ -156,14 +166,20 @@ var codeRootsCache struct {
156166
var usingModules bool
157167

158168
func findCodeRoots() []Dir {
159-
list := []Dir{{"", filepath.Join(buildCtx.GOROOT, "src")}}
160-
169+
var list []Dir
161170
if !testGOPATH {
162171
// Check for use of modules by 'go env GOMOD',
163172
// which reports a go.mod file path if modules are enabled.
164173
stdout, _ := exec.Command("go", "env", "GOMOD").Output()
165174
gomod := string(bytes.TrimSpace(stdout))
175+
166176
usingModules = len(gomod) > 0
177+
if usingModules {
178+
list = append(list,
179+
Dir{dir: filepath.Join(buildCtx.GOROOT, "src"), inModule: true},
180+
Dir{importPath: "cmd", dir: filepath.Join(buildCtx.GOROOT, "src", "cmd"), inModule: true})
181+
}
182+
167183
if gomod == os.DevNull {
168184
// Modules are enabled, but the working directory is outside any module.
169185
// We can still access std, cmd, and packages specified as source files
@@ -174,8 +190,9 @@ func findCodeRoots() []Dir {
174190
}
175191

176192
if !usingModules {
193+
list = append(list, Dir{dir: filepath.Join(buildCtx.GOROOT, "src")})
177194
for _, root := range splitGopath() {
178-
list = append(list, Dir{"", filepath.Join(root, "src")})
195+
list = append(list, Dir{dir: filepath.Join(root, "src")})
179196
}
180197
return list
181198
}
@@ -185,6 +202,21 @@ func findCodeRoots() []Dir {
185202
// to handle the entire file system search and become go/packages,
186203
// but for now enumerating the module roots lets us fit modules
187204
// into the current code with as few changes as possible.
205+
mainMod, vendorEnabled, err := vendorEnabled()
206+
if err != nil {
207+
return list
208+
}
209+
if vendorEnabled {
210+
// Add the vendor directory to the search path ahead of "std".
211+
// That way, if the main module *is* "std", we will identify the path
212+
// without the "vendor/" prefix before the one with that prefix.
213+
list = append([]Dir{{dir: filepath.Join(mainMod.Dir, "vendor"), inModule: false}}, list...)
214+
if mainMod.Path != "std" {
215+
list = append(list, Dir{importPath: mainMod.Path, dir: mainMod.Dir, inModule: true})
216+
}
217+
return list
218+
}
219+
188220
cmd := exec.Command("go", "list", "-m", "-f={{.Path}}\t{{.Dir}}", "all")
189221
cmd.Stderr = os.Stderr
190222
out, _ := cmd.Output()
@@ -195,9 +227,77 @@ func findCodeRoots() []Dir {
195227
}
196228
path, dir := line[:i], line[i+1:]
197229
if dir != "" {
198-
list = append(list, Dir{path, dir})
230+
list = append(list, Dir{importPath: path, dir: dir, inModule: true})
199231
}
200232
}
201233

202234
return list
203235
}
236+
237+
// The functions below are derived from x/tools/internal/imports at CL 203017.
238+
239+
type moduleJSON struct {
240+
Path, Dir, GoVersion string
241+
}
242+
243+
var modFlagRegexp = regexp.MustCompile(`-mod[ =](\w+)`)
244+
245+
// vendorEnabled indicates if vendoring is enabled.
246+
// Inspired by setDefaultBuildMod in modload/init.go
247+
func vendorEnabled() (*moduleJSON, bool, error) {
248+
mainMod, go114, err := getMainModuleAnd114()
249+
if err != nil {
250+
return nil, false, err
251+
}
252+
253+
stdout, _ := exec.Command("go", "env", "GOFLAGS").Output()
254+
goflags := string(bytes.TrimSpace(stdout))
255+
matches := modFlagRegexp.FindStringSubmatch(goflags)
256+
var modFlag string
257+
if len(matches) != 0 {
258+
modFlag = matches[1]
259+
}
260+
if modFlag != "" {
261+
// Don't override an explicit '-mod=' argument.
262+
return mainMod, modFlag == "vendor", nil
263+
}
264+
if mainMod == nil || !go114 {
265+
return mainMod, false, nil
266+
}
267+
// Check 1.14's automatic vendor mode.
268+
if fi, err := os.Stat(filepath.Join(mainMod.Dir, "vendor")); err == nil && fi.IsDir() {
269+
if mainMod.GoVersion != "" && semver.Compare("v"+mainMod.GoVersion, "v1.14") >= 0 {
270+
// The Go version is at least 1.14, and a vendor directory exists.
271+
// Set -mod=vendor by default.
272+
return mainMod, true, nil
273+
}
274+
}
275+
return mainMod, false, nil
276+
}
277+
278+
// getMainModuleAnd114 gets the main module's information and whether the
279+
// go command in use is 1.14+. This is the information needed to figure out
280+
// if vendoring should be enabled.
281+
func getMainModuleAnd114() (*moduleJSON, bool, error) {
282+
const format = `{{.Path}}
283+
{{.Dir}}
284+
{{.GoVersion}}
285+
{{range context.ReleaseTags}}{{if eq . "go1.14"}}{{.}}{{end}}{{end}}
286+
`
287+
cmd := exec.Command("go", "list", "-m", "-f", format)
288+
cmd.Stderr = os.Stderr
289+
stdout, err := cmd.Output()
290+
if err != nil {
291+
return nil, false, nil
292+
}
293+
lines := strings.Split(string(stdout), "\n")
294+
if len(lines) < 5 {
295+
return nil, false, fmt.Errorf("unexpected stdout: %q", stdout)
296+
}
297+
mod := &moduleJSON{
298+
Path: lines[0],
299+
Dir: lines[1],
300+
GoVersion: lines[2],
301+
}
302+
return mod, lines[3] == "go1.14", nil
303+
}

src/cmd/doc/doc_test.go

+4-1
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,10 @@ func TestMain(m *testing.M) {
2727
if err != nil {
2828
panic(err)
2929
}
30-
dirsInit(Dir{"testdata", testdataDir}, Dir{"testdata/nested", filepath.Join(testdataDir, "nested")}, Dir{"testdata/nested/nested", filepath.Join(testdataDir, "nested", "nested")})
30+
dirsInit(
31+
Dir{importPath: "testdata", dir: testdataDir},
32+
Dir{importPath: "testdata/nested", dir: filepath.Join(testdataDir, "nested")},
33+
Dir{importPath: "testdata/nested/nested", dir: filepath.Join(testdataDir, "nested", "nested")})
3134

3235
os.Exit(m.Run())
3336
}

src/cmd/go/testdata/script/mod_doc.txt

+24
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,30 @@ env GOPROXY=off
4141
! go doc example.com/hello
4242
stderr '^doc: cannot find module providing package example.com/hello: module lookup disabled by GOPROXY=off$'
4343

44+
# When in a module with a vendor directory, doc should use the vendored copies
45+
# of the packages. 'std' and 'cmd' are convenient examples of such modules.
46+
#
47+
# When in those modules, the "// import" comment should refer to the same import
48+
# path used in source code, not to the absolute path relative to GOROOT.
49+
50+
cd $GOROOT/src
51+
go doc cryptobyte
52+
stdout '// import "golang.org/x/crypto/cryptobyte"'
53+
54+
cd $GOROOT/src/cmd/go
55+
go doc modfile
56+
stdout '// import "golang.org/x/mod/modfile"'
57+
58+
# When outside of the 'std' module, its vendored packages
59+
# remain accessible using the 'vendor/' prefix, but report
60+
# the correct "// import" comment as used within std.
61+
cd $GOPATH
62+
go doc vendor/golang.org/x/crypto/cryptobyte
63+
stdout '// import "vendor/golang.org/x/crypto/cryptobyte"'
64+
65+
go doc cmd/vendor/golang.org/x/mod/modfile
66+
stdout '// import "cmd/vendor/golang.org/x/mod/modfile"'
67+
4468
-- go.mod --
4569
module x
4670
require rsc.io/quote v1.5.2

0 commit comments

Comments
 (0)