Skip to content

Commit 027bfb8

Browse files
committed
move to envpath.Stack struct
1 parent 78157f2 commit 027bfb8

File tree

7 files changed

+152
-80
lines changed

7 files changed

+152
-80
lines changed

internal/impl/devbox.go

Lines changed: 11 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020
"github.com/pkg/errors"
2121
"github.com/samber/lo"
2222
"go.jetpack.io/devbox/internal/devpkg"
23+
"go.jetpack.io/devbox/internal/impl/envpath"
2324
"go.jetpack.io/devbox/internal/impl/generate"
2425
"go.jetpack.io/devbox/internal/searcher"
2526
"go.jetpack.io/devbox/internal/shellgen"
@@ -784,16 +785,10 @@ func (d *Devbox) computeNixEnv(ctx context.Context, usePrintDevEnvCache bool) (m
784785
}
785786
}
786787

787-
currentEnvPath := env["PATH"]
788-
debug.Log("current environment PATH is: %s", currentEnvPath)
788+
debug.Log("current environment PATH is: %s", env["PATH"])
789789

790-
pathStackEnv, ok := env["PATH_STACK"]
791-
if !ok {
792-
// if path stack is empty, then set the first element
793-
pathStackEnv = d.ogPathKey()
794-
env[d.ogPathKey()] = currentEnvPath
795-
}
796-
debug.Log("path stack is: %s", pathStackEnv)
790+
pathStack := envpath.NewStack(env)
791+
debug.Log("path stack is: %s", pathStack)
797792

