Skip to content

Commit f851253

Browse files
committed
go/build: invoke go command to find modules during Import, Context.Import
The introduction of modules has broken (intentionally) the rule that the source code for a package x/y/z is in GOPATH/src/x/y/z (or GOROOT/src/x/y/z). This breaks the code in go/build.Import, which uses that rule to find the directory for a package. In the long term, the fix is to move programs that load packages off of go/build and onto golang.org/x/tools/go/packages, which we hope will eventually become go/packages. That code invokes the go command to learn what it needs to know about where packages are. In the short term, though, there are lots of programs that use go/build and will not be able to find code in module dependencies. To help those programs, go/build now runs the go command to ask where a package's source code can be found, if it sees that modules are in use. (If modules are not in use, it falls back to the usual lookup code and does not invoke the go command, so that existing uses are unaffected and not slowed down.) Helps #24661. Fixes #26504. Change-Id: I0dac68854cf5011005c3b2272810245d81b7cc5a Reviewed-on: https://go-review.googlesource.com/125296 Reviewed-by: Michael Matloob <[email protected]> Reviewed-by: Bryan C. Mills <[email protected]>
1 parent 8450fd9 commit f851253

File tree

6 files changed

+211
-11
lines changed

6 files changed

+211
-11
lines changed

src/cmd/go/go_test.go

+6-1
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ var testGOCACHE string
105105

106106
var testGo string
107107
var testTmpDir string
108+
var testBin string
108109

109110
// The TestMain function creates a go command for testing purposes and
110111
// deletes it after the tests have been run.
@@ -133,7 +134,11 @@ func TestMain(m *testing.M) {
133134
}
134135

