Skip to content
This repository has been 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

Conversation

ibrasho
Copy link
Collaborator

@ibrasho ibrasho commented May 24, 2017

This started as an attempt to clarify the FAQ update done by @brianstarke in #247.
I went to the code to understand how handling symlinks as project root is implemented currently and came to the following conclusion:

  • If the project root (either a regular directory or symlink) is not within a GOPATH, an error will be returned since context.NewContext will return an error if the cwd is not within a GOPATH before context.resolveProjectRoot is reached where symlinks are handled currently. Tests pass currently since they don't go through main like a regular cmd will.

I've tried to rewrite the FAQ answer to provide some clarification:

  • I opted to use project root instead of cwd, since it's more consistent (running dep ensure in a project subdirectory will traverse up to the project root regardless of the cwd).
  • I've updated the implementation to reflect the answer below. Need @sdboyer & @brianstarke to confirm if my understanding and implementation is correct.

How does dep behave if the project root is a symbolic link?

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

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.
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 returned.
  • If both the symlink and the resolved path are in the same GOPATH, then an error is returned.
  • If both the symlink and the resolved path are not in GOPATH, then an error is returned

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.

Fixes #218.

@sdboyer
Copy link
Member

sdboyer commented May 25, 2017

Hmm - it seems we've regressed then, because this definitely did work when we brought in #247. Clearly our tests aren't properly covering this case.

Instead of updating the FAQ (which reflects how it's supposed to be), let's fix the code.

@ibrasho
Copy link
Collaborator Author

ibrasho commented May 25, 2017

As of now, running dep where the project root is a symlink outside GOPATH will print project not in GOPATH and exit. This true for the last commit in #247 (753c8dd).
The error is returned when NewContext() is called (in cmd/dep/main.go) before reaching ResolveProjectRoot(). The tests were covering TestResolveProjectRoot() and that works as expected.


@sdboyer The only change I made to the FAQ (besides trying to clarify it) is the following change:

If neither path is within a GOPATH, dep produces an error.

to

If both the symlink and the resolved path are not in GOPATH, then an error is returned

This doesn't make sense, since determining the GOPATH for this case is not possible (unless we assume it's the cwd regardless). I've updated the FAQ and implementation to reflect this.

Let me know if this should be reverted.

Signed-off-by: Ibrahim AshShohail <ibra.sho@gmail.com>
Signed-off-by: Ibrahim AshShohail <ibra.sho@gmail.com>
…ctRootAndGoPath() and add detectGoPath()

Signed-off-by: Ibrahim AshShohail <ibra.sho@gmail.com>
@ibrasho ibrasho force-pushed the symlink-project-roots-changes branch from f3df625 to 91ac4d2 Compare June 3, 2017 19:53
Signed-off-by: Ibrahim AshShohail <ibra.sho@gmail.com>
Copy link
Member

@sdboyer sdboyer left a comment

Choose a reason for hiding this comment

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

just one little nit right now. unfortunately, i'm out of reviewing juice tonight 😢

context.go Outdated
// 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/

…GOPATH()

Edit letter case of:

* detectGoPath() to detectGOPATH()
* ResolveProjectRootAndGoPath() to ResolveProjectRootAndGOPATH()

Signed-off-by: Ibrahim AshShohail <ibra.sho@gmail.com>
Signed-off-by: Ibrahim AshShohail <ibra.sho@gmail.com>
@ibrasho ibrasho self-assigned this Jun 10, 2017
@ibrasho
Copy link
Collaborator Author

ibrasho commented Jun 10, 2017

This will need more changes since #673 is merged. 😁
I'll update it soon.

@ofpiyush
Copy link

@ibrasho I tried your branch with project root as a symlink to a directory outside GOPATH and got all dirs lacked any go code

A little digging revealed this line to be the culprit.

Maybe have a way to handle symlinked directories in pkgtree as well?
I haven't dug very deep yet, but I can lend a hand on weekends if that helps :)

@sdboyer
Copy link
Member

sdboyer commented Jun 12, 2017

Maybe have a way to handle symlinked directories in pkgtree as well?

@ofpiyush unfortunately, we don't plan on supporting this. It makes clear, consistent behavior vastly more difficult. There have been PRs in the past that attempted it - often going on for weeks, and ultimately resulting in unexpected behaviors that we didn't initially anticipate.

Copy link
Member

@sdboyer sdboyer left a comment

Choose a reason for hiding this comment

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

@ibrasho i'm sorry this keeps sliding - tbh, i have just put so much time into reviewing mind-numbing symlink PRs already that my mind seems to be resisting grokking yet one more. But, I've dug in a bit.

The direction here looks fundamentally correct. The most important point to make, really, is that #247 (comment) remains the closest thing to a spec for how this behavior should work that we have. If something deviates from that, then it's a bug. And this seems to bring us closer, which is good.

but yes, as you note, given #673, this'll need some more refactoring 😞

