diff --git a/context.go b/context.go index 6ce18d0689..2e1c800255 100644 --- a/context.go +++ b/context.go @@ -17,7 +17,8 @@ import ( // Ctx defines the supporting context of the tool. type Ctx struct { - GOPATH string // Go path + GOPATH string // Selected Go path + GOPATHS []string // Other Go paths } // NewContext creates a struct with the project's GOPATH. It assumes @@ -26,18 +27,28 @@ func NewContext() (*Ctx, error) { // this way we get the default GOPATH that was added in 1.8 buildContext := build.Default wd, err := os.Getwd() + if err != nil { return nil, errors.Wrap(err, "getting work directory") } wd = filepath.FromSlash(wd) + ctx := &Ctx{} + for _, gp := range filepath.SplitList(buildContext.GOPATH) { gp = filepath.FromSlash(gp) + if filepath.HasPrefix(wd, gp) { - return &Ctx{GOPATH: gp}, nil + ctx.GOPATH = gp } + + ctx.GOPATHS = append(ctx.GOPATHS, gp) } - return nil, errors.New("project not in a GOPATH") + if ctx.GOPATH == "" { + return nil, errors.New("project not in a GOPATH") + } + + return ctx, nil } func (c *Ctx) SourceManager() (*gps.SourceMgr, error) { @@ -74,6 +85,13 @@ func (c *Ctx) LoadProject(path string) (*Project, error) { return nil, err } + // The path may lie within a symlinked directory, resolve the path + // before moving forward + p.AbsRoot, err = c.resolveProjectRoot(p.AbsRoot) + if err != nil { + return nil, errors.Wrapf(err, "resolve project root") + } + ip, err := c.SplitAbsoluteProjectRoot(p.AbsRoot) if err != nil { return nil, errors.Wrap(err, "split absolute project root") @@ -117,6 +135,43 @@ func (c *Ctx) LoadProject(path string) (*Project, error) { return p, nil } +// resolveProjectRoot evaluates the root directory and does the following: +// +// If the passed path is a symlink outside GOPATH to a directory within a +// GOPATH, the resolved full real path is returned. +// +// If the passed path is a symlink within a GOPATH, we return an error. +// +// If the passed path isn't a symlink at all, we just pass through. +func (c *Ctx) resolveProjectRoot(path string) (string, error) { + // Determine if this path is a Symlink + l, err := os.Lstat(path) + if err != nil { + return "", errors.Wrap(err, "resolveProjectRoot") + } + + // Pass through if not + if l.Mode()&os.ModeSymlink == 0 { + return path, nil + } + + // Resolve path + resolved, err := filepath.EvalSymlinks(path) + if err != nil { + return "", errors.Wrap(err, "resolveProjectRoot") + } + + // Determine if the symlink is within any of the GOPATHs, in which case we're not + // sure how to resolve it. + for _, gp := range c.GOPATHS { + if filepath.HasPrefix(path, gp) { + return "", errors.Errorf("'%s' is linked to another path within a GOPATH (%s)", path, gp) + } + } + + return resolved, nil +} + // SplitAbsoluteProjectRoot takes an absolute path and compares it against declared // GOPATH(s) to determine what portion of the input path should be treated as an // import path - as a project root. diff --git a/context_test.go b/context_test.go index e71c0a879d..753d05c863 100644 --- a/context_test.go +++ b/context_test.go @@ -38,6 +38,7 @@ func TestSplitAbsoluteProjectRoot(t *testing.T) { defer h.Cleanup() h.TempDir("src") + h.Setenv("GOPATH", h.Path(".")) depCtx := &Ctx{GOPATH: h.Path(".")} @@ -346,3 +347,74 @@ func TestCaseInsentitiveGOPATH(t *testing.T) { t.Fatalf("expected %s, got %s", ip, pr) } } + +func TestResolveProjectRoot(t *testing.T) { + tg := test.NewHelper(t) + defer tg.Cleanup() + + tg.TempDir("go") + tg.TempDir("go/src") + tg.TempDir("go/src/real") + tg.TempDir("go/src/real/path") + tg.TempDir("go/src/sym") + + tg.TempDir("gotwo") // Another directory used as a GOPATH + tg.TempDir("gotwo/src") + tg.TempDir("gotwo/src/real") + tg.TempDir("gotwo/src/real/path") + tg.TempDir("gotwo/src/sym") + + tg.TempDir("sym") // Directory for symlinks + + tg.Setenv("GOPATH", tg.Path(filepath.Join(".", "go"))) + + ctx := &Ctx{ + GOPATH: tg.Path(filepath.Join(".", "go")), + GOPATHS: []string{ + tg.Path(filepath.Join(".", "go")), + tg.Path(filepath.Join(".", "gotwo")), + }, + } + + realPath := filepath.Join(ctx.GOPATH, "src", "real", "path") + realPathTwo := filepath.Join(ctx.GOPATHS[1], "src", "real", "path") + symlinkedPath := filepath.Join(tg.Path("."), "sym", "symlink") + symlinkedInGoPath := filepath.Join(ctx.GOPATH, "src/sym/path") + symlinkedInOtherGoPath := filepath.Join(tg.Path("."), "sym", "symtwo") + os.Symlink(realPath, symlinkedPath) + os.Symlink(realPath, symlinkedInGoPath) + os.Symlink(realPathTwo, symlinkedInOtherGoPath) + + // Real path should be returned, no symlinks to deal with + p, err := ctx.resolveProjectRoot(realPath) + if err != nil { + t.Fatalf("Error resolving project root: %s", err) + } + if p != realPath { + t.Fatalf("Want path to be %s, got %s", realPath, p) + } + + // Real path should be returned, symlink is outside GOPATH + p, err = ctx.resolveProjectRoot(symlinkedPath) + if err != nil { + t.Fatalf("Error resolving project root: %s", err) + } + if p != realPath { + t.Fatalf("Want path to be %s, got %s", realPath, p) + } + + // Real path should be returned, symlink is in another GOPATH + p, err = ctx.resolveProjectRoot(symlinkedInOtherGoPath) + if err != nil { + t.Fatalf("Error resolving project root: %s", err) + } + if p != realPathTwo { + t.Fatalf("Want path to be %s, got %s", realPathTwo, p) + } + + // Symlinked path is inside GOPATH, should return error + _, err = ctx.resolveProjectRoot(symlinkedInGoPath) + if err == nil { + t.Fatalf("Wanted an error") + } +}