Skip to content
This repository was archived by the owner on Sep 9, 2020. It is now read-only.

Handling symlinks as project root #641

Merged
merged 24 commits into from
Jun 17, 2017
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
3b21f6e
update FAQ about symlinks
ibrasho Jun 3, 2017
9730410
internal/fs: add ResolvePath function
ibrasho Jun 3, 2017
91ac4d2
dep: update NewContext(), rename ResolveProjectRoot() to ResolveProje…
ibrasho Jun 3, 2017
3be59e3
dep: minor fixes & tweaks in context.go
ibrasho Jun 3, 2017
6a464ac
dep: update letter casing to detectGOPATH() and ResolveProjectRootAnd…
ibrasho Jun 6, 2017
9675932
Minor text edits
ibrasho Jun 6, 2017
b7048cc
Merge remote-tracking branch 'upstream/master' into symlink-project-r…
ibrasho Jun 12, 2017
28b6f86
internal/fs: Remove fs.ResolvePath()
ibrasho Jun 12, 2017
22cd768
fix tests
ibrasho Jun 12, 2017
5bf9205
dep: Add project.ResolvedAbsRoot()
ibrasho Jun 13, 2017
71a2607
dep: Add dep.NewProject() and add ResolveAbsRoot to dep.Project
ibrasho Jun 14, 2017
929ef28
dep/cmd/dep: Update init.go to use dep.Project
ibrasho Jun 14, 2017
cd9bea6
dep: add Ctx.SetPaths() ctx.DetectProjectGOPATH()
ibrasho Jun 14, 2017
6595001
Merge branch 'master' into symlink-project-roots-changes
ibrasho Jun 14, 2017
9166034
dep: Add *Project.SetRoot() instead of NewProject()
ibrasho Jun 14, 2017
5a3291a
dep: small nits in context.go
ibrasho Jun 14, 2017
0b8edb1
Ensure dep reads GOPATH from Config.Env
ibrasho Jun 15, 2017
026038a
Update docs for dep.Ctx. Ctx.SetPaths and Ctx.DetectProjectGOPATH
ibrasho Jun 16, 2017
b98d713
Tweak dep.Project.SetRoot to ensure consistent internal state
ibrasho Jun 16, 2017
138cebb
dep: Update godoc to use // instead of /* ... */
ibrasho Jun 17, 2017
3826bb0
dep: Simplify dep.*Project.SetRoot
ibrasho Jun 17, 2017
f4b9f49
dep: Fix godoc for dep.*Ctx.DetectProjectGOPATH
ibrasho Jun 17, 2017
a69d6ae
dep: update tests for dep.*Ctx.DetectProjectGOPATH
ibrasho Jun 17, 2017
13f512f
dep: cleanup TestLoadProject and TestDetectProjectGOPATH
ibrasho Jun 17, 2017
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 7 additions & 4 deletions FAQ.md
Original file line number Diff line number Diff line change
Expand Up @@ -189,11 +189,14 @@ There's another major performance issue that's much harder - the process of pick
> because we're not crazy people who delight in inviting chaos into our lives, we need to work within one GOPATH at a time.
-[@sdboyer in #247](https://github.com/golang/dep/pull/247#issuecomment-284181879)

Out of convenience, one might create a symlink to a directory within their `GOPATH`, e.g. `ln -s ~/go/src/github.com/golang/dep dep`. When `dep` is invoked it will resolve the current working directory accordingly:
Out of convenience, one might create a symlink to a directory within their `GOPATH/src`, e.g. `ln -s ~/go/src/github.com/user/awesome-project ~/Code/awesome-project`.

- If the cwd is a symlink outside a `GOPATH` and links to directory within a `GOPATH`, or vice versa, `dep` chooses whichever path is within the `GOPATH`. If neither path is within a `GOPATH`, `dep` produces an error.
- If both the cwd and resolved path are in the same `GOPATH`, an error is thrown since the users intentions and expectations can't be accurately deduced.
- If the symlink is within a `GOPATH` and the real path is within a *different* `GOPATH` - an error is thrown.
When `dep` is invoked with a project root that is a symlink, it will be resolved according to the following rules:

- If the symlink is outside `GOPATH` and links to a directory within a `GOPATH`, or vice versa, then `dep` will choose whichever path is within `GOPATH`.
- If the symlink is within a `GOPATH` and the resolved path is within a *different* `GOPATH`, then an error is thrown.
- If both the symlink and the resolved path are in the same `GOPATH`, then an error is thrown.
- If both the symlink and the resolved path are not in a `GOPATH`, then an error is thrown.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sentence construction thing:

"If neither the symlink nor the resolved path are in a GOPATH, then an error is thrown".


This is the only symbolic link support that `dep` really intends to provide. In keeping with the general practices of the `go` tool, `dep` tends to either ignore symlinks (when walking) or copy the symlink itself, depending on the filesystem operation being performed.

Expand Down
10 changes: 10 additions & 0 deletions cmd/dep/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,16 @@ func (cmd *initCommand) Run(ctx *dep.Ctx, args []string) error {
}
}

