Skip to content

fix(task): convert PATH to MSYS Unix form when spawning POSIX shells on Windows#9547

Merged
jdx merged 6 commits intojdx:mainfrom
JamBalaya56562:windows-msys-path-conversion
May 2, 2026
Merged

fix(task): convert PATH to MSYS Unix form when spawning POSIX shells on Windows#9547
jdx merged 6 commits intojdx:mainfrom
JamBalaya56562:windows-msys-path-conversion

Conversation

@JamBalaya56562
Copy link
Copy Markdown
Contributor

Maintainer context — scope and prior decisions

This PR addresses the long-standing class of bugs around mise + Windows + POSIX subshells discussed in #3961, #5581, #6633. Per maintainer feedback on those threads:

Accepted:

  • Pure Rust, no cygpath subprocess (Command::new("cygpath")).
  • Narrow scope: only the conversion mise actually needs (PATH list direction).
  • 80% of common cases covered; edge cases pass-through or error explicitly.

Rejected on past PRs (and avoided here):

  • Subprocess to cygpath.
  • Hardcoded MSYSTEM env trigger (the PowerShell→mise→bash path has no MSYSTEM).
  • Full cygpath compat (mount table, UNC, reserved names, etc.).
  • "Land now, fix perf later" — "We need it right the first time."

Distinct from already-merged #4048: that PR fixed mise activate bash output when mise itself runs inside Git Bash. This PR fixes the orthogonal case — mise spawning a POSIX subshell from any host shell.


Summary

  • Add crate::path::windows_path_list_to_unix (pure-Rust ;-separated → :-separated, drive-letter C:\/c/, UNC pass-through).
  • Add crate::path::is_posix_shell_program (basename match against bash/sh/zsh/fish/ksh/dash, case-insensitive, .exe stripped).
  • In task_executor::exec_program, just before CmdLineRunner::envs(...), call maybe_convert_env_for_msys_shell(program, env). On Windows + recognized POSIX shell only, clone the env and rewrite the PATH entry; otherwise return Cow::Borrowed (zero allocation).

The cfg-gated Cow pattern keeps the call site OS-agnostic and adds zero cost to Linux/macOS or to Windows-native-shell tasks.

Design choices

