From ca404a285d4a4b76b02eae8039f9a7ca7152b3de Mon Sep 17 00:00:00 2001 From: Luca Di Maio Date: Sat, 20 Jun 2026 15:29:28 +0200 Subject: [PATCH] fix(enter): reorder PATH so /usr/local/{s,}bin precede /usr/{s,}bin Signed-off-by: Luca Di Maio --- pkg/containermanager/containermanager.go | 37 ++++++++++++++++++- .../providers/docker_internal_test.go | 16 ++++---- 2 files changed, 43 insertions(+), 10 deletions(-) diff --git a/pkg/containermanager/containermanager.go b/pkg/containermanager/containermanager.go index 740a2f06..673ae1b0 100644 --- a/pkg/containermanager/containermanager.go +++ b/pkg/containermanager/containermanager.go @@ -155,11 +155,44 @@ func BuildContainerPath(cleanPath bool, hostPath string, containerPath string) s } } + merged := hostPath if len(additionalPaths) > 0 { - return hostPath + ":" + strings.Join(additionalPaths, ":") + merged = hostPath + ":" + strings.Join(additionalPaths, ":") } - return hostPath + return reorderFHSPath(merged) +} + +// reorderFHSPath ensures /usr/local/bin precedes /usr/bin and /usr/local/sbin +// precedes /usr/sbin, so distrobox wrappers in /usr/local/* win — mirroring the +// reference shell (distrobox-enter:478-512). +func reorderFHSPath(path string) string { + var reordered []string + for _, p := range strings.Split(path, ":") { + switch p { + case "/usr/local/bin", "/usr/local/sbin": + // Skip here; re-inserted right before its /usr counterpart. + case "/usr/bin": + reordered = append(reordered, "/usr/local/bin", "/usr/bin") + case "/usr/sbin": + reordered = append(reordered, "/usr/local/sbin", "/usr/sbin") + default: + reordered = append(reordered, p) + } + } + + result := strings.Join(reordered, ":") + + // If /usr/bin or /usr/sbin were absent, their local counterparts were + // skipped above; re-add any that went missing (prepended, like the shell). + for _, lp := range []string{"/usr/local/bin", "/usr/local/sbin"} { + pattern := regexp.MustCompile(`(:|^)` + regexp.QuoteMeta(lp) + `(:|$)`) + if !pattern.MatchString(result) { + result = lp + ":" + result + } + } + + return result } func BuildXDGPaths(envVar string, standardPaths []string) string { diff --git a/pkg/containermanager/providers/docker_internal_test.go b/pkg/containermanager/providers/docker_internal_test.go index 7ff72b2b..6b0b8409 100644 --- a/pkg/containermanager/providers/docker_internal_test.go +++ b/pkg/containermanager/providers/docker_internal_test.go @@ -148,21 +148,21 @@ func TestBuildContainerPath(t *testing.T) { cleanPath: false, hostPath: "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", containerPath: "/container/path", - want: "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", + want: "/usr/local/sbin:/usr/sbin:/usr/local/bin:/usr/bin:/sbin:/bin", }, { name: "hostPath missing some standard paths - adds them", cleanPath: false, hostPath: "/usr/bin:/bin", containerPath: "/container/path", - want: "/usr/bin:/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/sbin", + want: "/usr/local/bin:/usr/bin:/bin:/usr/local/sbin:/usr/sbin:/sbin", }, { name: "hostPath with custom paths and missing standard paths", cleanPath: false, hostPath: "/custom/bin:/usr/bin:/another/path", containerPath: "/container/path", - want: "/custom/bin:/usr/bin:/another/path:/usr/local/sbin:/usr/local/bin:/usr/sbin:/sbin:/bin", + want: "/custom/bin:/usr/local/bin:/usr/bin:/another/path:/usr/local/sbin:/usr/sbin:/sbin:/bin", }, { name: "empty hostPath - returns containerPath", @@ -181,32 +181,32 @@ func TestBuildContainerPath(t *testing.T) { name: "hostPath with standard paths at beginning", cleanPath: false, hostPath: "/bin:/usr/bin:/custom/path", - want: "/bin:/usr/bin:/custom/path:/usr/local/sbin:/usr/local/bin:/usr/sbin:/sbin", + want: "/bin:/usr/local/bin:/usr/bin:/custom/path:/usr/local/sbin:/usr/sbin:/sbin", }, { name: "hostPath with standard paths at end", cleanPath: false, hostPath: "/custom/path:/bin:/usr/bin", - want: "/custom/path:/bin:/usr/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/sbin", + want: "/custom/path:/bin:/usr/local/bin:/usr/bin:/usr/local/sbin:/usr/sbin:/sbin", }, { name: "hostPath with similar but not exact standard paths", cleanPath: false, hostPath: "/usr/binary:/binfo", - want: "/usr/binary:/binfo:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", + want: "/usr/binary:/binfo:/usr/local/sbin:/usr/sbin:/usr/local/bin:/usr/bin:/sbin:/bin", }, { name: "single custom path in hostPath", cleanPath: false, hostPath: "/opt/custom/bin", containerPath: "/container/path", - want: "/opt/custom/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", + want: "/opt/custom/bin:/usr/local/sbin:/usr/sbin:/usr/local/bin:/usr/bin:/sbin:/bin", }, { name: "empty containerPath with non-empty hostPath", cleanPath: false, hostPath: "/custom/path", - want: "/custom/path:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", + want: "/custom/path:/usr/local/sbin:/usr/sbin:/usr/local/bin:/usr/bin:/sbin:/bin", }, { name: "empty containerPath with empty hostPath",