FAQ.md Outdated
- 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".


// ResolvePath resolves the path of the given symlink. If the given path is not a symlink,
// then it returns the same path.
func ResolvePath(path 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.

How is this func different from just calling filepath.EvalSymlinks() directly?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

It's totally useless. 😅
I just noticed I already call IsSymlink() before ResolvePath().

I'll remove it when I've time to refactor the PR.

@ofpiyush
Copy link

@sdboyer Go tool chain currently works fine with that setup. And google cloud builder necessitates that structure, check #743

@sdboyer
Copy link
Member

sdboyer commented Jun 12, 2017

@ofpiyush

Go tool chain currently works fine with that setup.

For this particular case, the toolchain has less complex requirements than dep does. The toolchain is entirely package-oriented right now; we have to be project-oriented (trees of packages).

And google cloud builder necessitates that structure, check #743

Yep, I've seen #743. Without having dug too far into what cloud builder is doing, it does not appear to rely on arbitrary symlinking within package trees - only symlinked roots, which is what we're addressing here.

I understand that broader symlinking support can be helpful for certain cases, but we can't have it unless we can contain the complexity (not to mention potential attack vectors) it introduces in the system. Feel free to look through some of the past wreckage: sdboyer/gps#157 sdboyer/gps#177

@ofpiyush
Copy link

ofpiyush commented Jun 12, 2017

@sdboyer

only symlinked roots, which is what we're addressing here.

I think there is some miscommunication here. I am talking about this case in particular.

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.

Case:
GOPATH=/workspace/gopath
code_directory= /workspace
project_root = /workspace/gopath/github.com/private/repository -> /workspace

In this case, I get the error all dirs lacked any go code. Isn't this is the vice-versa case that is supposed to work?


For this particular case, the toolchain has less complex requirements than dep does. The toolchain is entirely package-oriented right now; we have to be project-oriented (trees of packages).

As an end user, I generally expect all tools to behave in a consistent way with each other, even if that way might not be something I agree with. It's pretty sad that I hit that one case where one tool has to diverge from the norm :-/

Feel free to look through some of the past wreckage: sdboyer/gps#157 sdboyer/gps#177

I will definitely check it out through this weekend. Though, I suspect I am not going to find the golden fix if better people have tried and given up. Worth a shot anyway. ☮️

Edit: ordering based on importance

@sdboyer
Copy link
Member

sdboyer commented Jun 12, 2017

In this case, I get the error all dirs lacked any go code. Isn't this is the vice-versa case that is supposed to work?

Yes, and it's something that either this PR, or a follow-up, should fix.

As an end user, I generally expect all tools to behave in a consistent way with each other, even if that way might not be something I agree with. It's pretty sad that I hit that one case where one tool has to diverge from the norm :-/

Throughout the process, we've sought to minimize the number of significant changes that dep introduces to the ecosystem. The committee came to the conclusion that the knock-on effects of not adopting a project-orientated view were much worse than doing so. Possible changes in symlink behavior are, unfortunately, one of the results.

@ofpiyush
Copy link

ofpiyush commented Jun 12, 2017

Yes, and it's something that either this PR, or a follow-up, should fix.

That's how I reached pkgtree. This line fails because it's expecting a directory and the project root is a symlink.

Would a special case fix for project roots work?

…oots-changes

Signed-off-by: Ibrahim AshShohail <ibra.sho@gmail.com>
Signed-off-by: Ibrahim AshShohail <ibra.sho@gmail.com>
Signed-off-by: Ibrahim AshShohail <ibra.sho@gmail.com>
Project.ResolvedAbsRoot() returns the resolved project.AbsRoot if it's a
symlink.

Signed-off-by: Ibrahim AshShohail <ibra.sho@gmail.com>
@ibrasho ibrasho force-pushed the symlink-project-roots-changes branch from e0f2a5f to 5bf9205 Compare June 13, 2017 16:55
@ibrasho ibrasho force-pushed the symlink-project-roots-changes branch 2 times, most recently from 96ede09 to aa6fd8e Compare June 15, 2017 02:23
Signed-off-by: Ibrahim AshShohail <ibra.sho@gmail.com>
@ibrasho ibrasho force-pushed the symlink-project-roots-changes branch from aa6fd8e to 0b8edb1 Compare June 15, 2017 03:13
@ibrasho
Copy link
Collaborator Author

ibrasho commented Jun 15, 2017

Tests should be passing now.

Copy link
Member

@sdboyer sdboyer left a comment

Choose a reason for hiding this comment

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

very close now

}

*/
// Ctx defines the supporting context of dep.
Copy link
Member

Choose a reason for hiding this comment

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

I'd like to see equivalent docs to what was previously there - examples, and such.

