Skip to content

Commit 183cc0c

Browse files
committed
cmd/go: add preliminary support for vendor directories
When GO15VENDOREXPERIMENT=1 is in the environment, this CL changes the resolution of import paths according to the Go 1.5 vendor proposal: If there is a source directory d/vendor, then, when compiling a source file within the subtree rooted at d, import "p" is interpreted as import "d/vendor/p" if that exists. When there are multiple possible resolutions, the most specific (longest) path wins. The short form must always be used: no import path can contain “/vendor/” explicitly. Import comments are ignored in vendored packages. The goal of these changes is to allow authors to vendor (copy) external packages into their source trees without any modifications to the code. This functionality has been achieved in tools like godep, nut, and gb by requiring GOPATH manipulation. This alternate directory-based approach eliminates the need for GOPATH manipulation and in keeping with the go command's use of directory layout-based configuration. The flag allows experimentation with these vendoring semantics once Go 1.5 is released, without forcing them on by default. If the experiment is deemed a success, the flag will default to true in Go 1.6 and then be removed in Go 1.7. For more details, see the original proposal by Keith Rarick at https://groups.google.com/d/msg/golang-dev/74zjMON9glU/dGhnoi2IMzsJ. Change-Id: I2c6527e777d14ac6dc43c53e4b3ff24f3279216e Reviewed-on: https://go-review.googlesource.com/10923 Reviewed-by: Andrew Gerrand <[email protected]>
1 parent 7bcc6a1 commit 183cc0c

File tree

17 files changed

+403
-11
lines changed

17 files changed

+403
-11
lines changed

src/cmd/go/build.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2135,6 +2135,14 @@ func (gcToolchain) gc(b *builder, p *Package, archive, obj string, asmhdr bool,
21352135
gcargs = append(gcargs, "-buildid", p.buildID)
21362136
}
21372137

