Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions e2e/shell/test_shims_activate_prepend
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,21 @@ assert_contains "env PATH='$CUSTOM_PATH' $MISE_BIN activate bash --shims" "expor

# Shims not in PATH: should also be emitted as a prepend with shims first
assert_contains "env PATH='/some/other/bin:/usr/bin' $MISE_BIN activate bash --shims" "export PATH=\"$SHIMS_DIR:"

# Regression #10264: when the dir containing the mise executable is already in
# PATH (e.g. a deb install at /usr/bin), --shims must NOT re-prepend it, which
# would reorder PATH (moving /usr/bin ahead of /usr/local/bin). Only the shims
# dir should be prepended, so exactly one `export PATH=` line is emitted.
EXE_DIR="$(dirname "$MISE_BIN")"
if ! out="$(env PATH="$EXE_DIR:/usr/local/bin:/usr/bin" "$MISE_BIN" activate bash --shims 2>&1)"; then
echo "FAIL: 'mise activate bash --shims' exited non-zero:"
printf '%s\n' "$out"
exit 1
fi
assert_contains_text "$out" "export PATH=\"$SHIMS_DIR:"
path_lines="$(printf '%s\n' "$out" | grep -c 'export PATH=' || true)"
if [[ $path_lines != "1" ]]; then
echo "FAIL: expected exactly one 'export PATH=' line (shims only) since the mise dir is already in PATH, got $path_lines:"
printf '%s\n' "$out"
exit 1
fi
19 changes: 12 additions & 7 deletions src/cli/activate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -102,10 +102,14 @@ impl Activate {
fn activate_shims(&self, shell: &dyn Shell, mise_bin: &Path) -> std::io::Result<()> {
let exe_dir = mise_bin.parent().unwrap();
let mut prelude = vec![];
// For shells with native path dedup/reorder (fish), always emit path commands
// using MovePrependEnv so entries get moved to front on re-source (e.g. VS Code).
// For other shells, keep the is_dir_in_path guard to avoid PATH growth on re-source.
if let Some(p) = self.shims_prepend_path(shell, exe_dir) {
// The shims dir is always (move-)prepended so it stays at the front of PATH
// even when activation is re-sourced (e.g. VS Code terminals) — see #8757.
// The mise executable's own dir only needs to be present so `mise` is
// callable, so it uses the guarded prepend: this avoids re-prepending (and
// thereby reordering) a system dir such as /usr/bin that is already in PATH
// for deb/rpm installs, which would otherwise move it ahead of
// /usr/local/bin (#10264).
if let Some(p) = self.prepend_path(exe_dir) {
prelude.push(p);
}
Comment on lines +112 to 114

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Fish: exe_dir switches from MovePrependEnv to PrependEnv

Before this PR, activate_shims() called shims_prepend_path(shell, exe_dir), which emitted MovePrependEnv for fish (using fish's native path dedup so re-sourcing is idempotent). The new prepend_path(exe_dir) always emits PrependEnv regardless of shell. For fish users where exe_dir is not already in PATH (e.g. a custom ~/.local/bin install), re-sourcing the config after a system-level rc resets PATH could duplicate exe_dir, because PrependEnv is not natively idempotent under fish. The guard !is_dir_in_path makes this safe for the common re-source case, but it breaks in the "PATH reset between sources" scenario that MovePrependEnv handles natively. Consider emitting MovePrependEnv(exe_dir) when shell.supports_move_path() is true, falling back to the guarded PrependEnv otherwise.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Thanks, but I don''t think this one applies here. On fish, PrependEnv renders as fish_add_path --global --path (src/shell/fish.rs), and fish_add_path is idempotent — it won''t add a path that''s already present, so re-sourcing (even after a PATH reset) doesn''t duplicate exe_dir. The only behavioural difference vs MovePrependEnv (fish_add_path --move) is whether an already-present entry gets moved to the front.

Moving it is exactly what this PR is fixing: for a deb install exe_dir is /usr/bin, which is already in PATH, so MovePrependEnv would move /usr/bin ahead of /usr/local/bin — reintroducing #10264 on fish. The guarded prepend_path leaves an already-present exe_dir untouched on every shell (and fish_add_path --path adds it idempotently when it is genuinely missing), matching what the non-shims activate() path already does for exe_dir. So I''m keeping prepend_path here.

if let Some(p) = self.shims_prepend_path(shell, &dirs::SHIMS) {
Expand Down Expand Up @@ -165,9 +169,10 @@ impl Activate {
}
}

/// Used by activate_shims. Always prepends the path to the front, even if
/// already present (accepting a duplicate entry). For shells with native path
/// dedup (fish), uses MovePrependEnv to reorder without duplicating.
/// Used by activate_shims for the shims directory. Always prepends the path to
/// the front, even if already present (accepting a duplicate entry), so the
/// shims dir wins on re-source. For shells with native path dedup (fish), uses
/// MovePrependEnv to reorder without duplicating.
fn shims_prepend_path(&self, shell: &dyn Shell, p: &Path) -> Option<ActivatePrelude> {
if !is_dir_not_in_nix(p) || p.is_relative() {
return None;
Expand Down
Loading