context.go Outdated
if err != nil {
return "", errors.Wrap(err, "resolveProjectRoot")
// DetectProjectGOPATH will return an error in the following cases:
// If p.AbsRoot is not a symlink and is not within any known GOPATH.
Copy link
Member

Choose a reason for hiding this comment

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

Put in a newline and further indent each of these by one space. That'll make godoc format them as a fixed block. As-written, this will all get collapsed without any line breaks.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Done.

project.go Outdated
p.ResolvedAbsRoot = root

// if p.AbsRoot is a symlink, set p.ResolvedAbsRoot to the resolved path.
if sym, err := fs.IsSymlink(p.AbsRoot); err != nil {
Copy link
Member

Choose a reason for hiding this comment

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

Probably don't need to check this; filepath.EvalSymlinks() will just return the input if it's not a symlink, which is the desired behavior anyway.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

filepath.EvalSymlinks() will recursively walk over the symlinks and errors might happen along the way (lstat or readlink could fail). 😞

Anyhow, I changed the flow a bit.

project.go Outdated
// SetRoot set the project AbsRoot. If the root is a symlink, it attempts to resolve it.
func (p *Project) SetRoot(root string) error {
p.AbsRoot = root
p.ResolvedAbsRoot = root
Copy link
Member

Choose a reason for hiding this comment

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

It's probably preferable that we only change the *Project's internal state if filepath.EvalSymlinks() exits without an error. This makes it generally consistent with the practice that Go functions should not return values from funcs when an error is also returned - just, in this case, those values are internal properties, rather than being return values.

Does that make sense?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

👍

Signed-off-by: Ibrahim AshShohail <ibra.sho@gmail.com>
@ibrasho ibrasho force-pushed the symlink-project-roots-changes branch 3 times, most recently from 0306540 to f40426e Compare June 16, 2017 19:08
Signed-off-by: Ibrahim AshShohail <ibra.sho@gmail.com>
@ibrasho ibrasho force-pushed the symlink-project-roots-changes branch 3 times, most recently from 53e69de to b98d713 Compare June 16, 2017 20:45
context.go Outdated
l, err := os.Lstat(path)
if err != nil {
return "", errors.Wrap(err, "resolveProjectRoot")
/*
Copy link
Member

Choose a reason for hiding this comment

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

nit: i know this works, but the standard is really just to use the // leader for godoc comments, even when they become quite large.

Copy link
Member

Choose a reason for hiding this comment

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

i'm just now realizing that some of the existing comments did this. that's my bad - i shouldn't have merged those in the first place. still, let's not repeat the pattern.

Copy link
Collaborator Author

@ibrasho ibrasho Jun 17, 2017

Choose a reason for hiding this comment

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

Done. 👍 I've fixed the existing occurrences.

project.go Outdated
// ImportRoot is the import path of the project's root directory.
ImportRoot gps.ProjectRoot
Manifest *Manifest
Lock *Lock // Optional
}

// SetRoot set the project AbsRoot. If the root is a symlink, it attempts to resolve it.
func (p *Project) SetRoot(root string) error {
sym, err := fs.IsSymlink(root)
Copy link
Member

Choose a reason for hiding this comment

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

What I was suggesting before would be this:

func (p *Project) SetRoot(root string) error {
	rroot, err := filepath.EvalSymlinks(root)
	if err != nil {
		return err
	}
	
	p.ResolvedAbsRoot, p.AbsRoot = rroot, root
	return nil
}

I think this would be equivalent. If root is not a symlink, then it's basically a no-op. If root is a symlink, then any error that would occur from fs.IsSymlink() are a subset of the errors from filepath.EvalSymlinks().

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Apparently, I misunderstood the original comment. Updated now. 😁

Signed-off-by: Ibrahim AshShohail <ibra.sho@gmail.com>
Signed-off-by: Ibrahim AshShohail <ibra.sho@gmail.com>
Signed-off-by: Ibrahim AshShohail <ibra.sho@gmail.com>
Signed-off-by: Ibrahim AshShohail <ibra.sho@gmail.com>
@ibrasho ibrasho force-pushed the symlink-project-roots-changes branch from 0d05390 to 4945522 Compare June 17, 2017 01:24
@sdboyer
Copy link
Member

sdboyer commented Jun 17, 2017

ok, i think we're looking good - waiting for tests now :D

Signed-off-by: Ibrahim AshShohail <ibra.sho@gmail.com>
@ibrasho ibrasho force-pushed the symlink-project-roots-changes branch from 4945522 to 13f512f Compare June 17, 2017 01:32
@ibrasho
Copy link
Collaborator Author

ibrasho commented Jun 17, 2017

Tests are passing now. 🎉
CodeClimate has actually finished running, but Github is still waiting for results for some reason. 😕

@sdboyer sdboyer merged commit a42708e into golang:master Jun 17, 2017
@sdboyer
Copy link
Member

sdboyer commented Jun 17, 2017

and in we finally go 😄

@ibrasho ibrasho deleted the symlink-project-roots-changes branch June 17, 2017 03:04
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

dep ensure fails when working directory includes a symlink
4 participants