-
Notifications
You must be signed in to change notification settings - Fork 249
[shellenv] fix PATH nesting for devbox-project and devbox-global #1508
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
Current dependencies on/for this PR:
This comment was auto-generated by Graphite. |
ab2109f
to
e4411e5
Compare
@mikeland73 have a look please. I think the main approach is in place. I needed to move to a struct to keep the logic encapsulated in both Not 100% confident about all the places I added |
e4411e5
to
af9beed
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Doing RC for flag name and changing the piping logic so we don't have to pass a boolean everwhere.
I think the flag should the exception behavior. So no flag means environment gets stacked on, yes flag means path stack is kept in tact.
I'll review new envpath package in parallel
In parallel
internal/boxcli/shellenv.go
Outdated
command.Flags().BoolVar( | ||
&flags.pure, "path-stack-in-place", true, | ||
"If true, Devbox will not give top priority to the PATH of this project's shellenv. "+ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I thought the name we agreed on was something like preserve-path-stack
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why is it defaulting to true? Should be false. I'm confused by the name maybe
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
oh yeah, I was struggling to recall the name we'd agreed on. I can change it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ugh, it should default to false.
The PR's state is a bit subpar b/c my fish shell has gotten into a bad state and I've been having problems running programs. So, sent this for prelim feedback.
devbox.go
Outdated
@@ -20,10 +20,10 @@ type Devbox interface { | |||
Config() *devconfig.Config | |||
EnvVars(ctx context.Context) ([]string, error) | |||
Info(ctx context.Context, pkg string, markdown bool) (string, error) | |||
Install(ctx context.Context) error | |||
Install(ctx context.Context, pathStackInPlace bool) error |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To really reduce the size and file touches of this PR, you can add this to devopt.Opts
instead. And also avoid adding boolean params.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I can do that but I feel kinda mixed about it.
We're not eliminating the boolean param, but sending it via a somewhat global state.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We're not eliminating the boolean param, but sending it via a somewhat global state.
Yeah, but I see the utility in not complicating the core funciton interfaces for this edge-case handling.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah agree devbox options are semi-global (not quite global as this PR can attest)
But it really makes everything else easier to read and the risk is minimal (specifically a single devbox object being used multiple times in a single command which is a pattern we don't use at all)
af9beed
to
fca3dab
Compare
internal/boxcli/shellenv.go
Outdated
"If true, Devbox will not give top priority to the PATH of this project's shellenv. "+ | ||
"This project's place in the path stack will be unchanged.") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Seggested description:
"Preserves existing PATH order if this project's environment is already in PATH. Useful if you want to avoid overshadowing another devbox project that is already active"
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
like it!
internal/impl/envpath/pathstack.go
Outdated
// Add this key only if absent from the stack | ||
!lo.Contains(s.keys, key) { | ||
|
||
s.keys = lo.Uniq(slices.Insert(s.keys, 0, key)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
totally get why you chose Insert
over append (so that we keep same order as needed by JoinPathLists
) but I always think of golang slices as natural stacks where append
can be used as a natural push
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yes! I totally get you.
I first implemented stack.keys
with later values having higher priority. Because append is a natural push, as you say.
But then changed to have the highest-priority be the zeroth index, because it aligns the path-stack's golang and string representations with the expanded form $PATH variable. This makes debugging much easier, when we dump out these values in fmt.Printf
or echo $DEVBOX_PATH_STACK
or echo $PATH
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@mikeland73 made updates as requested. Also, renamed envpath.go
to envpath/pathlists.go
and moved its unit test to this package.
I still need to write a unit test for envpath.stack
.
I'm also thinking it may make sense to wait on landing this until Remove Nixpkgs and the Script Exit on Error are released. To minimize risk.
internal/impl/envpath/pathstack.go
Outdated
// Add this key only if absent from the stack | ||
!lo.Contains(s.keys, key) { | ||
|
||
s.keys = lo.Uniq(slices.Insert(s.keys, 0, key)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yes! I totally get you.
I first implemented stack.keys
with later values having higher priority. Because append is a natural push, as you say.
But then changed to have the highest-priority be the zeroth index, because it aligns the path-stack's golang and string representations with the expanded form $PATH variable. This makes debugging much easier, when we dump out these values in fmt.Printf
or echo $DEVBOX_PATH_STACK
or echo $PATH
.
internal/impl/devbox.go
Outdated
env = pathStack.PushAndUpdateEnv(env, d.projectDirHash(), nixEnvPath, d.preservePathStack) | ||
debug.Log("New path stack is: %s", pathStack) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I thought I had written this comment previously, but github seems to have swallowed it. Here's a suggestion for an interface I find a bit easier to follow:
pathStack := envpath.Stack(env)
if !d.preservePathStack || !pathStack.Has(d.projectDirHash()) {
pathStack.PushAndDedup(d.projectDirHash(), nixEnvPath)
}
env["PATH"] = pathStack.Path()
Benefits:
- Remove preserve logic from stack
- All code is concentrated together.
- Split out push from computing the path
- Change
Has
to take the hash instead of key. key can now be private
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like this.
Another benefit:
- we move out handling env["PATH"]
- we can later have an
envpath.Path
struct that encapsulates further a lot of the complexity of this function, that then calls theenvpath.Stack
as shown here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There is a problem with this. We need to update the env-vars for the nixEnvPathKey regardless of the if-condition. And I think that should be inside the stack, since its internal logic.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So, a fix could be:
pathStack := envpath.Stack(env)
if !d.preservePathStack || !pathStack.Has(d.projectDirHash()) {
pathStack.PushAndDedup(d.projectDirHash(), nixEnvPath)
}
env[envpath.Key(d.projectDirHash())] = nixEnvPath // exposes internal logic of pathstack.
env["PATH"] = pathStack.Path()
but my concern would be that it breaks encapsulation and exposes internal logic.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good catch. Yeah, agree that it's not great to expose the internals.
In that case maybe just put the stack creation and use next to each other?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yup, did that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually we could still do:
pathStack := envpath.Stack(env)
pathStack.PushAndUpdateEnv(d.projectDirHash(), nixEnvPath, d.preservePathStack)
env := pathStack.Env()
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm going to land as-is, so can focus on other work.
Changing to .Env()
I have some concerns with:
- We'll be making a copy of
env
internally in the constructor. Then applying that over any changes between the constructor and doingpathStack.Env()
could potentially lead to issues, since it can lose information.
I prefer the existing design so that:
- Internal state is restricted to the path-stack keys. Other state is directly stored in
env
that is passed in. - The
PATH
is updated externally. That enables us adding a separate data structure to abstract the PATH handling in this function (future work).
Either way, don't feel strongly and we can iterate on it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I know you guys will probably disagree, but the Go-ish way would be to make the zero-value do what the constructor is doing:
type Stack struct {
keys []string
}
// Remove func Stack()
func (s *Stack) Push(env map[string]string, ...) {
if s.keys == nil {
// do what Stack() was doing
}
// continue with PushAndUpdateEnv
}
Usage:
var s envpath.Stack
s.Push(...)
internal/impl/envpath/pathstack.go
Outdated
env[InitPathEnv] = env["PATH"] | ||
} | ||
return &stack{ | ||
keys: strings.Split(stackEnv, ":"), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure if this matters, but keys
will be a slice with a single empty string if stackEnv
is an empty string.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
good point! I can check for stackEnv being an empty string
internal/impl/envvars.go
Outdated
return os.Getenv(d.ogPathKey()) != "" | ||
env := map[string]string{} | ||
for _, keyVal := range os.Environ() { | ||
parts := strings.Split(keyVal, "=") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use strings.Cut, otherwise you'll lose the ends of variables that have an =
in them. I think you could also export envir.mapToPairs
and reuse it here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
oh i like that. Done!
@gcurtis I don't think dislike this pattern at all! I think it's cool. But it doesn't quite work because the stack needs to be initialized with In that case the struct would contain an Env field. One downside to not initializing is that now you need to have the initialization code in every public method. |
@mikeland73 it seems like the
I don't think initializing the slice is too bad if the number of methods is small. It matches how types like Edit: @savil sorry for adding noise to your PR! Just wanted to be clear that I fully support merging this as-is. |
@gcurtis I think we're mixing pushing environment and pushing the path (we call the path What gets pushed on the stack is PATH fragments. But we also need to pass in the full environment because the stack itself is stored in env variables, as well as another env variable that stores the stack keys. I guess we can push the environment AND the path in each push. But I think initializing with env makes it nicer (see #1508 (comment)) |
de13cf7
to
ee49755
Compare
Summary
Problem:
High level problem description:
devbox global add [email protected]
adds bin-wrappers forsh
andbash
. Thissh
wrapper is used to execute the devbox script.sh
) needs to re-evaluate its own environmentCause
In our implementation of
shellenv
, we save an "OG Path" in an env-var. This is the PATH prior to enteringdevbox shell
. We then calculate theprint-dev-env
PATH. Finally, we construct the PATH for theshellenv
as<print-dev-env paths>:<og-paths>
.However, if another devbox-project has introduced additional path-elements in PATH, those will get dropped. Because when this shellenv is re-evaluated: it will do
PATH=<new nixEnvPath>:<og-path>
.Solution:
Develop a
PATH_STACK
that can track which path parts come from which devbox-projects.So, for a devbox-project nested within a shell that is using devbox-global, the PATH_STACK looks like
{<devbox-project-nix-env-path>, <devbox-global-nix-env-path>, <devbox-init-path>}
Now, if the global shellenv is re-evaluated, then the
<devbox-global-nix-env-path>
value is overidden, but the PATH_STACK structure is maintained.Finally, we construct PATH for
shellenv
asPATH=<devbox-project-nix-env-path>:<devbox-global-nix-env-path>:<devbox-init-path>
.How was it tested?
envpath.Stack
TODO: fill in.
devbox global add bash
doesn't lead to errors withdevbox run build
.Inside a
devbox add vim && devbox shell
, then doingdevbox rm vim
doesn't give the nix store path i.e. [Bug]: devbox shellenv shouldn't prepend to existing path #687 is still fixed.Adhoc test:
Previously, the last "cowsay works" would fail
https://gist.github.com/savil/2c6e5f86208ff95ae54e717c17c794d6