1
0
mirror of https://github.com/golang/go synced 2024-11-23 20:20:01 -07:00

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 <bcmills@google.com>
Reviewed-by: Jay Conrod <jayconrod@google.com>
This commit is contained in:
Bryan C. Mills 2019-10-31 10:03:54 -04:00
parent a2b1dc863f
commit 3c2979682d
3 changed files with 136 additions and 9 deletions

View File

@ -6,12 +6,16 @@ package main
import (
"bytes"
"fmt"
"log"
"os"
"os/exec"
"path/filepath"
"regexp"
"strings"
"sync"
"golang.org/x/mod/semver"
)
// A Dir describes a directory holding code by specifying
@ -19,6 +23,7 @@ import (
type Dir struct {
importPath string // import path for that dir
dir string // file system directory
inModule bool
}
// Dirs is a structure for scanning the directory tree.
@ -113,9 +118,14 @@ func (d *Dirs) bfsWalkRoot(root Dir) {
if name[0] == '.' || name[0] == '_' || name == "testdata" {
continue
}
// Ignore vendor when using modules.
if usingModules && name == "vendor" {
continue
// When in a module, ignore vendor directories and stop at module boundaries.
if root.inModule {
if name == "vendor" {
continue
}
if fi, err := os.Stat(filepath.Join(dir, name, "go.mod")); err == nil && !fi.IsDir() {
continue
}
}
// Remember this (fully qualified) directory for the next pass.
next = append(next, filepath.Join(dir, name))
@ -129,7 +139,7 @@ func (d *Dirs) bfsWalkRoot(root Dir) {
}
importPath += filepath.ToSlash(dir[len(root.dir)+1:])
}
d.scan <- Dir{importPath, dir}
d.scan <- Dir{importPath, dir, root.inModule}
}
}
@ -156,14 +166,20 @@ var codeRootsCache struct {
var usingModules bool
func findCodeRoots() []Dir {
list := []Dir{{"", filepath.Join(buildCtx.GOROOT, "src")}}
var list []Dir
if !testGOPATH {
// Check for use of modules by 'go env GOMOD',
// which reports a go.mod file path if modules are enabled.
stdout, _ := exec.Command("go", "env", "GOMOD").Output()
gomod := string(bytes.TrimSpace(stdout))
usingModules = len(gomod) > 0
if usingModules {
list = append(list,
Dir{dir: filepath.Join(buildCtx.GOROOT, "src"), inModule: true},
Dir{importPath: "cmd", dir: filepath.Join(buildCtx.GOROOT, "src", "cmd"), inModule: true})
}
if gomod == os.DevNull {
// Modules are enabled, but the working directory is outside any module.
// We can still access std, cmd, and packages specified as source files
@ -174,8 +190,9 @@ func findCodeRoots() []Dir {
}
if !usingModules {
list = append(list, Dir{dir: filepath.Join(buildCtx.GOROOT, "src")})
for _, root := range splitGopath() {
list = append(list, Dir{"", filepath.Join(root, "src")})
list = append(list, Dir{dir: filepath.Join(root, "src")})
}
return list
}
@ -185,6 +202,21 @@ func findCodeRoots() []Dir {
// to handle the entire file system search and become go/packages,
// but for now enumerating the module roots lets us fit modules
// into the current code with as few changes as possible.
mainMod, vendorEnabled, err := vendorEnabled()
if err != nil {
return list
}
if vendorEnabled {
// Add the vendor directory to the search path ahead of "std".
// That way, if the main module *is* "std", we will identify the path
// without the "vendor/" prefix before the one with that prefix.
list = append([]Dir{{dir: filepath.Join(mainMod.Dir, "vendor"), inModule: false}}, list...)
if mainMod.Path != "std" {
list = append(list, Dir{importPath: mainMod.Path, dir: mainMod.Dir, inModule: true})
}
return list
}
cmd := exec.Command("go", "list", "-m", "-f={{.Path}}\t{{.Dir}}", "all")
cmd.Stderr = os.Stderr
out, _ := cmd.Output()
@ -195,9 +227,77 @@ func findCodeRoots() []Dir {
}
path, dir := line[:i], line[i+1:]
if dir != "" {
list = append(list, Dir{path, dir})
list = append(list, Dir{importPath: path, dir: dir, inModule: true})
}
}
return list
}
// The functions below are derived from x/tools/internal/imports at CL 203017.
type moduleJSON struct {
Path, Dir, GoVersion string
}
var modFlagRegexp = regexp.MustCompile(`-mod[ =](\w+)`)
// vendorEnabled indicates if vendoring is enabled.
// Inspired by setDefaultBuildMod in modload/init.go
func vendorEnabled() (*moduleJSON, bool, error) {
mainMod, go114, err := getMainModuleAnd114()
if err != nil {
return nil, false, err
}
stdout, _ := exec.Command("go", "env", "GOFLAGS").Output()
goflags := string(bytes.TrimSpace(stdout))
matches := modFlagRegexp.FindStringSubmatch(goflags)
var modFlag string
if len(matches) != 0 {
modFlag = matches[1]
}
if modFlag != "" {
// Don't override an explicit '-mod=' argument.
return mainMod, modFlag == "vendor", nil
}
if mainMod == nil || !go114 {
return mainMod, false, nil
}
// Check 1.14's automatic vendor mode.
if fi, err := os.Stat(filepath.Join(mainMod.Dir, "vendor")); err == nil && fi.IsDir() {
if mainMod.GoVersion != "" && semver.Compare("v"+mainMod.GoVersion, "v1.14") >= 0 {
// The Go version is at least 1.14, and a vendor directory exists.
// Set -mod=vendor by default.
return mainMod, true, nil
}
}
return mainMod, false, nil
}
// getMainModuleAnd114 gets the main module's information and whether the
// go command in use is 1.14+. This is the information needed to figure out
// if vendoring should be enabled.
func getMainModuleAnd114() (*moduleJSON, bool, error) {
const format = `{{.Path}}
{{.Dir}}
{{.GoVersion}}
{{range context.ReleaseTags}}{{if eq . "go1.14"}}{{.}}{{end}}{{end}}
`
cmd := exec.Command("go", "list", "-m", "-f", format)
cmd.Stderr = os.Stderr
stdout, err := cmd.Output()
if err != nil {
return nil, false, nil
}
lines := strings.Split(string(stdout), "\n")
if len(lines) < 5 {
return nil, false, fmt.Errorf("unexpected stdout: %q", stdout)
}
mod := &moduleJSON{
Path: lines[0],
Dir: lines[1],
GoVersion: lines[2],
}
return mod, lines[3] == "go1.14", nil
}

View File

@ -27,7 +27,10 @@ func TestMain(m *testing.M) {
if err != nil {
panic(err)
}
dirsInit(Dir{"testdata", testdataDir}, Dir{"testdata/nested", filepath.Join(testdataDir, "nested")}, Dir{"testdata/nested/nested", filepath.Join(testdataDir, "nested", "nested")})
dirsInit(
Dir{importPath: "testdata", dir: testdataDir},
Dir{importPath: "testdata/nested", dir: filepath.Join(testdataDir, "nested")},
Dir{importPath: "testdata/nested/nested", dir: filepath.Join(testdataDir, "nested", "nested")})
os.Exit(m.Run())
}

View File

@ -41,6 +41,30 @@ env GOPROXY=off
! go doc example.com/hello
stderr '^doc: cannot find module providing package example.com/hello: module lookup disabled by GOPROXY=off$'
# When in a module with a vendor directory, doc should use the vendored copies
# of the packages. 'std' and 'cmd' are convenient examples of such modules.
#
# When in those modules, the "// import" comment should refer to the same import
# path used in source code, not to the absolute path relative to GOROOT.
cd $GOROOT/src
go doc cryptobyte
stdout '// import "golang.org/x/crypto/cryptobyte"'
cd $GOROOT/src/cmd/go
go doc modfile
stdout '// import "golang.org/x/mod/modfile"'
# When outside of the 'std' module, its vendored packages
# remain accessible using the 'vendor/' prefix, but report
# the correct "// import" comment as used within std.
cd $GOPATH
go doc vendor/golang.org/x/crypto/cryptobyte
stdout '// import "vendor/golang.org/x/crypto/cryptobyte"'
go doc cmd/vendor/golang.org/x/mod/modfile
stdout '// import "cmd/vendor/golang.org/x/mod/modfile"'
-- go.mod --
module x
require rsc.io/quote v1.5.2