2138+
for _, path := range p.Imports {
2139+
if i := strings.LastIndex(path, "/vendor/"); i >= 0 {
2140+
gcargs = append(gcargs, "-importmap", path[i+len("/vendor/"):]+"="+path)
2141+
} else if strings.HasPrefix(path, "vendor/") {
2142+
gcargs = append(gcargs, "-importmap", path[len("vendor/"):]+"="+path)
2143+
}
2144+
}
2145+
21382146
args := []interface{}{buildToolExec, tool("compile"), "-o", ofile, "-trimpath", b.work, buildGcflags, gcargs, "-D", p.localPrefix, importArgs}
21392147
if ofile == archive {
21402148
args = append(args, "-pack")

src/cmd/go/pkg.go

Lines changed: 213 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,12 @@ func reloadPackage(arg string, stk *importStack) *Package {
215215
return loadPackage(arg, stk)
216216
}
217217

218+
// The Go 1.5 vendoring experiment is enabled by setting GO15VENDOREXPERIMENT=1.
219+
// The variable is obnoxiously long so that years from now when people find it in
220+
// their profiles and wonder what it does, there is some chance that a web search
221+
// might answer the question.
222+
var go15VendorExperiment = os.Getenv("GO15VENDOREXPERIMENT") == "1"
223+
218224
// dirToImportPath returns the pseudo-import path we use for a package
219225
// outside the Go path. It begins with _/ and then contains the full path
220226
// to the directory. If the package lives in c:\home\gopher\my\pkg then
@@ -239,23 +245,33 @@ func makeImportValid(r rune) rune {
239245
// but possibly a local import path (an absolute file system path or one beginning
240246
// with ./ or ../). A local relative path is interpreted relative to srcDir.
241247
// It returns a *Package describing the package found in that directory.
242-
func loadImport(path string, srcDir string, stk *importStack, importPos []token.Position) *Package {
248+
func loadImport(path, srcDir string, parent *Package, stk *importStack, importPos []token.Position) *Package {
243249
stk.push(path)
244250
defer stk.pop()
245251

246252
// Determine canonical identifier for this package.
247253
// For a local import the identifier is the pseudo-import path
248254
// we create from the full directory to the package.
249255
// Otherwise it is the usual import path.
256+
// For vendored imports, it is the expanded form.
250257
importPath := path
258+
origPath := path
251259
isLocal := build.IsLocalImport(path)
260+
var vendorSearch []string
252261
if isLocal {
253262
importPath = dirToImportPath(filepath.Join(srcDir, path))
263+
} else {
264+
path, vendorSearch = vendoredImportPath(parent, path)
265+
importPath = path
254266
}
267+
255268
if p := packageCache[importPath]; p != nil {
256269
if perr := disallowInternal(srcDir, p, stk); perr != p {
257270
return perr
258271
}
272+
if perr := disallowVendor(srcDir, origPath, p, stk); perr != p {
273+
return perr
274+
}
259275
return reusePackage(p, stk)
260276
}
261277

@@ -271,11 +287,33 @@ func loadImport(path string, srcDir string, stk *importStack, importPos []token.
271287
// TODO: After Go 1, decide when to pass build.AllowBinary here.
272288
// See issue 3268 for mistakes to avoid.
273289
bp, err := buildContext.Import(path, srcDir, build.ImportComment)
290+
291+
// If we got an error from go/build about package not found,
292+
// it contains the directories from $GOROOT and $GOPATH that
293+
// were searched. Add to that message the vendor directories
294+
// that were searched.
295+
if err != nil && len(vendorSearch) > 0 {
296+
// NOTE(rsc): The direct text manipulation here is fairly awful,
297+
// but it avoids defining new go/build API (an exported error type)
298+
// late in the Go 1.5 release cycle. If this turns out to be a more general
299+
// problem we could define a real error type when the decision can be
300+
// considered more carefully.
301+
text := err.Error()
302+
if strings.Contains(text, "cannot find package \"") && strings.Contains(text, "\" in any of:\n\t") {
303+
old := strings.SplitAfter(text, "\n")
304+
lines := []string{old[0]}
305+
for _, dir := range vendorSearch {
306+
lines = append(lines, "\t"+dir+" (vendor tree)\n")
307+
}
308+
lines = append(lines, old[1:]...)
309+
err = errors.New(strings.Join(lines, ""))
310+
}
311+
}
274312
bp.ImportPath = importPath
275313
if gobin != "" {
276314
bp.BinDir = gobin
277315
}
278-
if err == nil && !isLocal && bp.ImportComment != "" && bp.ImportComment != path {
316+
if err == nil && !isLocal && bp.ImportComment != "" && bp.ImportComment != path && (!go15VendorExperiment || !strings.Contains(path, "/vendor/")) {
279317
err = fmt.Errorf("code in directory %s expects import %q", bp.Dir, bp.ImportComment)
280318
}
281319
p.load(stk, bp, err)
@@ -288,10 +326,81 @@ func loadImport(path string, srcDir string, stk *importStack, importPos []token.
288326
if perr := disallowInternal(srcDir, p, stk); perr != p {
289327
return perr
290328
}
329+
if perr := disallowVendor(srcDir, origPath, p, stk); perr != p {
330+
return perr
331+
}
291332

292333
return p
293334
}
294335

336+
var isDirCache = map[string]bool{}
337+
338+
func isDir(path string) bool {
339+
result, ok := isDirCache[path]
340+
if ok {
341+
return result
342+
}
343+
344+
fi, err := os.Stat(path)
345+
result = err == nil && fi.IsDir()
346+
isDirCache[path] = result
347+
return result
348+
}
349+
350+
// vendoredImportPath returns the expansion of path when it appears in parent.
351+
// If parent is x/y/z, then path might expand to x/y/z/vendor/path, x/y/vendor/path,
352+
// x/vendor/path, vendor/path, or else stay x/y/z if none of those exist.
353+
// vendoredImportPath returns the expanded path or, if no expansion is found, the original.
354+
// If no epxansion is found, vendoredImportPath also returns a list of vendor directories
355+
// it searched along the way, to help prepare a useful error message should path turn
356+
// out not to exist.
357+
func vendoredImportPath(parent *Package, path string) (found string, searched []string) {
358+
if parent == nil || !go15VendorExperiment {
359+
return path, nil
360+
}
361+
dir := filepath.Clean(parent.Dir)
362+
root := filepath.Clean(parent.Root)
363+
if !strings.HasPrefix(dir, root) || len(dir) <= len(root) || dir[len(root)] != filepath.Separator {
364+
fatalf("invalid vendoredImportPath: dir=%q root=%q separator=%q", dir, root, string(filepath.Separator))
365+
}
366+
vpath := "vendor/" + path
367+
for i := len(dir); i >= len(root); i-- {
368+
if i < len(dir) && dir[i] != filepath.Separator {
369+
continue
370+
}
371+
// Note: checking for the vendor directory before checking
372+
// for the vendor/path directory helps us hit the
373+
// isDir cache more often. It also helps us prepare a more useful
374+
// list of places we looked, to report when an import is not found.
375+
if !isDir(filepath.Join(dir[:i], "vendor")) {
376+
continue
377+
}
378+
targ := filepath.Join(dir[:i], vpath)
379+
if isDir(targ) {
380+
// We started with parent's dir c:\gopath\src\foo\bar\baz\quux\xyzzy.
381+
// We know the import path for parent's dir.
382+
// We chopped off some number of path elements and
383+
// added vendor\path to produce c:\gopath\src\foo\bar\baz\vendor\path.
384+
// Now we want to know the import path for that directory.
385+
// Construct it by chopping the same number of path elements
386+
// (actually the same number of bytes) from parent's import path
387+
// and then append /vendor/path.
388+
chopped := len(dir) - i
389+
if chopped == len(parent.ImportPath)+1 {
390+
// We walked up from c:\gopath\src\foo\bar
391+
// and found c:\gopath\src\vendor\path.
392+
// We chopped \foo\bar (length 8) but the import path is "foo/bar" (length 7).
393+
// Use "vendor/path" without any prefix.
394+
return vpath, nil
395+
}
396+
return parent.ImportPath[:len(parent.ImportPath)-chopped] + "/" + vpath, nil
397+
}
398+
// Note the existence of a vendor directory in case path is not found anywhere.
399+
searched = append(searched, targ)
400+
}
401+
return path, searched
402+
}
403+
295404
// reusePackage reuses package p to satisfy the import at the top
296405
// of the import stack stk. If this use causes an import loop,
297406
// reusePackage updates p's error information to record the loop.
@@ -384,6 +493,101 @@ func findInternal(path string) (index int, ok bool) {
384493
return 0, false
385494
}
386495

496+
// disallowVendor checks that srcDir is allowed to import p as path.
497+
// If the import is allowed, disallowVendor returns the original package p.
498+
// If not, it returns a new package containing just an appropriate error.
499+
func disallowVendor(srcDir, path string, p *Package, stk *importStack) *Package {
500+
if !go15VendorExperiment {
501+
return p
502+
}
503+
504+
// The stack includes p.ImportPath.
505+
// If that's the only thing on the stack, we started
506+
// with a name given on the command line, not an
507+
// import. Anything listed on the command line is fine.
508+
if len(*stk) == 1 {
509+
return p
510+
}
511+
512+
if perr := disallowVendorVisibility(srcDir, p, stk); perr != p {
513+
return perr
514+
}
515+
516+
// Paths like x/vendor/y must be imported as y, never as x/vendor/y.
517+
if i, ok := findVendor(path); ok {
518+
perr := *p
519+
perr.Error = &PackageError{
520+
ImportStack: stk.copy(),
521+
Err: "must be imported as " + path[i+len("vendor/"):],
522+
}
523+
perr.Incomplete = true
524+
return &perr
525+
}
526+
527+
return p
528+
}
529+
530+
// disallowVendorVisibility checks that srcDir is allowed to import p.
531+
// The rules are the same as for /internal/ except that a path ending in /vendor
532+
// is not subject to the rules, only subdirectories of vendor.
533+
// This allows people to have packages and commands named vendor,
534+
// for maximal compatibility with existing source trees.
535+
func disallowVendorVisibility(srcDir string, p *Package, stk *importStack) *Package {
536+
// The stack includes p.ImportPath.
537+
// If that's the only thing on the stack, we started
538+
// with a name given on the command line, not an
539+
// import. Anything listed on the command line is fine.
540+
if len(*stk) == 1 {
541+
return p
542+
}
543+
544+
// Check for "vendor" element.
545+
i, ok := findVendor(p.ImportPath)
546+
if !ok {
547+
return p
548+
}
549+
550+
// Vendor is present.
551+
// Map import path back to directory corresponding to parent of vendor.
552+
if i > 0 {
553+
i-- // rewind over slash in ".../vendor"
554+
}
555+
parent := p.Dir[:i+len(p.Dir)-len(p.ImportPath)]
556+
if hasPathPrefix(filepath.ToSlash(srcDir), filepath.ToSlash(parent)) {
557+
return p
558+
}
559+
560+
// Vendor is present, and srcDir is outside parent's tree. Not allowed.
561+
perr := *p
562+
perr.Error = &PackageError{
563+
ImportStack: stk.copy(),
564+
Err: "use of vendored package not allowed",
565+
}
566+
perr.Incomplete = true
567+
return &perr
568+
}
569+
570+
// findVendor looks for the last non-terminating "vendor" path element in the given import path.
571+
// If there isn't one, findVendor returns ok=false.
572+
// Otherwise, findInternal returns ok=true and the index of the "vendor".
573+
//
574+
// Note that terminating "vendor" elements don't count: "x/vendor" is its own package,
575+
// not the vendored copy of an import "" (the empty import path).
576+
// This will allow people to have packages or commands named vendor.
577+
// This may help reduce breakage, or it may just be confusing. We'll see.
578+
func findVendor(path string) (index int, ok bool) {
579+
// Two cases, depending on internal at start of string or not.
580+
// The order matters: we must return the index of the final element,
581+
// because the final one is where the effective import path starts.
582+
switch {
583+
case strings.Contains(path, "/vendor/"):
584+
return strings.LastIndex(path, "/vendor/") + 1, true
585+
case strings.HasPrefix(path, "vendor/"):
586+
return 0, true
587+
}
588+
return 0, false
589+
}
590+
387591
type targetDir int
388592

389593
const (
@@ -630,7 +834,7 @@ func (p *Package) load(stk *importStack, bp *build.Package, err error) *Package
630834
if path == "C" {
631835
continue
632836
}
633-
p1 := loadImport(path, p.Dir, stk, p.build.ImportPos[path])
837+
p1 := loadImport(path, p.Dir, p, stk, p.build.ImportPos[path])
634838
if p1.Name == "main" {
635839
p.Error = &PackageError{
636840
ImportStack: stk.copy(),
@@ -652,8 +856,11 @@ func (p *Package) load(stk *importStack, bp *build.Package, err error) *Package
652856
p.Error.Pos = pos[0].String()
653857
}
654858
}
655-
path = p1.ImportPath
656-
importPaths[i] = path
859+
}
860+
path = p1.ImportPath
861+
importPaths[i] = path
862+
if i < len(p.Imports) {
863+
p.Imports[i] = path
657864
}
658865
deps[path] = p1
659866
imports = append(imports, p1)
@@ -1294,7 +1501,7 @@ func loadPackage(arg string, stk *importStack) *Package {
12941501
}
12951502
}
12961503

1297-
return loadImport(arg, cwd, stk, nil)
1504+
return loadImport(arg, cwd, nil, stk, nil)
12981505
}
12991506

13001507
// packages returns the packages named by the

src/cmd/go/test.go

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -573,8 +573,8 @@ func (b *builder) test(p *Package) (buildAction, runAction, printAction *action,
573573
var imports, ximports []*Package
574574
var stk importStack
575575
stk.push(p.ImportPath + " (test)")
576-
for _, path := range p.TestImports {
577-
p1 := loadImport(path, p.Dir, &stk, p.build.TestImportPos[path])
576+
for i, path := range p.TestImports {
577+
p1 := loadImport(path, p.Dir, p, &stk, p.build.TestImportPos[path])
578578
if p1.Error != nil {
579579
return nil, nil, nil, p1.Error
580580
}
@@ -589,21 +589,23 @@ func (b *builder) test(p *Package) (buildAction, runAction, printAction *action,
589589
}
590590
return nil, nil, nil, err
591591
}
592+
p.TestImports[i] = p1.ImportPath
592593
imports = append(imports, p1)
593594
}
594595
stk.pop()
595596
stk.push(p.ImportPath + "_test")
596597
pxtestNeedsPtest := false
597-
for _, path := range p.XTestImports {
598+
for i, path := range p.XTestImports {
598599
if path == p.ImportPath {
599600
pxtestNeedsPtest = true
600601
continue
601602
}
602-
p1 := loadImport(path, p.Dir, &stk, p.build.XTestImportPos[path])
603+
p1 := loadImport(path, p.Dir, p, &stk, p.build.XTestImportPos[path])
603604
if p1.Error != nil {
604605
return nil, nil, nil, p1.Error
605606
}
606607
ximports = append(ximports, p1)
608+
p.XTestImports[i] = p1.ImportPath
607609
}
608610
stk.pop()
609611

@@ -728,7 +730,7 @@ func (b *builder) test(p *Package) (buildAction, runAction, printAction *action,
728730
if dep == ptest.ImportPath {
729731
pmain.imports = append(pmain.imports, ptest)
730732
} else {
731-
p1 := loadImport(dep, "", &stk, nil)
733+
p1 := loadImport(dep, "", nil, &stk, nil)
732734
if p1.Error != nil {
733735
return nil, nil, nil, p1.Error
734736
}

src/cmd/go/testdata/src/vend/bad.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
package vend
2+
3+
import _ "r"

src/cmd/go/testdata/src/vend/good.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
package vend
2+
3+
import _ "p"
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
"strings" // really ../vendor/strings
6+
)
7+
8+
func main() {
9+
fmt.Printf("%s\n", strings.Msg)
10+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package main
2+
3+
import (
4+
"strings" // really ../vendor/strings
5+
"testing"
6+
)
7+
8+
func TestMsgInternal(t *testing.T) {
9+
if strings.Msg != "hello, world" {
10+
t.Fatal("unexpected msg: %v", strings.Msg)
11+
}
12+
}

0 commit comments

Comments
 (0)