135136
if canRun {
136-
testGo = filepath.Join(testTmpDir, "testgo"+exeSuffix)
137+
testBin = filepath.Join(testTmpDir, "testbin")
138+
if err := os.Mkdir(testBin, 0777); err != nil {
139+
log.Fatal(err)
140+
}
141+
testGo = filepath.Join(testBin, "go"+exeSuffix)
137142
args := []string{"build", "-tags", "testgo", "-o", testGo}
138143
if race.Enabled {
139144
args = append(args, "-race")

src/cmd/go/internal/cfg/cfg.go

+7-1
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import (
2020
var (
2121
BuildA bool // -a flag
2222
BuildBuildmode string // -buildmode flag
23-
BuildContext = build.Default
23+
BuildContext = defaultContext()
2424
BuildGetmode string // -getmode flag
2525
BuildI bool // -i flag
2626
BuildLinkshared bool // -linkshared flag
@@ -43,6 +43,12 @@ var (
4343
DebugActiongraph string // -debug-actiongraph flag (undocumented, unstable)
4444
)
4545

46+
func defaultContext() build.Context {
47+
ctxt := build.Default
48+
ctxt.JoinPath = filepath.Join // back door to say "do not use go command"
49+
return ctxt
50+
}
51+
4652
func init() {
4753
BuildToolchainCompiler = func() string { return "missing-compiler" }
4854
BuildToolchainLinker = func() string { return "missing-linker" }

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

-6
Original file line numberDiff line numberDiff line change
@@ -94,12 +94,6 @@ func Init() {
9494
}
9595
}
9696

97-
// If this is testgo - the test binary during cmd/go tests -
98-
// then do not let it look for a go.mod unless GO111MODULE has an explicit setting or this is 'go mod -init'.
99-
if base := filepath.Base(os.Args[0]); (base == "testgo" || base == "testgo.exe") && env == "" && !CmdModInit {
100-
return
101-
}
102-
10397
// Disable any prompting for passwords by Git.
10498
// Only has an effect for 2.3.0 or later, but avoiding
10599
// the prompt in earlier versions is just too hard.

src/cmd/go/script_test.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ func (ts *testScript) setup() {
8585
ts.cd = filepath.Join(ts.workdir, "gopath/src")
8686
ts.env = []string{
8787
"WORK=" + ts.workdir, // must be first for ts.abbrev
88-
"PATH=" + os.Getenv("PATH"),
88+
"PATH=" + testBin + string(filepath.ListSeparator) + os.Getenv("PATH"),
8989
homeEnvName() + "=/no-home",
9090
"GOARCH=" + runtime.GOARCH,
9191
"GOCACHE=" + testGOCACHE,
@@ -702,7 +702,7 @@ func (ts *testScript) check(err error) {
702702
// exec runs the given command line (an actual subprocess, not simulated)
703703
// in ts.cd with environment ts.env and then returns collected standard output and standard error.
704704
func (ts *testScript) exec(command string, args ...string) (stdout, stderr string, err error) {
705-
cmd := exec.Command(testGo, args...)
705+
cmd := exec.Command(command, args...)
706706
cmd.Dir = ts.cd
707707
cmd.Env = append(ts.env, "PWD="+ts.cd)
708708
var stdoutBuf, stderrBuf strings.Builder
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
# go/build's Import should find modules by invoking the go command
2+
3+
go build -o $WORK/testimport.exe ./testimport
4+
5+
# GO111MODULE=off
6+
env GO111MODULE=off
7+
! exec $WORK/testimport.exe x/y/z/w .
8+
9+
# GO111MODULE=auto in GOPATH/src
10+
env GO111MODULE=
11+
! exec $WORK/testimport.exe x/y/z/w .
12+
env GO111MODULE=auto
13+
! exec $WORK/testimport.exe x/y/z/w .
14+
15+
# GO111MODULE=auto outside GOPATH/src
16+
cd $GOPATH/other
17+
env GO111MODULE=
18+
exec $WORK/testimport.exe other/x/y/z/w .
19+
stdout w2.go
20+
21+
! exec $WORK/testimport.exe x/y/z/w .
22+
stderr 'cannot find module providing package x/y/z/w'
23+
24+
cd z
25+
env GO111MODULE=auto
26+
exec $WORK/testimport.exe other/x/y/z/w .
27+
stdout w2.go
28+
29+
# GO111MODULE=on outside GOPATH/src
30+
env GO111MODULE=on
31+
exec $WORK/testimport.exe other/x/y/z/w .
32+
stdout w2.go
33+
34+
# GO111MODULE=on in GOPATH/src
35+
cd $GOPATH/src
36+
exec $WORK/testimport.exe x/y/z/w .
37+
stdout w1.go
38+
cd w
39+
exec $WORK/testimport.exe x/y/z/w ..
40+
stdout w1.go
41+
42+
-- go.mod --
43+
module x/y/z
44+
45+
-- z.go --
46+
package z
47+
48+
-- w/w1.go --
49+
package w
50+
51+
-- testimport/x.go --
52+
package main
53+
54+
import (
55+
"fmt"
56+
"go/build"
57+
"log"
58+
"os"
59+
"strings"
60+
)
61+
62+
func main() {
63+
p, err := build.Import(os.Args[1], os.Args[2], 0)
64+
if err != nil {
65+
log.Fatal(err)
66+
}
67+
fmt.Printf("%s\n%s\n", p.Dir, strings.Join(p.GoFiles, " "))
68+
}
69+
70+
-- $GOPATH/other/go.mod --
71+
module other/x/y
72+
73+
-- $GOPATH/other/z/w/w2.go --
74+
package w

src/go/build/build.go

+122-1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import (
1616
"io/ioutil"
1717
"log"
1818
"os"
19+
"os/exec"
1920
pathpkg "path"
2021
"path/filepath"
2122
"runtime"
@@ -277,6 +278,8 @@ func defaultGOPATH() string {
277278
return ""
278279
}
279280

281+
var defaultReleaseTags []string
282+
280283
func defaultContext() Context {
281284
var c Context
282285

@@ -297,6 +300,8 @@ func defaultContext() Context {
297300
c.ReleaseTags = append(c.ReleaseTags, "go1."+strconv.Itoa(i))
298301
}
299302

303+
defaultReleaseTags = append([]string{}, c.ReleaseTags...) // our own private copy
304+
300305
env := os.Getenv("CGO_ENABLED")
301306
if env == "" {
302307
env = defaultCGO_ENABLED
@@ -583,13 +588,19 @@ func (ctxt *Context) Import(path string, srcDir string, mode ImportMode) (*Packa
583588
return p, fmt.Errorf("import %q: cannot import absolute path", path)
584589
}
585590

591+
gopath := ctxt.gopath() // needed by both importGo and below; avoid computing twice
592+
if err := ctxt.importGo(p, path, srcDir, mode, gopath); err == nil {
593+
goto Found
594+
} else if err != errNoModules {
595+
return p, err
596+
}
597+
586598
// tried records the location of unsuccessful package lookups
587599
var tried struct {
588600
vendor []string
589601
goroot string
590602
gopath []string
591603
}
592-
gopath := ctxt.gopath()
593604

594605
// Vendor directories get first chance to satisfy import.
595606
if mode&IgnoreVendor == 0 && srcDir != "" {
@@ -930,6 +941,116 @@ Found:
930941
return p, pkgerr
931942
}
932943

944+
var errNoModules = errors.New("not using modules")
945+
946+
// importGo checks whether it can use the go command to find the directory for path.
947+
// If using the go command is not appopriate, importGo returns errNoModules.
948+
// Otherwise, importGo tries using the go command and reports whether that succeeded.
949+
// Using the go command lets build.Import and build.Context.Import find code
950+
// in Go modules. In the long term we want tools to use go/packages (currently golang.org/x/tools/go/packages),
951+
// which will also use the go command.
952+
// Invoking the go command here is not very efficient in that it computes information
953+
// about the requested package and all dependencies and then only reports about the requested package.
954+
// Then we reinvoke it for every dependency. But this is still better than not working at all.
955+
// See golang.org/issue/26504.
956+
func (ctxt *Context) importGo(p *Package, path, srcDir string, mode ImportMode, gopath []string) error {
957+
const debugImportGo = false
958+
959+
// To invoke the go command, we must know the source directory,
960+
// we must not being doing special things like AllowBinary or IgnoreVendor,
961+
// and all the file system callbacks must be nil (we're meant to use the local file system).
962+
if srcDir == "" || mode&AllowBinary != 0 || mode&IgnoreVendor != 0 ||
963+
ctxt.JoinPath != nil || ctxt.SplitPathList != nil || ctxt.IsAbsPath != nil || ctxt.IsDir != nil || ctxt.HasSubdir != nil || ctxt.ReadDir != nil || ctxt.OpenFile != nil || !equal(ctxt.ReleaseTags, defaultReleaseTags) {
964+
return errNoModules
965+
}
966+
967+
// If modules are not enabled, then the in-process code works fine and we should keep using it.
968+
switch os.Getenv("GO111MODULE") {
969+
case "off":
970+
return errNoModules
971+
case "on":
972+
// ok
973+
default: // "", "auto", anything else
974+
// Automatic mode: no module use in $GOPATH/src.
975+
for _, root := range gopath {
976+
sub, ok := ctxt.hasSubdir(root, srcDir)
977+
if ok && strings.HasPrefix(sub, "src/") {
978+
return errNoModules
979+
}
980+
}
981+
}
982+
983+
// For efficiency, if path is a standard library package, let the usual lookup code handle it.
984+
if ctxt.GOROOT != "" {
985+
dir := ctxt.joinPath(ctxt.GOROOT, "src", path)
986+
if ctxt.isDir(dir) {
987+
return errNoModules
988+
}
989+
}
990+
991+
// Look to see if there is a go.mod.
992+
abs, err := filepath.Abs(srcDir)
993+
if err != nil {
994+
return errNoModules
995+
}
996+
for {
997+
info, err := os.Stat(filepath.Join(abs, "go.mod"))
998+
if err == nil && !info.IsDir() {
999+
break
1000+
}
1001+
d := filepath.Dir(abs)
1002+
if len(d) >= len(abs) {
1003+
return errNoModules // reached top of file system, no go.mod
1004+
}
1005+
abs = d
1006+
}
1007+
1008+
cmd := exec.Command("go", "list", "-compiler="+ctxt.Compiler, "-tags="+strings.Join(ctxt.BuildTags, ","), "-installsuffix="+ctxt.InstallSuffix, "-f={{.Dir}}\n{{.ImportPath}}\n{{.Root}}\n{{.Goroot}}\n", path)
1009+
cmd.Dir = srcDir
1010+
var stdout, stderr strings.Builder
1011+
cmd.Stdout = &stdout
1012+
cmd.Stderr = &stderr
1013+
1014+
cgo := "0"
1015+
if ctxt.CgoEnabled {
1016+
cgo = "1"
1017+
}
1018+
cmd.Env = append(os.Environ(),
1019+
"GOOS="+ctxt.GOOS,
1020+
"GOARCH="+ctxt.GOARCH,
1021+
"GOROOT="+ctxt.GOROOT,
1022+
"GOPATH="+ctxt.GOPATH,
1023+
"CGO_ENABLED="+cgo,
1024+
)
1025+
1026+
if err := cmd.Run(); err != nil {
1027+
return fmt.Errorf("go/build: importGo %s: %v\n%s\n", path, err, stderr.String())
1028+
}
1029+
1030+
f := strings.Split(stdout.String(), "\n")
1031+
if len(f) != 5 || f[4] != "" {
1032+
return fmt.Errorf("go/build: importGo %s: unexpected output:\n%s\n", path, stdout.String())
1033+
}
1034+
1035+
p.Dir = f[0]
1036+
p.ImportPath = f[1]
1037+
p.Root = f[2]
1038+
p.Goroot = f[3] == "true"
1039+
return nil
1040+
}
1041+
1042+
func equal(x, y []string) bool {
1043+
if len(x) != len(y) {
1044+
return false
1045+
}
1046+
for i, xi := range x {
1047+
if xi != y[i] {
1048+
return false
1049+
}
1050+
}
1051+
return true
1052+
}
1053+
9331054
// hasGoFiles reports whether dir contains any files with names ending in .go.
9341055
// For a vendor check we must exclude directories that contain no .go files.
9351056
// Otherwise it is not possible to vendor just a/b/c and still import the

0 commit comments

Comments
 (0)