// The root path may lie within a symlinked directory, resolve the path
// before moving forward
var err error
root, ctx.GOPATH, err = ctx.ResolveProjectRootAndGoPath(root)
if err != nil {
return errors.Wrapf(err, "resolve project root")
} else if ctx.GOPATH == "" {
return errors.New("project not within a GOPATH")
}

mf := filepath.Join(root, dep.ManifestName)
lf := filepath.Join(root, dep.LockName)
vpath := filepath.Join(root, "vendor")
Expand Down
7 changes: 1 addition & 6 deletions cmd/dep/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -149,12 +149,7 @@ func (c *Config) Run() (exitCode int) {
}

// Set up the dep context.
ctx, err := dep.NewContext(c.WorkingDir, c.Env, loggers)
if err != nil {
loggers.Err.Println(err)
exitCode = 1
return
}
ctx := dep.NewContext(c.WorkingDir, c.Env, loggers)

// Run the command with the post-flag-processing args.
if err := cmd.Run(ctx, fs.Args()); err != nil {
Expand Down
102 changes: 63 additions & 39 deletions context.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@ import (

// Ctx defines the supporting context of the tool.
type Ctx struct {
GOPATH string // Selected Go path
GOPATHS []string // Other Go paths
WorkingDir string
GOPATHS []string // Other Go paths
GOPATH string // Selected Go path
*Loggers
}

Expand All @@ -32,30 +32,21 @@ type Loggers struct {
Verbose bool
}

// NewContext creates a struct with the project's GOPATH. It assumes
// that of your "GOPATH"'s we want the one we are currently in.
func NewContext(wd string, env []string, loggers *Loggers) (*Ctx, error) {
// NewContext creates a struct with all the environment's GOPATHs.
func NewContext(wd string, env []string, loggers *Loggers) *Ctx {
ctx := &Ctx{WorkingDir: wd, Loggers: loggers}

GOPATH := getEnv(env, "GOPATH")

if GOPATH == "" {
GOPATH = defaultGOPATH()
}
for _, gp := range filepath.SplitList(GOPATH) {
gp = filepath.FromSlash(gp)

if fs.HasFilepathPrefix(filepath.FromSlash(wd), gp) {
ctx.GOPATH = gp
}

ctx.GOPATHS = append(ctx.GOPATHS, gp)
}

if ctx.GOPATH == "" {
return nil, errors.New("project not in a GOPATH")
for _, gp := range filepath.SplitList(GOPATH) {
ctx.GOPATHS = append(ctx.GOPATHS, filepath.FromSlash(gp))
}

return ctx, nil
return ctx
}

// getEnv returns the last instance of an environment variable.
Expand Down Expand Up @@ -116,9 +107,11 @@ func (c *Ctx) LoadProject() (*Project, error) {

// The path may lie within a symlinked directory, resolve the path
// before moving forward
p.AbsRoot, err = c.resolveProjectRoot(p.AbsRoot)
p.AbsRoot, c.GOPATH, err = c.ResolveProjectRootAndGoPath(p.AbsRoot)
if err != nil {
return nil, errors.Wrapf(err, "resolve project root")
} else if c.GOPATH == "" {
return nil, errors.New("project not within a GOPATH")
}

ip, err := c.SplitAbsoluteProjectRoot(p.AbsRoot)
Expand Down Expand Up @@ -168,41 +161,72 @@ func (c *Ctx) LoadProject() (*Project, error) {
return p, nil
}

// resolveProjectRoot evaluates the root directory and does the following:
// ResolveProjectRootAndGoPath evaluates the project root and the containing GOPATH
// by doing 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 path isn't a symlink and is within a GOPATH, path and its GOPATH are returned.
//
// If the passed path is a symlink within a GOPATH, we return an error.
// If path is a symlink not within any GOPATH and resolves to a directory within a
// GOPATH, the resolved path and its GOPATH are returned.
//
// 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)
// ResolveProjectRootAndGoPath will return an error in the following cases:
//
// If path is not a symlink and it's not within any GOPATH.
// If both path and the directory it resolves to are not within any GOPATH.
// If path is a symlink within a GOPATH, an error is returned.
// If both path and the directory it resolves to are within the same GOPATH.
// If path and the directory it resolves to are each within a different GOPATH.
func (c *Ctx) ResolveProjectRootAndGoPath(path string) (string, string, error) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

s/GoPath/GOPATH/

pgp, pgperr := c.detectGoPath(path)

if sym, err := fs.IsSymlink(path); err != nil {
return "", "", errors.Wrap(err, "IsSymlink")
} else if !sym {
// If path is not a symlink and detectGoPath failed, then we assume that path is not
// within a known GOPATH.
if pgperr != nil {
return "", "", errors.Errorf("project root %v not within a GOPATH", path)
}
return path, pgp, nil
}

resolved, err := fs.ResolvePath(path)
if err != nil {
return "", errors.Wrap(err, "resolveProjectRoot")
return "", "", errors.Wrap(err, "resolveProjectRoot")
}

// Pass through if not
if l.Mode()&os.ModeSymlink == 0 {
return path, nil
rgp, rgperr := c.detectGoPath(resolved)
if pgperr != nil && rgperr != nil {
return "", "", errors.Errorf("path %s resolved to %s, both are not within any GOPATH", path, resolved)
}

// Resolve path
resolved, err := filepath.EvalSymlinks(path)
if err != nil {
return "", errors.Wrap(err, "resolveProjectRoot")
// If pgp equals rgp, then both are within the same GOPATH.
if pgp == rgp {
return "", "", errors.Errorf("path %s resolved to %s, both in the same GOPATH %s", path, resolved, pgp)
}

// Determine if the symlink is within any of the GOPATHs, in which case we're not
// sure how to resolve it.
// path and resolved are within different GOPATHs
if pgp != "" && rgp != "" && pgp == rgp {
return "", "", errors.Errorf("path %s resolved to %s, each is in a different GOPATH", path, resolved)
}

// Otherwise, either the symlink or the resolved path is within a GOPATH.
if pgp == "" {
return resolved, rgp, nil
}

return path, pgp, nil
}

// detectGoPath detects the GOPATH for a given path from ctx.GOPATHS.
func (c *Ctx) detectGoPath(path string) (string, error) {
for _, gp := range c.GOPATHS {
if fs.HasFilepathPrefix(path, gp) {
return "", errors.Errorf("'%s' is linked to another path within a GOPATH (%s)", path, gp)
if fs.HasFilepathPrefix(filepath.FromSlash(path), gp) {
return gp, nil
}
}

return resolved, nil
return "", errors.Errorf("Unable to detect GOPATH for %s", path)
}

// SplitAbsoluteProjectRoot takes an absolute path and compares it against declared
Expand Down
Loading