diff --git a/internal/boxcli/shellenv.go b/internal/boxcli/shellenv.go index 69ea343a1df..2e6fbbd5e47 100644 --- a/internal/boxcli/shellenv.go +++ b/internal/boxcli/shellenv.go @@ -15,10 +15,11 @@ import ( type shellEnvCmdFlags struct { envFlag - config configFlags - runInitHook bool - install bool - pure bool + config configFlags + runInitHook bool + install bool + pure bool + preservePathStack bool } func shellEnvCmd() *cobra.Command { @@ -48,6 +49,11 @@ func shellEnvCmd() *cobra.Command { command.Flags().BoolVar( &flags.pure, "pure", false, "If this flag is specified, devbox creates an isolated environment inheriting almost no variables from the current environment. A few variables, in particular HOME, USER and DISPLAY, are retained.") + command.Flags().BoolVar( + &flags.preservePathStack, "preserve-path-stack", false, + "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") + _ = command.Flags().MarkHidden("preserve-path-stack") flags.config.register(command) flags.envFlag.register(command) @@ -63,10 +69,11 @@ func shellEnvFunc(cmd *cobra.Command, flags shellEnvCmdFlags) (string, error) { return "", err } box, err := devbox.Open(&devopt.Opts{ - Dir: flags.config.path, - Stderr: cmd.ErrOrStderr(), - Pure: flags.pure, - Env: env, + Dir: flags.config.path, + Stderr: cmd.ErrOrStderr(), + PreservePathStack: flags.preservePathStack, + Pure: flags.pure, + Env: env, }) if err != nil { return "", err diff --git a/internal/envir/util.go b/internal/envir/util.go index e7d8d880d13..8ebefb87847 100644 --- a/internal/envir/util.go +++ b/internal/envir/util.go @@ -5,7 +5,9 @@ package envir import ( "os" + "slices" "strconv" + "strings" ) func IsDevboxCloud() bool { @@ -43,3 +45,31 @@ func GetValueOrDefault(key, def string) string { return val } + +// MapToPairs creates a slice of environment variable "key=value" pairs from a +// map. +func MapToPairs(m map[string]string) []string { + pairs := make([]string, len(m)) + i := 0 + for k, v := range m { + pairs[i] = k + "=" + v + i++ + } + slices.Sort(pairs) // for reproducibility + return pairs +} + +// PairsToMap creates a map from a slice of "key=value" environment variable +// pairs. Note that maps are not ordered, which can affect the final variable +// values when pairs contains duplicate keys. +func PairsToMap(pairs []string) map[string]string { + vars := make(map[string]string, len(pairs)) + for _, p := range pairs { + k, v, ok := strings.Cut(p, "=") + if !ok { + continue + } + vars[k] = v + } + return vars +} diff --git a/internal/impl/devbox.go b/internal/impl/devbox.go index b295bcdbbb3..6809aca6f40 100644 --- a/internal/impl/devbox.go +++ b/internal/impl/devbox.go @@ -9,6 +9,7 @@ import ( "fmt" "io" "io/fs" + "maps" "os" "os/exec" "path/filepath" @@ -21,6 +22,7 @@ import ( "github.com/pkg/errors" "github.com/samber/lo" "go.jetpack.io/devbox/internal/devpkg" + "go.jetpack.io/devbox/internal/impl/envpath" "go.jetpack.io/devbox/internal/impl/generate" "go.jetpack.io/devbox/internal/searcher" "go.jetpack.io/devbox/internal/shellgen" @@ -59,6 +61,7 @@ type Devbox struct { nix nix.Nixer projectDir string pluginManager *plugin.Manager + preservePathStack bool pure bool allowInsecureAdds bool customProcessComposeFile string @@ -89,6 +92,7 @@ func Open(opts *devopt.Opts) (*Devbox, error) { projectDir: projectDir, pluginManager: plugin.NewManager(), stderr: opts.Stderr, + preservePathStack: opts.PreservePathStack, pure: opts.Pure, customProcessComposeFile: opts.CustomProcessComposeFile, allowInsecureAdds: opts.AllowInsecureAdds, @@ -317,7 +321,7 @@ func (d *Devbox) EnvVars(ctx context.Context) ([]string, error) { if err != nil { return nil, err } - return mapToPairs(envs), nil + return envir.MapToPairs(envs), nil } func (d *Devbox) ShellEnvHash(ctx context.Context) (string, error) { @@ -782,18 +786,10 @@ func (d *Devbox) computeNixEnv(ctx context.Context, usePrintDevEnvCache bool) (m } } - currentEnvPath := env["PATH"] - debug.Log("current environment PATH is: %s", currentEnvPath) - // Use the original path, if available. If not available, set it for future calls. - // See https://github.com/jetpack-io/devbox/issues/687 - // We add the project dir hash to ensure that we don't have conflicts - // between different projects (including global) - // (moving a project would change the hash and that's fine) - originalPath, ok := env[d.ogPathKey()] - if !ok { - env[d.ogPathKey()] = currentEnvPath - originalPath = currentEnvPath - } + debug.Log("current environment PATH is: %s", env["PATH"]) + + originalEnv := make(map[string]string, len(env)) + maps.Copy(originalEnv, env) vaf, err := d.nix.PrintDevEnv(ctx, &nix.PrintDevEnvArgs{ FlakesFilePath: d.nixFlakesFilePath(), @@ -853,13 +849,13 @@ func (d *Devbox) computeNixEnv(ctx context.Context, usePrintDevEnvCache bool) (m addEnvIfNotPreviouslySetByDevbox(env, pluginEnv) - env["PATH"] = JoinPathLists( + env["PATH"] = envpath.JoinPathLists( nix.ProfileBinPath(d.projectDir), env["PATH"], ) if !d.OmitBinWrappersFromPath { - env["PATH"] = JoinPathLists( + env["PATH"] = envpath.JoinPathLists( filepath.Join(d.projectDir, plugin.WrapperBinPath), env["PATH"], ) @@ -899,14 +895,18 @@ func (d *Devbox) computeNixEnv(ctx context.Context, usePrintDevEnvCache bool) (m }) debug.Log("PATH after filtering with buildInputs (%v) is: %s", buildInputs, nixEnvPath) - env["PATH"] = JoinPathLists(nixEnvPath, originalPath) + pathStack := envpath.Stack(env, originalEnv) + pathStack.Push(env, d.projectDirHash(), nixEnvPath, d.preservePathStack) + env["PATH"] = pathStack.Path(env) + debug.Log("New path stack is: %s", pathStack) + debug.Log("computed environment PATH is: %s", env["PATH"]) d.setCommonHelperEnvVars(env) if !d.pure { // preserve the original XDG_DATA_DIRS by prepending to it - env["XDG_DATA_DIRS"] = JoinPathLists(env["XDG_DATA_DIRS"], os.Getenv("XDG_DATA_DIRS")) + env["XDG_DATA_DIRS"] = envpath.JoinPathLists(env["XDG_DATA_DIRS"], os.Getenv("XDG_DATA_DIRS")) } for k, v := range d.env { @@ -941,10 +941,6 @@ func (d *Devbox) nixEnv(ctx context.Context) (map[string]string, error) { return d.computeNixEnv(ctx, usePrintDevEnvCache) } -func (d *Devbox) ogPathKey() string { - return "DEVBOX_OG_PATH_" + d.projectDirHash() -} - func (d *Devbox) nixPrintDevEnvCachePath() string { return filepath.Join(d.projectDir, ".devbox/.nix-print-dev-env-cache") } @@ -1120,8 +1116,8 @@ var ignoreDevEnvVar = map[string]bool{ // common setups (e.g. gradio, rust) func (d *Devbox) setCommonHelperEnvVars(env map[string]string) { profileLibDir := filepath.Join(d.projectDir, nix.ProfilePath, "lib") - env["LD_LIBRARY_PATH"] = JoinPathLists(profileLibDir, env["LD_LIBRARY_PATH"]) - env["LIBRARY_PATH"] = JoinPathLists(profileLibDir, env["LIBRARY_PATH"]) + env["LD_LIBRARY_PATH"] = envpath.JoinPathLists(profileLibDir, env["LD_LIBRARY_PATH"]) + env["LIBRARY_PATH"] = envpath.JoinPathLists(profileLibDir, env["LIBRARY_PATH"]) } // NixBins returns the paths to all the nix binaries that are installed by @@ -1197,7 +1193,7 @@ func (d *Devbox) parseEnvAndExcludeSpecialCases(currentEnv []string) (map[string return nil, err } includedInPath = append(includedInPath, dotdevboxBinPath(d)) - env["PATH"] = JoinPathLists(includedInPath...) + env["PATH"] = envpath.JoinPathLists(includedInPath...) } return env, nil } diff --git a/internal/impl/devbox_test.go b/internal/impl/devbox_test.go index c0945d71637..1467f1ab18a 100644 --- a/internal/impl/devbox_test.go +++ b/internal/impl/devbox_test.go @@ -14,6 +14,7 @@ import ( "github.com/bmatcuk/doublestar/v4" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "go.jetpack.io/devbox/internal/impl/envpath" "go.jetpack.io/devbox/internal/devconfig" "go.jetpack.io/devbox/internal/envir" @@ -85,10 +86,9 @@ func TestComputeNixPathIsIdempotent(t *testing.T) { assert.NotEmpty(t, path, "path should not be nil") t.Setenv("PATH", path) - t.Setenv( - "DEVBOX_OG_PATH_"+devbox.projectDirHash(), - env["DEVBOX_OG_PATH_"+devbox.projectDirHash()], - ) + t.Setenv(envpath.InitPathEnv, env[envpath.InitPathEnv]) + t.Setenv(envpath.PathStackEnv, env[envpath.PathStackEnv]) + t.Setenv(envpath.Key(devbox.projectDirHash()), env[envpath.Key(devbox.projectDirHash())]) env, err = devbox.computeNixEnv(ctx, false /*use cache*/) require.NoError(t, err, "computeNixEnv should not fail") @@ -108,10 +108,9 @@ func TestComputeNixPathWhenRemoving(t *testing.T) { assert.Contains(t, path, "/tmp/my/path", "path should contain /tmp/my/path") t.Setenv("PATH", path) - t.Setenv( - "DEVBOX_OG_PATH_"+devbox.projectDirHash(), - env["DEVBOX_OG_PATH_"+devbox.projectDirHash()], - ) + t.Setenv(envpath.InitPathEnv, env[envpath.InitPathEnv]) + t.Setenv(envpath.PathStackEnv, env[envpath.PathStackEnv]) + t.Setenv(envpath.Key(devbox.projectDirHash()), env[envpath.Key(devbox.projectDirHash())]) devbox.nix.(*testNix).path = "" env, err = devbox.computeNixEnv(ctx, false /*use cache*/) diff --git a/internal/impl/devopt/devboxopts.go b/internal/impl/devopt/devboxopts.go index 041b86fb1db..438f2af9e89 100644 --- a/internal/impl/devopt/devboxopts.go +++ b/internal/impl/devopt/devboxopts.go @@ -8,6 +8,7 @@ type Opts struct { AllowInsecureAdds bool Dir string Env map[string]string + PreservePathStack bool Pure bool IgnoreWarnings bool CustomProcessComposeFile string diff --git a/internal/impl/envpath/pathlists.go b/internal/impl/envpath/pathlists.go new file mode 100644 index 00000000000..a432a73807e --- /dev/null +++ b/internal/impl/envpath/pathlists.go @@ -0,0 +1,37 @@ +package envpath + +import ( + "path/filepath" + "strings" +) + +// JoinPathLists joins and cleans PATH-style strings of +// [os.ListSeparator] delimited paths. To clean a path list, it splits it and +// does the following for each element: +// +// 1. Applies [filepath.Clean]. +// 2. Removes the path if it's relative (must begin with '/' and not be '.'). +// 3. Removes the path if it's a duplicate. +func JoinPathLists(pathLists ...string) string { + if len(pathLists) == 0 { + return "" + } + + seen := make(map[string]bool) + var cleaned []string + for _, path := range pathLists { + for _, path := range filepath.SplitList(path) { + path = filepath.Clean(path) + if path == "." || path[0] != '/' { + // Remove empty paths and don't allow relative + // paths for security reasons. + continue + } + if !seen[path] { + cleaned = append(cleaned, path) + } + seen[path] = true + } + } + return strings.Join(cleaned, string(filepath.ListSeparator)) +} diff --git a/internal/impl/envpath/pathlists_test.go b/internal/impl/envpath/pathlists_test.go new file mode 100644 index 00000000000..e3b36a4dad9 --- /dev/null +++ b/internal/impl/envpath/pathlists_test.go @@ -0,0 +1,32 @@ +package envpath + +import ( + "testing" +) + +func TestCleanEnvPath(t *testing.T) { + tests := []struct { + name string + inPath string + outPath string + }{ + { + name: "NoEmptyPaths", + inPath: "/usr/local/bin::", + outPath: "/usr/local/bin", + }, + { + name: "NoRelativePaths", + inPath: "/usr/local/bin:/usr/bin:../test:/bin:/usr/sbin:/sbin:.:..", + outPath: "/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin", + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got := JoinPathLists(test.inPath) + if got != test.outPath { + t.Errorf("Got incorrect cleaned PATH.\ngot: %s\nwant: %s", got, test.outPath) + } + }) + } +} diff --git a/internal/impl/envpath/stack.go b/internal/impl/envpath/stack.go new file mode 100644 index 00000000000..88824e0e7cd --- /dev/null +++ b/internal/impl/envpath/stack.go @@ -0,0 +1,97 @@ +package envpath + +import ( + "strings" + + "github.com/samber/lo" + "golang.org/x/exp/slices" +) + +const ( + // PathStackEnv stores the string representation of the stack, as a ":" separated list. + // Each element in the list is also the key to the env-var that stores the + // nixEnvPath for that devbox-project. Except for the last element, which is InitPathEnv. + PathStackEnv = "DEVBOX_PATH_STACK" + + // InitPathEnv stores the path prior to any devbox shellenv modifying the environment + InitPathEnv = "DEVBOX_INIT_PATH" +) + +// stack has the following design: +// 1. The stack enables tracking which sub-paths in PATH come from which devbox-project +// 2. It is an ordered-list of keys to env-vars that store nixEnvPath values of devbox-projects. +// 3. The final PATH is reconstructed by concatenating the env-var values of each nixEnvPathKey. +// 5. The stack is stored in its own env-var PathStackEnv, shared by all devbox-projects in this shell. +type stack struct { + // keys holds the stack elements. + // Earlier (lower index number) keys get higher priority. + // This keeps the string representation of the stack aligned with the PATH value. + keys []string +} + +// Stack initializes the path stack in the `env` environment. +// It relies on old state stored in the `originalEnv` environment. +func Stack(env, originalEnv map[string]string) *stack { + stackEnv, ok := originalEnv[PathStackEnv] + if !ok || strings.TrimSpace(stackEnv) == "" { + // if path stack is empty, then push the current PATH, which is the + // external environment prior to any devbox-shellenv being applied to it. + stackEnv = InitPathEnv + env[InitPathEnv] = originalEnv["PATH"] + } + return &stack{ + keys: strings.Split(stackEnv, ":"), + } +} + +// String is the value of the stack stored in its env-var. +func (s *stack) String() string { + return strings.Join(s.keys, ":") +} + +func (s *stack) Path(env map[string]string) string { + // Look up the paths-list for each stack element, and join them together to get the final PATH. + pathLists := lo.Map(s.keys, func(part string, idx int) string { return env[part] }) + return JoinPathLists(pathLists...) +} + +// Key is the element stored in the stack for a devbox-project. It represents +// a pointer to the nixEnvPath value stored in its own env-var, also using this same +// Key. +func Key(projectHash string) string { + return "DEVBOX_NIX_ENV_PATH_" + projectHash +} + +// Push adds the new nixEnvPath for the devbox-project identified by projectHash. +// The nixEnvPath is pushed to the top of the stack (given highest priority), +// unless preservePathStack is enabled. +// +// It also updates the env by modifying the PathStack env-var, and the env-var +// for storing the nixEnvPath. +func (s *stack) Push( + env map[string]string, + projectHash string, + nixEnvPath string, + preservePathStack bool, +) { + key := Key(projectHash) + + // Add this nixEnvPath to env + env[key] = nixEnvPath + + // Common case: ensure this key is at the top of the stack + if !preservePathStack || + // Case preservePathStack == true, usually from bin-wrapper or (in future) shell hook. + // Add this key only if absent from the stack + !lo.Contains(s.keys, key) { + + s.keys = lo.Uniq(slices.Insert(s.keys, 0, key)) + } + env[PathStackEnv] = s.String() +} + +// Has tests if the stack has the specified key. Refer to the Key function for constructing +// the appropriate key for any devbox-project. +func (s *stack) Has(projectHash string) bool { + return lo.Contains(s.keys, Key(projectHash)) +} diff --git a/internal/impl/envpath/stack_test.go b/internal/impl/envpath/stack_test.go new file mode 100644 index 00000000000..74846dc1463 --- /dev/null +++ b/internal/impl/envpath/stack_test.go @@ -0,0 +1,103 @@ +package envpath + +import ( + "fmt" + "strings" + "testing" +) + +func TestNewStack(t *testing.T) { + // Initialize a new Stack from the existing env + originalEnv := map[string]string{ + "PATH": "/init-path", + } + env := make(map[string]string) + stack := Stack(env, originalEnv) + if len(stack.keys) == 0 { + t.Errorf("Stack has no keys but should have %s", InitPathEnv) + } + if len(stack.keys) != 1 { + t.Errorf("Stack has should have exactly one key (%s) but has %d keys. Keys are: %s", + InitPathEnv, len(stack.keys), strings.Join(stack.keys, ", ")) + } + + // Each testStep below is applied in order, and the resulting env + // is used implicitly as input into the subsequent test step. + // + // These test steps are NOT independent! These are not "test cases" that + // would usually be independent. + testSteps := []struct { + projectHash string + nixEnvPath string + preservePathStack bool + expectedKeysLength int + expectedEnv map[string]string + }{ + { + projectHash: "fooProjectHash", + nixEnvPath: "/foo1:/foo2", + preservePathStack: false, + expectedKeysLength: 2, + expectedEnv: map[string]string{ + "PATH": "/foo1:/foo2:/init-path", + InitPathEnv: "/init-path", + Key("fooProjectHash"): "/foo1:/foo2", + }, + }, + { + projectHash: "barProjectHash", + nixEnvPath: "/bar1:/bar2", + preservePathStack: false, + expectedKeysLength: 3, + expectedEnv: map[string]string{ + "PATH": "/bar1:/bar2:/foo1:/foo2:/init-path", + InitPathEnv: "/init-path", + Key("fooProjectHash"): "/foo1:/foo2", + Key("barProjectHash"): "/bar1:/bar2", + }, + }, + { + projectHash: "fooProjectHash", + nixEnvPath: "/foo3:/foo2", + preservePathStack: false, + expectedKeysLength: 3, + expectedEnv: map[string]string{ + "PATH": "/foo3:/foo2:/bar1:/bar2:/init-path", + InitPathEnv: "/init-path", + Key("fooProjectHash"): "/foo3:/foo2", + Key("barProjectHash"): "/bar1:/bar2", + }, + }, + { + projectHash: "barProjectHash", + nixEnvPath: "/bar3:/bar2", + preservePathStack: true, + expectedKeysLength: 3, + expectedEnv: map[string]string{ + "PATH": "/foo3:/foo2:/bar3:/bar2:/init-path", + InitPathEnv: "/init-path", + Key("fooProjectHash"): "/foo3:/foo2", + Key("barProjectHash"): "/bar3:/bar2", + }, + }, + } + + for idx, testStep := range testSteps { + t.Run( + fmt.Sprintf("step_%d", idx), func(t *testing.T) { + // Push to stack and update PATH env + stack.Push(env, testStep.projectHash, testStep.nixEnvPath, testStep.preservePathStack) + env["PATH"] = stack.Path(env) + + if len(stack.keys) != testStep.expectedKeysLength { + t.Errorf("Stack should have exactly %d keys but has %d keys. Keys are: %s", + testStep.expectedKeysLength, len(stack.keys), strings.Join(stack.keys, ", ")) + } + for k, v := range testStep.expectedEnv { + if env[k] != v { + t.Errorf("env[%s] should be %s but is %s", k, v, env[k]) + } + } + }) + } +} diff --git a/internal/impl/envvars.go b/internal/impl/envvars.go index 1f0701dcff5..488e9ad0508 100644 --- a/internal/impl/envvars.go +++ b/internal/impl/envvars.go @@ -7,38 +7,13 @@ import ( "os" "slices" "strings" + + "go.jetpack.io/devbox/internal/envir" + "go.jetpack.io/devbox/internal/impl/envpath" ) const devboxSetPrefix = "__DEVBOX_SET_" -// mapToPairs creates a slice of environment variable "key=value" pairs from a -// map. -func mapToPairs(m map[string]string) []string { - pairs := make([]string, len(m)) - i := 0 - for k, v := range m { - pairs[i] = k + "=" + v - i++ - } - slices.Sort(pairs) // for reproducibility - return pairs -} - -// pairsToMap creates a map from a slice of "key=value" environment variable -// pairs. Note that maps are not ordered, which can affect the final variable -// values when pairs contains duplicate keys. -func pairsToMap(pairs []string) map[string]string { - vars := make(map[string]string, len(pairs)) - for _, p := range pairs { - k, v, ok := strings.Cut(p, "=") - if !ok { - continue - } - vars[k] = v - } - return vars -} - // exportify formats vars as a line-separated string of shell export statements. // Each line is of the form `export key="value";` with any special characters in // value escaped. This means that the shell will always interpret values as @@ -98,5 +73,8 @@ func markEnvsAsSetByDevbox(envs ...map[string]string) { // as a proxy for this. This allows us to differentiate between global and // individual project shells. func (d *Devbox) IsEnvEnabled() bool { - return os.Getenv(d.ogPathKey()) != "" + fakeEnv := map[string]string{} + // the Stack is initialized in the fakeEnv, from the state in the real os.Environ + pathStack := envpath.Stack(fakeEnv, envir.PairsToMap(os.Environ())) + return pathStack.Has(d.projectDirHash()) } diff --git a/internal/impl/shell.go b/internal/impl/shell.go index a8c398c77e0..2f69476066e 100644 --- a/internal/impl/shell.go +++ b/internal/impl/shell.go @@ -247,7 +247,7 @@ func (s *DevboxShell) Run() error { env["SHELL"] = s.binPath cmd = exec.Command(s.binPath) - cmd.Env = mapToPairs(env) + cmd.Env = envir.MapToPairs(env) cmd.Args = append(cmd.Args, extraArgs...) cmd.Stdin = os.Stdin cmd.Stdout = os.Stdout @@ -387,37 +387,6 @@ func (s *DevboxShell) linkShellStartupFiles(shellSettingsDir string) { } } -// JoinPathLists joins and cleans PATH-style strings of -// [os.ListSeparator] delimited paths. To clean a path list, it splits it and -// does the following for each element: -// -// 1. Applies [filepath.Clean]. -// 2. Removes the path if it's relative (must begin with '/' and not be '.'). -// 3. Removes the path if it's a duplicate. -func JoinPathLists(pathLists ...string) string { - if len(pathLists) == 0 { - return "" - } - - seen := make(map[string]bool) - var cleaned []string - for _, path := range pathLists { - for _, path := range filepath.SplitList(path) { - path = filepath.Clean(path) - if path == "." || path[0] != '/' { - // Remove empty paths and don't allow relative - // paths for security reasons. - continue - } - if !seen[path] { - cleaned = append(cleaned, path) - } - seen[path] = true - } - } - return strings.Join(cleaned, string(filepath.ListSeparator)) -} - func filterPathList(pathList string, keep func(string) bool) string { filtered := []string{} for _, path := range filepath.SplitList(pathList) { diff --git a/internal/impl/shell_test.go b/internal/impl/shell_test.go index aeeae510a77..98255ef2b79 100644 --- a/internal/impl/shell_test.go +++ b/internal/impl/shell_test.go @@ -13,6 +13,7 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "go.jetpack.io/devbox/internal/envir" "go.jetpack.io/devbox/internal/shellgen" ) @@ -45,7 +46,7 @@ func testWriteDevboxShellrc(t *testing.T, testdirs []string) { test := &tests[i] test.name = filepath.Base(path) if b, err := os.ReadFile(filepath.Join(path, "env")); err == nil { - test.env = pairsToMap(strings.Split(string(b), "\n")) + test.env = envir.PairsToMap(strings.Split(string(b), "\n")) } test.hooksFilePath = shellgen.ScriptPath(projectDir, shellgen.HooksFilename) @@ -105,30 +106,3 @@ If the new shellrc is correct, you can update the golden file with: }) } } - -func TestCleanEnvPath(t *testing.T) { - tests := []struct { - name string - inPath string - outPath string - }{ - { - name: "NoEmptyPaths", - inPath: "/usr/local/bin::", - outPath: "/usr/local/bin", - }, - { - name: "NoRelativePaths", - inPath: "/usr/local/bin:/usr/bin:../test:/bin:/usr/sbin:/sbin:.:..", - outPath: "/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin", - }, - } - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - got := JoinPathLists(test.inPath) - if got != test.outPath { - t.Errorf("Got incorrect cleaned PATH.\ngot: %s\nwant: %s", got, test.outPath) - } - }) - } -} diff --git a/internal/wrapnix/wrapper.sh.tmpl b/internal/wrapnix/wrapper.sh.tmpl index 4d2b325a0e8..b4bb9198915 100644 --- a/internal/wrapnix/wrapper.sh.tmpl +++ b/internal/wrapnix/wrapper.sh.tmpl @@ -21,7 +21,7 @@ DO_NOT_TRACK=1 can be removed once we optimize segment to queue events. if [[ "${{ .ShellEnvHashKey }}" != "{{ .ShellEnvHash }}" ]] && [[ -z "${{ .ShellEnvHashKey }}_GUARD" ]]; then export {{ .ShellEnvHashKey }}_GUARD=true -eval "$(DO_NOT_TRACK=1 devbox shellenv -c {{ .ProjectDir }})" +eval "$(DO_NOT_TRACK=1 devbox shellenv --preserve-path-stack -c {{ .ProjectDir }})" fi {{/*