798793
vaf, err := d.nix.PrintDevEnv(ctx, &nix.PrintDevEnvArgs{
799794
FlakesFilePath: d.nixFlakesFilePath(),
@@ -853,13 +848,13 @@ func (d *Devbox) computeNixEnv(ctx context.Context, usePrintDevEnvCache bool) (m
853848

854849
addEnvIfNotPreviouslySetByDevbox(env, pluginEnv)
855850

856-
env["PATH"] = JoinPathLists(
851+
env["PATH"] = envpath.JoinPathLists(
857852
nix.ProfileBinPath(d.projectDir),
858853
env["PATH"],
859854
)
860855

861856
if !d.OmitBinWrappersFromPath {
862-
env["PATH"] = JoinPathLists(
857+
env["PATH"] = envpath.JoinPathLists(
863858
filepath.Join(d.projectDir, plugin.WrapperBinPath),
864859
env["PATH"],
865860
)
@@ -899,32 +894,15 @@ func (d *Devbox) computeNixEnv(ctx context.Context, usePrintDevEnvCache bool) (m
899894
})
900895
debug.Log("PATH after filtering with buildInputs (%v) is: %s", buildInputs, nixEnvPath)
901896

902-
// The PathStack enables:
903-
// 1. Tracking which sub-paths in PATH come from which devbox-project
904-
// 2. The priority order of these sub-paths: devbox-projects encountered later get higher priority.
905-
pathStack := strings.Split(pathStackEnv, ":")
906-
nixEnvPathKey := "DEVBOX_NIX_ENV_PATH_" + d.projectDirHash()
907-
if !lo.Contains(pathStack, nixEnvPathKey) {
908-
// if pathStack does not contain this project's nixEnvPath already,
909-
// then we add it
910-
pathStack = append(pathStack, nixEnvPathKey)
911-
}
912-
// Store the nixEnvPath for this devbox-project. This lets us replace this sub-path in the eventual PATH,
913-
// without affecting the sub-paths from other devbox-projects.
914-
env[nixEnvPathKey] = nixEnvPath
915-
env["PATH_STACK"] = strings.Join(pathStack, ":")
916-
917-
// Look up the path-list for each path-stack element, and join them together to get the final PATH.
918-
pathLists := lo.Map(pathStack, func(part string, idx int) string { return env[part] })
919-
env["PATH"] = JoinPathLists(lo.Reverse(pathLists)...) // reverse to give priority to the most recent project
897+
env = pathStack.AddToEnv(env, d.projectDirHash(), nixEnvPath)
920898

921899
debug.Log("computed environment PATH is: %s", env["PATH"])
922900

923901
d.setCommonHelperEnvVars(env)
924902

925903
if !d.pure {
926904
// preserve the original XDG_DATA_DIRS by prepending to it
927-
env["XDG_DATA_DIRS"] = JoinPathLists(env["XDG_DATA_DIRS"], os.Getenv("XDG_DATA_DIRS"))
905+
env["XDG_DATA_DIRS"] = envpath.JoinPathLists(env["XDG_DATA_DIRS"], os.Getenv("XDG_DATA_DIRS"))
928906
}
929907

930908
for k, v := range d.env {
@@ -959,10 +937,6 @@ func (d *Devbox) nixEnv(ctx context.Context) (map[string]string, error) {
959937
return d.computeNixEnv(ctx, usePrintDevEnvCache)
960938
}
961939

962-
func (d *Devbox) ogPathKey() string {
963-
return "DEVBOX_OG_PATH_" + d.projectDirHash()
964-
}
965-
966940
func (d *Devbox) nixPrintDevEnvCachePath() string {
967941
return filepath.Join(d.projectDir, ".devbox/.nix-print-dev-env-cache")
968942
}
@@ -1137,8 +1111,8 @@ var ignoreDevEnvVar = map[string]bool{
11371111
// common setups (e.g. gradio, rust)
11381112
func (d *Devbox) setCommonHelperEnvVars(env map[string]string) {
11391113
profileLibDir := filepath.Join(d.projectDir, nix.ProfilePath, "lib")
1140-
env["LD_LIBRARY_PATH"] = JoinPathLists(profileLibDir, env["LD_LIBRARY_PATH"])
1141-
env["LIBRARY_PATH"] = JoinPathLists(profileLibDir, env["LIBRARY_PATH"])
1114+
env["LD_LIBRARY_PATH"] = envpath.JoinPathLists(profileLibDir, env["LD_LIBRARY_PATH"])
1115+
env["LIBRARY_PATH"] = envpath.JoinPathLists(profileLibDir, env["LIBRARY_PATH"])
11421116
}
11431117

11441118
// NixBins returns the paths to all the nix binaries that are installed by
@@ -1214,7 +1188,7 @@ func (d *Devbox) parseEnvAndExcludeSpecialCases(currentEnv []string) (map[string
12141188
return nil, err
12151189
}
12161190
includedInPath = append(includedInPath, dotdevboxBinPath(d))
1217-
env["PATH"] = JoinPathLists(includedInPath...)
1191+
env["PATH"] = envpath.JoinPathLists(includedInPath...)
12181192
}
12191193
return env, nil
12201194
}

internal/impl/devbox_test.go

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414
"github.com/bmatcuk/doublestar/v4"
1515
"github.com/stretchr/testify/assert"
1616
"github.com/stretchr/testify/require"
17+
"go.jetpack.io/devbox/internal/impl/envpath"
1718

1819
"go.jetpack.io/devbox/internal/devconfig"
1920
"go.jetpack.io/devbox/internal/envir"
@@ -85,11 +86,9 @@ func TestComputeNixPathIsIdempotent(t *testing.T) {
8586
assert.NotEmpty(t, path, "path should not be nil")
8687

8788
t.Setenv("PATH", path)
88-
t.Setenv(
89-
"DEVBOX_OG_PATH_"+devbox.projectDirHash(),
90-
env["DEVBOX_OG_PATH_"+devbox.projectDirHash()],
91-
)
92-
t.Setenv("PATH_STACK", env["PATH_STACK"])
89+
t.Setenv(envpath.InitPathEnv, env[envpath.InitPathEnv])
90+
t.Setenv(envpath.PathStackEnv, env[envpath.PathStackEnv])
91+
t.Setenv(envpath.Key(devbox.projectDirHash()), env[envpath.Key(devbox.projectDirHash())])
9392

9493
env, err = devbox.computeNixEnv(ctx, false /*use cache*/)
9594
require.NoError(t, err, "computeNixEnv should not fail")
@@ -109,11 +108,9 @@ func TestComputeNixPathWhenRemoving(t *testing.T) {
109108
assert.Contains(t, path, "/tmp/my/path", "path should contain /tmp/my/path")
110109

111110
t.Setenv("PATH", path)
112-
t.Setenv(
113-
"DEVBOX_OG_PATH_"+devbox.projectDirHash(),
114-
env["DEVBOX_OG_PATH_"+devbox.projectDirHash()],
115-
)
116-
t.Setenv("PATH_STACK", env["PATH_STACK"])
111+
t.Setenv(envpath.InitPathEnv, env[envpath.InitPathEnv])
112+
t.Setenv(envpath.PathStackEnv, env[envpath.PathStackEnv])
113+
t.Setenv(envpath.Key(devbox.projectDirHash()), env[envpath.Key(devbox.projectDirHash())])
117114

118115
devbox.nix.(*testNix).path = ""
119116
env, err = devbox.computeNixEnv(ctx, false /*use cache*/)

internal/impl/envpath/envpath.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package envpath
2+
3+
import (
4+
"path/filepath"
5+
"strings"
6+
)
7+
8+
// JoinPathLists joins and cleans PATH-style strings of
9+
// [os.ListSeparator] delimited paths. To clean a path list, it splits it and
10+
// does the following for each element:
11+
//
12+
// 1. Applies [filepath.Clean].
13+
// 2. Removes the path if it's relative (must begin with '/' and not be '.').
14+
// 3. Removes the path if it's a duplicate.
15+
func JoinPathLists(pathLists ...string) string {
16+
if len(pathLists) == 0 {
17+
return ""
18+
}
19+
20+
seen := make(map[string]bool)
21+
var cleaned []string
22+
for _, path := range pathLists {
23+
for _, path := range filepath.SplitList(path) {
24+
path = filepath.Clean(path)
25+
if path == "." || path[0] != '/' {
26+
// Remove empty paths and don't allow relative
27+
// paths for security reasons.
28+
continue
29+
}
30+
if !seen[path] {
31+
cleaned = append(cleaned, path)
32+
}
33+
seen[path] = true
34+
}
35+
}
36+
return strings.Join(cleaned, string(filepath.ListSeparator))
37+
}

internal/impl/envpath/pathstack.go

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
package envpath
2+
3+
import (
4+
"strings"
5+
6+
"github.com/samber/lo"
7+
"golang.org/x/exp/slices"
8+
)
9+
10+
const (
11+
PathStackEnv = "DEVBOX_PATH_STACK"
12+
13+
// InitPathEnv stores the path prior to any devbox shellenv modifying the environment
14+
InitPathEnv = "DEVBOX_INIT_PATH"
15+
)
16+
17+
// Stack has the following design:
18+
// 1. The PathStack enables tracking which sub-paths in PATH come from which devbox-project
19+
// 2. What it stores: The PathStack is an ordered-list of nixEnvPathKeys
20+
// 3. Each nixEnvPathKey is set as an env-var with the value of the nixEnvPath for that devbox-project.
21+
// 4. The final PATH is reconstructed by concatenating the env-var values of each nixEnvPathKey env-var.
22+
// 5. The Stack is stored in its own env-var shared by all devbox-projects in this shell.
23+
type Stack struct {
24+
25+
// keys holds the stack elements.
26+
// Earlier (lower index number) keys get higher priority.
27+
// This keeps the string representation of the Stack aligned with the PATH value.
28+
keys []string
29+
}
30+
31+
func NewStack(env map[string]string) *Stack {
32+
stackEnv, ok := env[PathStackEnv]
33+
if !ok {
34+
// if path stack is empty, then push the current PATH, which is the
35+
// external environment prior to any devbox-shellenv being applied to it.
36+
stackEnv = InitPathEnv
37+
env[InitPathEnv] = env["PATH"]
38+
}
39+
return &Stack{
40+
keys: strings.Split(stackEnv, ":"),
41+
}
42+
}
43+
44+
// String is the value of the Stack stored in its env-var.
45+
func (s *Stack) String() string {
46+
return strings.Join(s.keys, ":")
47+
}
48+
49+
// Key is the element stored in the Stack for a devbox-project. It represents
50+
// a pointer to the nixEnvPath value stored in its own env-var, also using this same
51+
// Key.
52+
func Key(projectHash string) string {
53+
return "DEVBOX_NIX_" + projectHash
54+
}
55+
56+
// AddToEnv adds the new nixEnvPath for the devbox-project identified by projectHash to the env.
57+
// It does so by modifying the following env-vars:
58+
// 1. nixEnvPath key
59+
// 2. PathStack
60+
// 3. PATH
61+
func (s *Stack) AddToEnv(env map[string]string, projectHash, nixEnvPath string) map[string]string {
62+
key := Key(projectHash)
63+
64+
// Add this nixEnvPath to env
65+
env[key] = nixEnvPath
66+
67+
// Add this key to the stack b/c earlier keys get priority
68+
s.keys = lo.Uniq(slices.Insert(s.keys, 0, key))
69+
env[PathStackEnv] = s.String()
70+
71+
// Look up the paths-list for each paths-stack element, and join them together to get the final PATH.
72+
pathLists := lo.Map(s.keys, func(part string, idx int) string { return env[part] })
73+
env["PATH"] = JoinPathLists(pathLists...)
74+
return env
75+
}
76+
77+
// Has tests if the stack has the specified key. Refer to the Key function for constructing
78+
// the appropriate key for any devbox-project.
79+
func (s *Stack) Has(key string) bool {
80+
for _, k := range s.keys {
81+
if k == key {
82+
return true
83+
}
84+
}
85+
return false
86+
}

internal/impl/envvars.go

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import (
88
"os"
99
"sort"
1010
"strings"
11+
12+
"go.jetpack.io/devbox/internal/impl/envpath"
1113
)
1214

1315
const devboxSetPrefix = "__DEVBOX_SET_"
@@ -136,5 +138,11 @@ func markEnvsAsSetByDevbox(envs ...map[string]string) {
136138
// as a proxy for this. This allows us to differentiate between global and
137139
// individual project shells.
138140
func (d *Devbox) IsEnvEnabled() bool {
139-
return os.Getenv(d.ogPathKey()) != ""
141+
env := map[string]string{}
142+
for _, keyVal := range os.Environ() {
143+
parts := strings.Split(keyVal, "=")
144+
env[parts[0]] = parts[1]
145+
}
146+
pathStack := envpath.NewStack(env)
147+
return pathStack.Has(envpath.Key(d.projectDirHash()))
140148
}

internal/impl/shell.go

Lines changed: 0 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -386,37 +386,6 @@ func (s *DevboxShell) linkShellStartupFiles(shellSettingsDir string) {
386386
}
387387
}
388388

389-
// JoinPathLists joins and cleans PATH-style strings of
390-
// [os.ListSeparator] delimited paths. To clean a path list, it splits it and
391-
// does the following for each element:
392-
//
393-
// 1. Applies [filepath.Clean].
394-
// 2. Removes the path if it's relative (must begin with '/' and not be '.').
395-
// 3. Removes the path if it's a duplicate.
396-
func JoinPathLists(pathLists ...string) string {
397-
if len(pathLists) == 0 {
398-
return ""
399-
}
400-
401-
seen := make(map[string]bool)
402-
var cleaned []string
403-
for _, path := range pathLists {
404-
for _, path := range filepath.SplitList(path) {
405-
path = filepath.Clean(path)
406-
if path == "." || path[0] != '/' {
407-
// Remove empty paths and don't allow relative
408-
// paths for security reasons.
409-
continue
410-
}
411-
if !seen[path] {
412-
cleaned = append(cleaned, path)
413-
}
414-
seen[path] = true
415-
}
416-
}
417-
return strings.Join(cleaned, string(filepath.ListSeparator))
418-
}
419-
420389
func filterPathList(pathList string, keep func(string) bool) string {
421390
filtered := []string{}
422391
for _, path := range filepath.SplitList(pathList) {

internal/impl/shell_test.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
"testing"
1414

1515
"github.com/google/go-cmp/cmp"
16+
"go.jetpack.io/devbox/internal/impl/envpath"
1617
"go.jetpack.io/devbox/internal/shellgen"
1718
)
1819

@@ -125,7 +126,7 @@ func TestCleanEnvPath(t *testing.T) {
125126
}
126127
for _, test := range tests {
127128
t.Run(test.name, func(t *testing.T) {
128-
got := JoinPathLists(test.inPath)
129+
got := envpath.JoinPathLists(test.inPath)
129130
if got != test.outPath {
130131
t.Errorf("Got incorrect cleaned PATH.\ngot: %s\nwant: %s", got, test.outPath)
131132
}

0 commit comments

Comments
 (0)