1
0
mirror of https://github.com/golang/go synced 2024-11-18 06:54:49 -07:00

cmd/godoc: support automatic vendoring

Fixes golang/go#35429

Change-Id: I060ccfbed4c3975d1ddc94fda4fadea527b29841
Reviewed-on: https://go-review.googlesource.com/c/tools/+/232958
Run-TryBot: Agniva De Sarker <agniva.quicksilver@gmail.com>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Dmitri Shuralyov <dmitshur@golang.org>
This commit is contained in:
Agniva De Sarker 2020-05-09 23:19:08 +05:30 committed by Agniva De Sarker
parent 1b747fd945
commit 6eec81c746
3 changed files with 171 additions and 110 deletions

View File

@ -19,6 +19,7 @@ package main
import ( import (
"archive/zip" "archive/zip"
"bytes" "bytes"
"context"
"encoding/json" "encoding/json"
_ "expvar" // to serve /debug/vars _ "expvar" // to serve /debug/vars
"flag" "flag"
@ -44,6 +45,7 @@ import (
"golang.org/x/tools/godoc/vfs/gatefs" "golang.org/x/tools/godoc/vfs/gatefs"
"golang.org/x/tools/godoc/vfs/mapfs" "golang.org/x/tools/godoc/vfs/mapfs"
"golang.org/x/tools/godoc/vfs/zipfs" "golang.org/x/tools/godoc/vfs/zipfs"
"golang.org/x/tools/internal/gocommand"
"golang.org/x/xerrors" "golang.org/x/xerrors"
) )
@ -210,28 +212,48 @@ func main() {
usage() usage()
} }
// Try to download dependencies that are not in the module cache in order to // Detect whether to use vendor mode or not.
// to show their documentation. mainMod, vendorEnabled, err := gocommand.VendorEnabled(context.Background(), gocommand.Invocation{}, &gocommand.Runner{})
// This may fail if module downloading is disallowed (GOPROXY=off) or due to
// limited connectivity, in which case we print errors to stderr and show
// documentation only for packages that are available.
fillModuleCache(os.Stderr, goModFile)
// Determine modules in the build list.
mods, err := buildList(goModFile)
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, "failed to determine the build list of the main module: %v", err) fmt.Fprintf(os.Stderr, "failed to determine if vendoring is enabled: %v", err)
os.Exit(1) os.Exit(1)
} }
if vendorEnabled {
// Bind the root directory of the main module.
fs.Bind(path.Join("/src", mainMod.Path), gatefs.New(vfs.OS(mainMod.Dir), fsGate), "/", vfs.BindAfter)
// Bind module trees into Go root. // Bind the vendor directory.
for _, m := range mods { //
if m.Dir == "" { // Note that in module mode, vendor directories in locations
// Module is not available in the module cache, skip it. // other than the main module's root directory are ignored.
continue // See https://golang.org/ref/mod#vendoring.
vendorDir := filepath.Join(mainMod.Dir, "vendor")
fs.Bind("/src", gatefs.New(vfs.OS(vendorDir), fsGate), "/", vfs.BindAfter)
} else {
// Try to download dependencies that are not in the module cache in order to
// to show their documentation.
// This may fail if module downloading is disallowed (GOPROXY=off) or due to
// limited connectivity, in which case we print errors to stderr and show
// documentation only for packages that are available.
fillModuleCache(os.Stderr, goModFile)
// Determine modules in the build list.
mods, err := buildList(goModFile)
if err != nil {
fmt.Fprintf(os.Stderr, "failed to determine the build list of the main module: %v", err)
os.Exit(1)
}
// Bind module trees into Go root.
for _, m := range mods {
if m.Dir == "" {
// Module is not available in the module cache, skip it.
continue
}
dst := path.Join("/src", m.Path)
fs.Bind(dst, gatefs.New(vfs.OS(m.Dir), fsGate), "/", vfs.BindAfter)
} }
dst := path.Join("/src", m.Path)
fs.Bind(dst, gatefs.New(vfs.OS(m.Dir), fsGate), "/", vfs.BindAfter)
} }
} else { } else {
fmt.Println("using GOPATH mode") fmt.Println("using GOPATH mode")
@ -395,7 +417,7 @@ func goMod() (string, error) {
// with all dependencies of the main module in the current directory // with all dependencies of the main module in the current directory
// by invoking the go command. Module download logs are streamed to w. // by invoking the go command. Module download logs are streamed to w.
// If there are any problems encountered, they are also written to w. // If there are any problems encountered, they are also written to w.
// It should only be used when operating in module mode. // It should only be used in module mode, when vendor mode isn't on.
// //
// See https://golang.org/cmd/go/#hdr-Download_modules_to_local_cache. // See https://golang.org/cmd/go/#hdr-Download_modules_to_local_cache.
func fillModuleCache(w io.Writer, goMod string) { func fillModuleCache(w io.Writer, goMod string) {
@ -436,9 +458,14 @@ func fillModuleCache(w io.Writer, goMod string) {
} }
} }
type mod struct {
Path string // Module path.
Dir string // Directory holding files for this module, if any.
}
// buildList determines the build list in the current directory // buildList determines the build list in the current directory
// by invoking the go command. It should only be used when operating // by invoking the go command. It should only be used in module mode,
// in module mode. // when vendor mode isn't on.
// //
// See https://golang.org/cmd/go/#hdr-The_main_module_and_the_build_list. // See https://golang.org/cmd/go/#hdr-The_main_module_and_the_build_list.
func buildList(goMod string) ([]mod, error) { func buildList(goMod string) ([]mod, error) {
@ -467,11 +494,6 @@ func buildList(goMod string) ([]mod, error) {
return mods, nil return mods, nil
} }
type mod struct {
Path string // Module path.
Dir string // Directory holding files for this module, if any.
}
// moduleFS is a vfs.FileSystem wrapper used when godoc is running // moduleFS is a vfs.FileSystem wrapper used when godoc is running
// in module mode. It's needed so that packages inside modules are // in module mode. It's needed so that packages inside modules are
// considered to be third party. // considered to be third party.

View File

@ -0,0 +1,102 @@
// Copyright 2020 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package gocommand
import (
"bytes"
"context"
"fmt"
"os"
"path/filepath"
"regexp"
"strings"
"golang.org/x/mod/semver"
)
// ModuleJSON holds information about a module.
type ModuleJSON struct {
Path string // module path
Replace *ModuleJSON // replaced by this module
Main bool // is this the main module?
Indirect bool // is this module only an indirect dependency of main module?
Dir string // directory holding files for this module, if any
GoMod string // path to go.mod file for this module, if any
GoVersion string // go version used in module
}
var modFlagRegexp = regexp.MustCompile(`-mod[ =](\w+)`)
// VendorEnabled reports whether vendoring is enabled. It takes a *Runner to execute Go commands
// with the supplied context.Context and Invocation. The Invocation can contain pre-defined fields,
// of which only Verb and Args are modified to run the appropriate Go command.
// Inspired by setDefaultBuildMod in modload/init.go
func VendorEnabled(ctx context.Context, inv Invocation, r *Runner) (*ModuleJSON, bool, error) {
mainMod, go114, err := getMainModuleAnd114(ctx, inv, r)
if err != nil {
return nil, false, err
}
// We check the GOFLAGS to see if there is anything overridden or not.
inv.Verb = "env"
inv.Args = []string{"GOFLAGS"}
stdout, err := r.Run(ctx, inv)
if err != nil {
return nil, false, err
}
goflags := string(bytes.TrimSpace(stdout.Bytes()))
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(ctx context.Context, inv Invocation, r *Runner) (*ModuleJSON, bool, error) {
const format = `{{.Path}}
{{.Dir}}
{{.GoMod}}
{{.GoVersion}}
{{range context.ReleaseTags}}{{if eq . "go1.14"}}{{.}}{{end}}{{end}}
`
inv.Verb = "list"
inv.Args = []string{"-m", "-f", format}
stdout, err := r.Run(ctx, inv)
if err != nil {
return nil, false, err
}
lines := strings.Split(stdout.String(), "\n")
if len(lines) < 5 {
return nil, false, fmt.Errorf("unexpected stdout: %q", stdout.String())
}
mod := &ModuleJSON{
Path: lines[0],
Dir: lines[1],
GoMod: lines[2],
GoVersion: lines[3],
Main: true,
}
return mod, lines[4] == "go1.14", nil
}

View File

@ -15,7 +15,7 @@ import (
"strings" "strings"
"golang.org/x/mod/module" "golang.org/x/mod/module"
"golang.org/x/mod/semver" "golang.org/x/tools/internal/gocommand"
"golang.org/x/tools/internal/gopathwalk" "golang.org/x/tools/internal/gopathwalk"
) )
@ -24,31 +24,21 @@ import (
type ModuleResolver struct { type ModuleResolver struct {
env *ProcessEnv env *ProcessEnv
moduleCacheDir string moduleCacheDir string
dummyVendorMod *ModuleJSON // If vendoring is enabled, the pseudo-module that represents the /vendor directory. dummyVendorMod *gocommand.ModuleJSON // If vendoring is enabled, the pseudo-module that represents the /vendor directory.
roots []gopathwalk.Root roots []gopathwalk.Root
scanSema chan struct{} // scanSema prevents concurrent scans and guards scannedRoots. scanSema chan struct{} // scanSema prevents concurrent scans and guards scannedRoots.
scannedRoots map[gopathwalk.Root]bool scannedRoots map[gopathwalk.Root]bool
initialized bool initialized bool
main *ModuleJSON main *gocommand.ModuleJSON
modsByModPath []*ModuleJSON // All modules, ordered by # of path components in module Path... modsByModPath []*gocommand.ModuleJSON // All modules, ordered by # of path components in module Path...
modsByDir []*ModuleJSON // ...or Dir. modsByDir []*gocommand.ModuleJSON // ...or Dir.
// moduleCacheCache stores information about the module cache. // moduleCacheCache stores information about the module cache.
moduleCacheCache *dirInfoCache moduleCacheCache *dirInfoCache
otherCache *dirInfoCache otherCache *dirInfoCache
} }
type ModuleJSON struct {
Path string // module path
Replace *ModuleJSON // replaced by this module
Main bool // is this the main module?
Indirect bool // is this module only an indirect dependency of main module?
Dir string // directory holding files for this module, if any
GoMod string // path to go.mod file for this module, if any
GoVersion string // go version used in module
}
func newModuleResolver(e *ProcessEnv) *ModuleResolver { func newModuleResolver(e *ProcessEnv) *ModuleResolver {
r := &ModuleResolver{ r := &ModuleResolver{
env: e, env: e,
@ -62,7 +52,14 @@ func (r *ModuleResolver) init() error {
if r.initialized { if r.initialized {
return nil return nil
} }
mainMod, vendorEnabled, err := vendorEnabled(r.env)
inv := gocommand.Invocation{
BuildFlags: r.env.BuildFlags,
Env: r.env.env(),
Logf: r.env.Logf,
WorkingDir: r.env.WorkingDir,
}
mainMod, vendorEnabled, err := gocommand.VendorEnabled(context.TODO(), inv, r.env.GocmdRunner)
if err != nil { if err != nil {
return err return err
} }
@ -71,12 +68,12 @@ func (r *ModuleResolver) init() error {
// Vendor mode is on, so all the non-Main modules are irrelevant, // Vendor mode is on, so all the non-Main modules are irrelevant,
// and we need to search /vendor for everything. // and we need to search /vendor for everything.
r.main = mainMod r.main = mainMod
r.dummyVendorMod = &ModuleJSON{ r.dummyVendorMod = &gocommand.ModuleJSON{
Path: "", Path: "",
Dir: filepath.Join(mainMod.Dir, "vendor"), Dir: filepath.Join(mainMod.Dir, "vendor"),
} }
r.modsByModPath = []*ModuleJSON{mainMod, r.dummyVendorMod} r.modsByModPath = []*gocommand.ModuleJSON{mainMod, r.dummyVendorMod}
r.modsByDir = []*ModuleJSON{mainMod, r.dummyVendorMod} r.modsByDir = []*gocommand.ModuleJSON{mainMod, r.dummyVendorMod}
} else { } else {
// Vendor mode is off, so run go list -m ... to find everything. // Vendor mode is off, so run go list -m ... to find everything.
r.initAllMods() r.initAllMods()
@ -106,7 +103,7 @@ func (r *ModuleResolver) init() error {
if vendorEnabled { if vendorEnabled {
r.roots = append(r.roots, gopathwalk.Root{r.dummyVendorMod.Dir, gopathwalk.RootOther}) r.roots = append(r.roots, gopathwalk.Root{r.dummyVendorMod.Dir, gopathwalk.RootOther})
} else { } else {
addDep := func(mod *ModuleJSON) { addDep := func(mod *gocommand.ModuleJSON) {
if mod.Replace == nil { if mod.Replace == nil {
// This is redundant with the cache, but we'll skip it cheaply enough. // This is redundant with the cache, but we'll skip it cheaply enough.
r.roots = append(r.roots, gopathwalk.Root{mod.Dir, gopathwalk.RootModuleCache}) r.roots = append(r.roots, gopathwalk.Root{mod.Dir, gopathwalk.RootModuleCache})
@ -151,7 +148,7 @@ func (r *ModuleResolver) initAllMods() error {
return err return err
} }
for dec := json.NewDecoder(stdout); dec.More(); { for dec := json.NewDecoder(stdout); dec.More(); {
mod := &ModuleJSON{} mod := &gocommand.ModuleJSON{}
if err := dec.Decode(mod); err != nil { if err := dec.Decode(mod); err != nil {
return err return err
} }
@ -197,7 +194,7 @@ func (r *ModuleResolver) ClearForNewMod() {
// findPackage returns the module and directory that contains the package at // findPackage returns the module and directory that contains the package at
// the given import path, or returns nil, "" if no module is in scope. // the given import path, or returns nil, "" if no module is in scope.
func (r *ModuleResolver) findPackage(importPath string) (*ModuleJSON, string) { func (r *ModuleResolver) findPackage(importPath string) (*gocommand.ModuleJSON, string) {
// This can't find packages in the stdlib, but that's harmless for all // This can't find packages in the stdlib, but that's harmless for all
// the existing code paths. // the existing code paths.
for _, m := range r.modsByModPath { for _, m := range r.modsByModPath {
@ -283,7 +280,7 @@ func (r *ModuleResolver) cacheExports(ctx context.Context, env *ProcessEnv, info
// findModuleByDir returns the module that contains dir, or nil if no such // findModuleByDir returns the module that contains dir, or nil if no such
// module is in scope. // module is in scope.
func (r *ModuleResolver) findModuleByDir(dir string) *ModuleJSON { func (r *ModuleResolver) findModuleByDir(dir string) *gocommand.ModuleJSON {
// This is quite tricky and may not be correct. dir could be: // This is quite tricky and may not be correct. dir could be:
// - a package in the main module. // - a package in the main module.
// - a replace target underneath the main module's directory. // - a replace target underneath the main module's directory.
@ -310,7 +307,7 @@ func (r *ModuleResolver) findModuleByDir(dir string) *ModuleJSON {
// dirIsNestedModule reports if dir is contained in a nested module underneath // dirIsNestedModule reports if dir is contained in a nested module underneath
// mod, not actually in mod. // mod, not actually in mod.
func (r *ModuleResolver) dirIsNestedModule(dir string, mod *ModuleJSON) bool { func (r *ModuleResolver) dirIsNestedModule(dir string, mod *gocommand.ModuleJSON) bool {
if !strings.HasPrefix(dir, mod.Dir) { if !strings.HasPrefix(dir, mod.Dir) {
return false return false
} }
@ -490,7 +487,7 @@ func (r *ModuleResolver) scoreImportPath(ctx context.Context, path string) int {
return modRelevance(mod) return modRelevance(mod)
} }
func modRelevance(mod *ModuleJSON) int { func modRelevance(mod *gocommand.ModuleJSON) int {
switch { switch {
case mod == nil: // out of scope case mod == nil: // out of scope
return MaxRelevance - 4 return MaxRelevance - 4
@ -656,63 +653,3 @@ func modulePath(mod []byte) string {
} }
return "" // missing module path return "" // missing module path
} }
var modFlagRegexp = regexp.MustCompile(`-mod[ =](\w+)`)
// vendorEnabled indicates if vendoring is enabled.
// Inspired by setDefaultBuildMod in modload/init.go
func vendorEnabled(env *ProcessEnv) (*ModuleJSON, bool, error) {
mainMod, go114, err := getMainModuleAnd114(env)
if err != nil {
return nil, false, err
}
matches := modFlagRegexp.FindStringSubmatch(env.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(env *ProcessEnv) (*ModuleJSON, bool, error) {
const format = `{{.Path}}
{{.Dir}}
{{.GoMod}}
{{.GoVersion}}
{{range context.ReleaseTags}}{{if eq . "go1.14"}}{{.}}{{end}}{{end}}
`
stdout, err := env.invokeGo(context.TODO(), "list", "-m", "-f", format)
if err != nil {
return nil, false, nil
}
lines := strings.Split(stdout.String(), "\n")
if len(lines) < 5 {
return nil, false, fmt.Errorf("unexpected stdout: %q", stdout)
}
mod := &ModuleJSON{
Path: lines[0],
Dir: lines[1],
GoMod: lines[2],
GoVersion: lines[3],
Main: true,
}
return mod, lines[4] == "go1.14", nil
}