feat(shell): use to_unix_path_list for PATH conversion under GitBash/Msys2 on Windows#5581
feat(shell): use to_unix_path_list for PATH conversion under GitBash/Msys2 on Windows#5581drop-stones wants to merge 3 commits intojdx:mainfrom
Conversation
abbe418 to
c6a0d93
Compare
There was a problem hiding this comment.
Bug: Cygpath Test-Function Mismatch
The test_to_unix_path_list's cygpath availability check is inconsistent with the to_unix_path_list function's usage. The test uses cygpath --version and only verifies command execution (.is_ok()), whereas the function uses cygpath -u -p and also checks for successful exit status (.status.success()). This discrepancy can cause test failures where the test expects a Unix path but the function falls back to the original path.
src/path.rs#L54-L58
Lines 54 to 58 in 5aafb00
BugBot free trial expires on July 22, 2025
You have used $0.00 of your $50.00 spend limit so far. Manage your spend limit in the Cursor dashboard.
Was this report helpful? Give feedback by reacting with 👍 or 👎
|
|
||
| #[cfg(windows)] | ||
| pub(crate) fn to_unix_path_list(path: &str) -> String { | ||
| if let Ok(output) = std::process::Command::new("cygpath") |
There was a problem hiding this comment.
shelling out to an external command is probably going to ruin performance on windows
There was a problem hiding this comment.
Thank you for your feedback regarding performance concerns.
I have addressed this by using Lazy to check for cygpath availability only once at startup with which("cygpath").is_ok().
This ensures that the existence check is cached and not repeated, minimizing the performance impact.
ef0f4f1#diff-4f41ae2071112f0de383f7e16b2b53c4bd1cc49df54b30e665f94e59a2ca755cR62
| } | ||
| #[cfg(not(windows))] | ||
| { | ||
| exe.to_string_lossy().replace('\\', r#"\\"#) |
There was a problem hiding this comment.
I think you could reduce some of the duplication by passing an enum, something like path::to_path_list(PathEscape::Foo, ...)
There was a problem hiding this comment.
I have added the to_path_list function and introduced the PathEscape enum to make path escaping extensible and reduce duplication.
ef0f4f1#diff-4f41ae2071112f0de383f7e16b2b53c4bd1cc49df54b30e665f94e59a2ca755cR34-R56
5aafb00 to
e56d540
Compare
There was a problem hiding this comment.
Pull Request Overview
This PR introduces a new utility to convert Windows-style path lists to Unix-style using cygpath, and applies it across all shell modules to improve compatibility under MSYS2/Cygwin on Windows.
- Adds
to_path_listwith aPathEscapeenum insrc/path.rsfor flexible path transformations. - Updates all shell implementations (bash, zsh, fish, xonsh, nushell, elvish) to use
to_path_listfor executables andPATHenvironment values. - Includes unit tests covering basic backslash escaping and Unix conversion scenarios.
Reviewed Changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| src/path.rs | Defines PathEscape, implements to_path_list, and adds tests |
| src/shell/bash.rs | Imports and applies to_path_list for activate and set_env |
| src/shell/zsh.rs | Imports and applies to_path_list for activate |
| src/shell/fish.rs | Imports and applies to_path_list for activate and set_env |
| src/shell/xonsh.rs | Imports and applies to_path_list for activate and set_env |
| src/shell/nushell.rs | Imports and applies to_path_list for activate and set_env |
| src/shell/elvish.rs | Imports and applies to_path_list for activate and set_env |
Comments suppressed due to low confidence (2)
src/path.rs:34
- Consider adding doc comments for the
PathEscapeenum andto_path_listfunction to explain the intent, supported escape options, and expected input/output formats.
pub(crate) enum PathEscape {
src/path.rs:86
- Add a test case that applies both
PathEscape::UnixandPathEscape::EscapeBackslashtogether to verify combined transformations in one call.
fn test_to_path_list_backslash() {
| static CYGPATH_AVAILABLE: Lazy<bool> = Lazy::new(|| which("cygpath").is_ok()); | ||
|
|
||
| #[test] | ||
| fn test_to_path_list_unix() { | ||
| let input = "C:\\foo;D:\\bar"; | ||
| let output = to_path_list(&[PathEscape::Unix], input); | ||
| if *CYGPATH_AVAILABLE { |
There was a problem hiding this comment.
The tests define their own CYGPATH_AVAILABLE which duplicates module logic. Consider reusing or exposing the module's static to avoid divergence.
| static CYGPATH_AVAILABLE: Lazy<bool> = Lazy::new(|| which("cygpath").is_ok()); | |
| #[test] | |
| fn test_to_path_list_unix() { | |
| let input = "C:\\foo;D:\\bar"; | |
| let output = to_path_list(&[PathEscape::Unix], input); | |
| if *CYGPATH_AVAILABLE { | |
| // Use CYGPATH_AVAILABLE from the windows_path module. | |
| #[test] | |
| fn test_to_path_list_unix() { | |
| let input = "C:\\foo;D:\\bar"; | |
| let output = to_path_list(&[PathEscape::Unix], input); | |
| if *super::windows_path::CYGPATH_AVAILABLE { |
| use once_cell::sync::Lazy; | ||
| use which::which; | ||
|
|
||
| static CYGPATH_AVAILABLE: Lazy<bool> = Lazy::new(|| which("cygpath").is_ok()); | ||
|
|
||
| pub(super) fn to_unix_path_list(path: &str) -> String { | ||
| if *CYGPATH_AVAILABLE { | ||
| if let Ok(output) = std::process::Command::new("cygpath") | ||
| .args(["-u", "-p", path]) | ||
| .output() | ||
| { | ||
| if output.status.success() { | ||
| if let Ok(s) = String::from_utf8(output.stdout) { | ||
| return s.trim().to_string(); |
There was a problem hiding this comment.
Each call to to_unix_path_list may spawn a cygpath process. You might cache the converted path or batch conversions to reduce repeated process overhead.
| use once_cell::sync::Lazy; | |
| use which::which; | |
| static CYGPATH_AVAILABLE: Lazy<bool> = Lazy::new(|| which("cygpath").is_ok()); | |
| pub(super) fn to_unix_path_list(path: &str) -> String { | |
| if *CYGPATH_AVAILABLE { | |
| if let Ok(output) = std::process::Command::new("cygpath") | |
| .args(["-u", "-p", path]) | |
| .output() | |
| { | |
| if output.status.success() { | |
| if let Ok(s) = String::from_utf8(output.stdout) { | |
| return s.trim().to_string(); | |
| use once_cell::sync::{Lazy, Mutex}; | |
| use std::collections::HashMap; | |
| use which::which; | |
| static CYGPATH_AVAILABLE: Lazy<bool> = Lazy::new(|| which("cygpath").is_ok()); | |
| static PATH_CACHE: Lazy<Mutex<HashMap<String, String>>> = Lazy::new(|| Mutex::new(HashMap::new())); | |
| pub(super) fn to_unix_path_list(path: &str) -> String { | |
| if *CYGPATH_AVAILABLE { | |
| let mut cache = PATH_CACHE.lock().unwrap(); | |
| if let Some(cached) = cache.get(path) { | |
| return cached.clone(); | |
| } | |
| if let Ok(output) = std::process::Command::new("cygpath") | |
| .args(["-u", "-p", path]) | |
| .output() | |
| { | |
| if output.status.success() { | |
| if let Ok(s) = String::from_utf8(output.stdout) { | |
| let result = s.trim().to_string(); | |
| cache.insert(path.to_string(), result.clone()); | |
| return result; |
| static CYGPATH_AVAILABLE: Lazy<bool> = Lazy::new(|| which("cygpath").is_ok()); | ||
|
|
||
| pub(super) fn to_unix_path_list(path: &str) -> String { | ||
| if *CYGPATH_AVAILABLE { |
There was a problem hiding this comment.
Understood, I will update the implementation to check both relevant environment variables and the existence of cygpath.exe for more reliable detection.
There was a problem hiding this comment.
I have updated the implementation so that it first checks environment variables to determine the environment, and only checks for the existence of cygpath.exe when its execution is actually required.
|
|
||
| pub(super) fn to_unix_path_list(path: &str) -> String { | ||
| if *CYGPATH_AVAILABLE { | ||
| if let Ok(output) = std::process::Command::new("cygpath") |
There was a problem hiding this comment.
you didn't address my concern about perf
There was a problem hiding this comment.
To clarify, in the current implementation, cygpath.exe is only called in environments like Git Bash or MSYS2 if it is available; in typical Windows environments, it is not used at all.
Regarding your concern about performance, are you mainly worried about the overhead of calling cygpath.exe repeatedly in MSYS2?
If so, would you prefer that I reimplement the conversion logic internally instead of relying on cygpath.exe?
I would appreciate your guidance on which approach you think would be best for improving performance.
There was a problem hiding this comment.
Simple path conversion functionality can be easily implemented in Rust, but supporting the more complex path conversion logic provided by cygpath.exe (as implemented in cygwin/cygwin/winsup/utils/cygpath.cc) may be somewhat challenging.
There was a problem hiding this comment.
I have added an optimization so that when converting canonical Windows paths (such as C:/foo), the conversion is performed directly without calling cygpath.exe. This should improve performance for simple path conversions.
56f9f46 to
6f7200c
Compare
a928588 to
9b7c273
Compare
94c6ff9 to
f2196ca
Compare
|
@jdx I need this patch to reliably use IntelliJ IDEA with Claude Code side-by-side on Windows. If the performance is the primary concern here, I think it's better to merge this and improve the implementation later if needed. Even a non-performant implementation can be useful to many users. 🙏 |
|
This has definitely been a slightly stickler for me with Mise, using Bash on Windows. Is there a path forward here still? |
|
I'd prefer to see a solution that doesn't involve subprocesses. If I get some time I'll dust off my windows laptop and see if I can figure something out |
|
fwiw here is my +100 for pushing this even if suboptimal. git bash/wsl2 is quite high % of devs on windows. |
|
if I merge this in with perf issues nobody will ever fix it, so no, I'm not going to do that. We need it right the first time. |
|
@jdx so you want a implementation of cygpath in native rust to handle those remaining narrow cases? |
|
Yeah |
|
i'm not a rust expert but could loading of cygwin dll dynamically be an option and use the native api ? |
|
I think that'd be fine, but I suspect it's probably harder than just pointing claude at the c++ and asking to rewrite it in rust |
|
I thought I'd take a shot at pointing Claude at the c++ and it's estimating the effort would involve around 10k lines of code in order to not need to FFI to cygwin DLLs, but would still not guarantee parity. I am not nearly proficient enough at Rust to have any confidence that I could use Claude to produce this project without the result being I'm just crossing my fingers it gets it right. Here's the project breakdown as documented by Claude |
|
I don't understand how a function that converts unix paths to cygwin paths would need 10k loc |
|
I'm guessing it's because it's intended to be really comprehensive. There's all kinds of edge cases in how paths resolve and cygpath presumably tries to handle all of them correctly, which is non-trivial. If we exclude the FFI approach, and exclude the re-implement the whole thing approach, then then we have to decide what level of accuracy in the conversion is acceptable. Like cygpath handles paths that start with Would it be acceptable for mise to be broken if such a path would be encountered that needs those features to be converted correctly, for example? If the goal should be to implement a converter that would work for 80% of cases, how would the edge cases that need to be handled to pull that off be identified? |
|
yeah I think that'd be a good approach |
|
@drop-stones I think that means the only change needed would be to replace falling back to cygwin on unsupported cases with producing an error instead, since the basic conversion already in place should cover common use cases. |
|
@jdx was this replaced by some other PR? |
One of the issues is cygwin's mount table, which powers stuff like The mount table lives in a combination of the windows registry and cygwin's |
|
For about three months, I have been following the addition of Git Bash terminal support in Windows, because I exclusively use this terminal myself. I don't use mise yet, but I planned to switch to it immediately after Git Bash terminal support was added, so that the transition would be as easy as possible. I was very happy when this pull request and several others related to it were marked as drafts, because I thought that this issue would be resolved relatively quickly. And now, this pull request, like several others related to adding Git Bash terminal support, has simply been closed without any explanation... It seems that the problem is simply being ignored. I am more concerned not with solving this particular problem, but with the approach to solving problems as such. As a user of the Git Bash terminal in Windows, I would be satisfied with any solution, even the use of third-party tools such as cygpath, because any solution to the problem, even if it is far from ideal, is much better than a faulty tool. While I was following the addition of Git Bash terminal support, I read through about 90% of the documentation. I really liked mise as a tool and planned to use quite a few of its features, but now I don't even know what to do... Anyway, I would like to know the reason for closing this and other similar pull requests. If there are no plans to add Git Bash terminal support, I recommend clearly stating this in the documentation so that others know what to expect. But that would be strange, because the roadmap for 2025 includes the following:
|
|
if someone wants to make another pr with the cygpath fallback I'll reconsider |
|
@jdx you mean a PR without the cygpath fallback right? since that's the part you're not OK with? just stick to something which accomplishes the same thing in rust? this pr has that code for one simple case (all Windows paths according to the comment) but then it falls back to cygpath in other cases https://github.com/jdx/mise/pull/5581/files#diff-4f41ae2071112f0de383f7e16b2b53c4bd1cc49df54b30e665f94e59a2ca755cR84 |
|
yeah I think it's just a lot more complex than I thought to support it the way I wanted |
|
For what it's worth, I'm solving this within the bash code with a single call to cygpath to rewrite the entire PATH var in one go. Roughly, it looks like this: (a few other tweaks needed to make it safe) Be careful to cache the path to cygpath in a var if you're trying to run cygpath while PATH is corrupted. Remember to do this after every instance where mise rewrites PATH. Nice thing about doing this in bash is that end-users can trial and test this change themselves today, without any code changes to mise. Tweak the output of |
|
here is a workaround that i've been using for a week now, it works seamlessly! phrechu/mise-activate-gitbash |
|
Here's a caching version of the workaround: edit the standard unix bash activation code as follows: if [ -z "${__MISE_ORIG_PATH:-}" ]; then
export __MISE_ORIG_PATH="$(cygpath -wp "$PATH")"
fi
# ...
declare -gA mise_path_cache
_mise_hook() {
local previous_exit_status=$?
eval "$(command mise hook-env -s bash)";
[[ $PATH != *'\'* ]] || PATH=${mise_path_cache["$PATH"]="$(/bin/cygpath -p "$PATH")"}
return $previous_exit_status;
}...and also drop all the hardcoded paths to mise.exe, leaving "command mise" just as in the linux version. (e.g. by doing I've tested this with both Cygwin and git bash: it only needs to run cygpath -p whenever it sees a PATH value containing backslashes, that it hasn't seen before. (The caching part is optional and can be dropped if older bash versions need to be supported.) AFAICT, these are the only places where path-list conversion needs to take place. When cygwin or msys (git bash) call out to mise, PATH is auto-converted to Windows-style paths; but If you want to make this more robust, the activation script can check Looking at bash.rs, it looks pretty simple to add everything right there if you'd like a PR; I don't have as much experience with other shells but this would at least get git bash, cygwin bash, and other msys/mingw bash all sorted. |
|
Claude's plan is massively overscoped for what mise needs, btw: cygpath has over two dozen options, but the only combos mise needs are And you could reduce the scope even more by requiring mise settings instead of using
With these options in place, you could get away with not needing to read (and most importantly find!) /etc/fstab, the registry, or anything else. You're just doing pure string path manipulation (similar to e.g. https://github.com/forgottenswitch/cygpathint/ ) unless using the cygpath fallback. That being said, getting all the corner cases right might be tricky, and require a lot of test cases. |
…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.
…on Windows (#9547) ## Maintainer context — scope and prior decisions This PR addresses the long-standing class of bugs around mise + Windows + POSIX subshells discussed in [#3961](#3961), [#5581](#5581), [#6633](#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](#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](#3961): ```toml [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.* --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: jdx <[email protected]>
This PR adds a utility function
to_unix_path_listto convert Windows-style paths to Unix-style usingcygpath.All shell implementations now use this function to ensure correct PATH and executable handling on Windows, improving compatibility with MSYS2/cygwin environments.
to_unix_path_listinsrc/path.rsIf
cygpathis unavailable, the original path is used.Related discussion: #3961
Previous related issue: https://github.com/jdx/mise/issues/4011