Question Choice Why
Mount prefix /c/ only (Git Bash style) Cygwin's /cygdrive/c/ users typically don't go through bash -c from PowerShell. Configurable setting would be scope creep.
Where to convert task_executor::exec_program, env-passing site only. src/shell/*.rs and hook_env.rs untouched. The bug is in the mise run path, not mise activate (that's PR #5581's scope). Single chokepoint, no shell-escape concerns.
MSYS detection Target program basename (bash/sh/zsh/...), not MSYSTEM env MSYSTEM-trigger misses the PowerShell→mise→bash chain entirely. Program-name trigger covers shebang dispatch (#!/usr/bin/env bash) too.
PowerShell→mise→bash chain Resolved by item 3 above (target-program detection at the spawn boundary). task.shell() resolves to a program path before reaching exec_program; we read it there.

Out of scope (intentional)

  • Cygwin /etc/fstab mount table.
  • UNC paths (\?\C:\..., \server\share\...) — passed through verbatim, bash will fail to use them, matches no-conversion behavior.
  • Cygwin /cygdrive/c/ prefix.
  • Git Bash /usr magic mount — /c/Program Files/Git/usr/bin resolves to the same exe via PATH search, no remap needed.
  • windows_path_list_to_unix does not provide the reverse direction (-wp); not currently needed.

Tests

Unit tests (src/path.rs + src/task/task_executor.rs, all platforms):

  • windows_path_list_to_unix: basic, forward-slash input, mixed separators, Unix entry pass-through, UNC pass-through, empty entries (leading/trailing/sole ;), drive-letter case folding, Program Files with spaces, bare C: / C:foo pass-through, single entry — 11 cases.
  • is_posix_shell_program: bare names, .exe suffix, full Windows paths, Unix paths, BASH.EXE uppercase, negative cases (cmd, powershell, pwsh, rustc, empty) — 1 case with all assertions.
  • maybe_convert_env_for_msys_shell (Windows-only): converts for bash.exe, skips for cmd.exe, full path to bash.exe, plus a Unix-side no-op test — 3 Windows + 1 Unix.

Integration test (e2e-win/task.Tests.ps1): a Pester case that writes a mise.toml with shell = "bash -c", runs the task from PowerShell, and asserts the PATH the task observes contains : (Unix-style) rather than ; (Windows-style). Skips gracefully if bash.exe isn't on PATH.

Local verification

Check Result
cargo test -p mise --bin mise -- path::tests:: task::task_executor::tests:: 14/14 pass
cargo check --workspace --message-format=short exit 0, no new warnings
cargo fmt -p mise --check clean
cargo clippy -- -Dwarnings, cargo build --bin mise, pwsh repro, e2e-win Pester not run locally — Rust 1.95 + getrandom v0.3 raw-dylib requires dlltool that the local MSVC toolchain didn't provide. Deferred to CI.

Acceptance criterion

The repro from #3961:

[tasks.repro_build_rust]
tools = { rust = "1.84.0" }
shell = "bash -c"
run = '''
set -euo pipefail
rustup target add wasm32-wasip1 || true
cargo build --release --target wasm32-wasip1
'''

…run from PowerShell as mise run repro_build_rust should resolve rustup and cargo inside the bash subshell. Bug-source code path verified by unit tests; final repro to be confirmed by CI on this PR.


This PR description was authored with the assistance of an AI coding assistant.

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented May 2, 2026

Greptile Summary

This PR fixes Windows PATH conversion when mise spawns POSIX subshells (e.g. bash -c) for tasks. It adds windows_path_list_to_unix (pure Rust, ;:, drive letters to /c/ form) and is_posix_shell_program (basename detection), calls them at the exec_program spawn boundary via a Cow-based helper, and pre-resolves the shell executable against the unconverted Windows PATH before handing the child a Unix-form PATH so CreateProcess can still locate bash.exe.

Confidence Score: 5/5

This PR is safe to merge — no P0 or P1 issues found.

The implementation is correct and well-scoped. The ordering of resolve_posix_shell_program_path before maybe_convert_env_for_msys_shell is critical (uses unconverted PATH to find bash.exe first) and is correct. The Cow-based helper is zero-cost on non-Windows and non-POSIX-shell paths. All unit tests cover the documented edge cases. The e2e test correctly uses $TestDrive (via Set-Location TestDrive: in BeforeAll) and cleans up properly in a finally block.

No files require special attention.

Important Files Changed

Filename Overview
src/path.rs Adds windows_path_list_to_unix (single-pass, no intermediate Vec) and is_posix_shell_program with comprehensive unit tests covering drive letters, UNC passthrough, forward-slash inputs, empty entries, and basename/extension normalization.
src/task/task_executor.rs Adds resolve_posix_shell_program_path (Windows-only, pre-resolves shell exe using unconverted PATH) and maybe_convert_env_for_msys_shell (Cow-based, zero-cost on non-Windows and non-POSIX-shell paths), wired into exec_program in the correct order.
e2e-win/task.Tests.ps1 Adds a Pester e2e test that writes a shell = "bash -c" task config, runs it, and asserts PATH is : separated; skips gracefully when bash.exe is absent. Uses $TestDrive consistently (Set-Location TestDrive: is established in BeforeAll) and cleans up in a finally block.

Sequence Diagram

sequenceDiagram
    participant PS as PowerShell
    participant mise as mise (exec_program)
    participant RP as resolve_posix_shell_program_path
    participant MC as maybe_convert_env_for_msys_shell
    participant W32 as Win32 CreateProcess
    participant bash as bash subprocess

    PS->>mise: mise run task (shell = "bash -c")
    mise->>RP: ("bash", Windows-form PATH env)
    RP-->>mise: C:\Program Files\Git\bin\bash.exe
    mise->>MC: (bash.exe, Windows-form PATH env)
    MC-->>mise: Cow::Owned(env with Unix-form PATH)
    mise->>W32: spawn("C:\...\bash.exe", Unix PATH env)
    W32->>bash: bash.exe started
    bash-->>bash: PATH=/c/Users/...:/c/tools/bin (resolves tools ✓)
Loading

Reviews (6): Last reviewed commit: "Merge branch 'main' into windows-msys-pa..." | Re-trigger Greptile

Comment thread src/path.rs Outdated
Comment thread src/task/task_executor.rs
Comment thread src/path.rs
Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces automatic PATH conversion from Windows format to MSYS Unix format when spawning POSIX shells (such as bash, sh, or zsh) on Windows. It adds utility functions in src/path.rs to handle the conversion of drive letters and separators, integrates this logic into the task executor, and includes comprehensive unit and E2E tests. The review feedback suggests optimizing performance by avoiding environment cloning when no conversion is needed and improving the path conversion logic to handle relative paths containing backslashes.

Comment thread src/task/task_executor.rs
Comment thread src/path.rs Outdated
JamBalaya56562 and others added 5 commits May 3, 2026 04:43
…on Windows

When mise on Windows spawns a POSIX-style shell (`bash -c`, `sh -c`, ...)
for a task, the child receives PATH in Windows form (`C:\foo;D:\bar`).
bash uses `:` as the path separator, so it fails to resolve any command —
including tools mise has just installed for the task via per-task
`tools = { ... }`. PowerShell-launched mise inherits no MSYSTEM, so the
prior `MSYSTEM`-based detection (PR jdx#5581) cannot help here.

Convert PATH to Unix form (`/c/foo:/d/bar`) at the spawn boundary in
`task_executor::exec_program`, gated on the target program being a
recognized POSIX shell name. Pure Rust, no subprocess, no `cygpath`
fallback — narrow scope per maintainer guidance from jdx#3961 / jdx#5581 /
jdx#6633. Linux/macOS and Windows-native-shell tasks are unaffected.

Closes a long-standing class of bugs around per-task `tools = { ... }` +
`shell = "bash -c"` on Windows.
- windows_path_list_to_unix: write directly to a single output String,
  avoiding the intermediate Vec<String> + join allocation. The helper
  becomes append_single_windows_path_to_unix and writes char-by-char,
  removing the per-entry replace() allocation as well.
- maybe_convert_env_for_msys_shell: only clone the env if PATH is
  actually present, using a let-chain to keep the happy path linear.
…dows

Review fixes:
- Skip the env clone when PATH is already in Unix form (no `;` separator
  and no `\` to translate). This is the common case when mise itself runs
  inside Git Bash and spawns another bash subshell — Cow stays Borrowed.
- Translate `\` → `/` for non-canonical-drive, non-UNC entries too.
  POSIX shells do not treat `\` as a separator, so a relative entry like
  `node_modules\.bin` (which mise can inject via `[env] _.path`) was
  previously passed through unchanged and broke command resolution.

Lint fix:
- The pure-Rust path conversion lives in a generic module so unit tests
  build on all platforms, but the production callers are all gated on
  `#[cfg(windows)]`. On non-Windows non-test builds, that triggered
  `dead_code` via `clippy -- -D warnings`. Added
  `#[cfg_attr(not(windows), allow(dead_code))]` on the public symbols
  and the private helper instead of cfg-gating them, to keep the
  cross-platform unit-test surface intact.

Tests:
- New: relative paths with backslashes are translated.
- New: env stays Cow::Borrowed when PATH is already Unix-form.
- New: env stays Cow::Borrowed when PATH is absent entirely.
…n on Windows

Without this, the Windows e2e suite's "shebang task with bash" test and
the "converts PATH to MSYS Unix form" test both fail with empty output:
bash never starts.

Root cause: `Command::spawn` on Windows uses the *child* env's PATH (when
set via `.envs(...)`) to locate the program. The PATH conversion was
handing it `/c/foo:/d/bar`, which Win32 cannot use to find `bash.exe`,
so CreateProcess failed before bash could run.

Fix: when the program is a POSIX shell and the PATH would be converted,
resolve the program to its absolute path first using the still-Windows-
form PATH from the task env. The child then receives an absolute program
argument and the converted Unix-form PATH — the spawn itself bypasses
PATH search at the OS level, and bash sees a usable Unix PATH for its
own command resolution.

This affects both inline tasks (`shell = "bash -c"`) and shebang-driven
file tasks (`#!/usr/bin/env bash`); both flow through `exec_program`,
where the resolution is now performed.
@JamBalaya56562 JamBalaya56562 force-pushed the windows-msys-path-conversion branch from 71dd310 to 5632106 Compare May 2, 2026 19:44
@jdx jdx merged commit 229848c into jdx:main May 2, 2026
34 checks passed
mise-en-dev added a commit that referenced this pull request May 3, 2026
### 🚀 Features

- **(conda)** graduate conda backend out of experimental by @jdx in
[#9544](#9544)
- **(deps)** Add dart and flutter providers by @tjarvstrand in
[#9505](#9505)
- **(registry)** add neo4j by @mnm364 in
[#9525](#9525)
- **(registry)** add rustfs by @mnm364 in
[#9530](#9530)
- **(task)** support exclusion patterns in task sources by
@jlarmstrongiv in [#9496](#9496)
- **(vfox)** add stat function to lua file module by @esteve in
[#9497](#9497)

### 🐛 Bug Fixes

- **(backend)** flag regex prerelease versions by @jdx in
[#9500](#9500)
- **(backend)** mark -nightly/-canary/-experimental as prereleases by
@jdx in [#9523](#9523)
- **(backend)** suppress no-versions warning for unresolved-latest
backends by @jdx in [#9548](#9548)
- **(backend)** include dotnet prereleases from package flags by @jdx in
[#9551](#9551)
- **(backend)** scope PEP 440 prerelease detection to Python backends by
@jdx in [#9558](#9558)
- **(cargo)** Apply install_env during cargo install by @c22 in
[#9502](#9502)
- **(copr)** drop epel-9 chroots since rust >= 1.91 is unavailable by
@jdx in [#9484](#9484)
- **(github)** skip attestations on non-default api_url by @jdx in
[#9486](#9486)
- **(github)** retry ip allow list errors without auth by @risu729 in
[#9506](#9506)
- **(http)** update versions host tracking endpoint by @jdx in
[#9527](#9527)
- **(install)** don't warn for configured tools when version is passed
via CLI by @jdx in [#9522](#9522)
- **(install)** refresh latest before installing missing tools by @jdx
in [#9545](#9545)
- **(install)** don't cache nonexistent install paths by @jdx in
[#9553](#9553)
- **(lockfile)** don't propagate ad-hoc CLI overrides into the project
lockfile by @jdx in [#9562](#9562)
- **(plugin)** detect plugin types after cloning by @risu729 in
[#9540](#9540)
- **(release)** pass --no-git-checks to aube publish by @jdx in
[#9483](#9483)
- **(task)** convert PATH to MSYS Unix form when spawning POSIX shells
on Windows by @JamBalaya56562 in
[#9547](#9547)

### 📚 Documentation

- **(contributing)** require popularity check for registry PRs by @jdx
in
[7bbeebe](7bbeebe)
- **(watch)** update pitchfork domain to en.dev by @risu729 in
[#9536](#9536)
- document ghtkn GitHub token setup by @jdx in
[#9546](#9546)
- clarify registry backend acceptance policy by @jdx in
[#9543](#9543)
- Change exec command to use bash for variable echo by @kuboon in
[#9567](#9567)

### 🧪 Testing

- **(e2e)** run test-tool targets in parallel by @jdx in
[#9564](#9564)
- **(e2e)** run tests in parallel by @jdx in
[#9563](#9563)
- **(e2e)** bind-mount /tmp on disk and surface failed tests in CI
summary by @jdx in [#9570](#9570)
- **(tasks)** migrate test_task_help atask to usage field by @jdx in
[#9549](#9549)

### 📦️ Dependency Updates

- update fedora:45 docker digest to 8b838b3 by @renovate[bot] in
[#9507](#9507)
- update ghcr.io/jdx/mise:deb docker digest to f02194c by @renovate[bot]
in [#9509](#9509)
- update taiki-e/install-action digest to 7769b73 by @renovate[bot] in
[#9512](#9512)
- update ghcr.io/jdx/mise:alpine docker digest to 581f8a8 by
@renovate[bot] in [#9508](#9508)
- update rust crate ctor to v0.10.1 by @renovate[bot] in
[#9515](#9515)
- update ghcr.io/jdx/mise:rpm docker digest to a5c9655 by @renovate[bot]
in [#9510](#9510)
- update rust docker digest to a9cfb75 by @renovate[bot] in
[#9511](#9511)
- update rust crate age to v0.11.3 by @renovate[bot] in
[#9514](#9514)
- update rust crate jiff to v0.2.24 by @renovate[bot] in
[#9516](#9516)
- update dependency vitepress-plugin-tabs to ^0.9.0 by @renovate[bot] in
[#9518](#9518)
- update autofix-ci/action action to v1.3.4 by @renovate[bot] in
[#9513](#9513)
- update rust crate usage-lib to v3.2.1 by @renovate[bot] in
[#9517](#9517)
- update apple-actions/import-codesign-certs action to v7 by
@renovate[bot] in [#9519](#9519)
- update taiki-e/install-action digest to 51cd0b8 by @renovate[bot] in
[#9531](#9531)
- exclude taiki-e/install-action from renovate by @jdx in
[#9532](#9532)
- update rust crate blake3 to v1.8.5 by @renovate[bot] in
[#9533](#9533)

### 📦 Registry

- enable shellcheck on windows by @zeitlinger in
[#9487](#9487)
- add google-java-format by @zeitlinger in
[#9488](#9488)
- add expert
([aqua:expert-lsp/expert](https://github.com/expert-lsp/expert)) by
@AlternateRT in [#9498](#9498)
- update entry for checkmake by @eread in
[#9504](#9504)
- add systemctl-tui
([aqua:rgwood/systemctl-tui](https://github.com/rgwood/systemctl-tui))
by @2xdevv in [#9521](#9521)
- add codon by @3w36zj6 in
[#9538](#9538)
- add tool yr (backend:github:VirusTotal/yara-x) by @adam-moss in
[#9542](#9542)
- add tool betterleaks (backend:aqua/betterleaks/betterleaks) by
@adam-moss in [#9541](#9541)
- add `git-filter-repo` by @garysassano in
[#9550](#9550)
- add umoci
([aqua:opencontainers/umoci](https://github.com/opencontainers/umoci))
by @2xdevv in [#9555](#9555)
- add aqua backend for elixir-ls by @AlternateRT in
[#9557](#9557)
- deny inline backend options by @risu729 in
[#9565](#9565)

### Chore

- **(ci)** fail registry tests without summary by @jdx in
[#9559](#9559)
- **(ci)** use !cancelled() instead of always() for test-ci aggregator
by @jdx in [#9569](#9569)
- **(ci)** use namespace runners for ci jobs by @jdx in
[#9561](#9561)
- **(config)** deprecate shorthands_file setting by @risu729 in
[#9534](#9534)
- **(docs)** remove shrill.en.dev analytics script by @jdx in
[#9539](#9539)
- **(release)** replace bc with awk in release-plz star formatting by
@jdx in
[d7f177f](d7f177f)
- bump hk to 1.44.3 by @jdx in
[#9493](#9493)
- invert CLAUDE.md/AGENTS.md so AGENTS.md is canonical by @jdx in
[#9560](#9560)
- set dev profile debug to 1 by @jdx in
[#9572](#9572)

### New Contributors

- @kuboon made their first contribution in
[#9567](#9567)
- @AlternateRT made their first contribution in
[#9557](#9557)
- @2xdevv made their first contribution in
[#9555](#9555)
- @adam-moss made their first contribution in
[#9541](#9541)
- @jlarmstrongiv made their first contribution in
[#9496](#9496)
- @tjarvstrand made their first contribution in
[#9505](#9505)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants