Skip to content

feat(shell): use to_unix_path_list for PATH conversion under GitBash/Msys2 on Windows#5581

Closed
drop-stones wants to merge 3 commits intojdx:mainfrom
drop-stones:feature/msys2-path-support
Closed

feat(shell): use to_unix_path_list for PATH conversion under GitBash/Msys2 on Windows#5581
drop-stones wants to merge 3 commits intojdx:mainfrom
drop-stones:feature/msys2-path-support

Conversation

@drop-stones
Copy link
Copy Markdown

This PR adds a utility function to_unix_path_list to convert Windows-style paths to Unix-style using cygpath.
All shell implementations now use this function to ensure correct PATH and executable handling on Windows, improving compatibility with MSYS2/cygwin environments.

  • Adds to_unix_path_list in src/path.rs
  • Applies conversion for PATH and exe in all shell modules
  • Includes tests for the new utility

If cygpath is unavailable, the original path is used.

Related discussion: #3961
Previous related issue: https://github.com/jdx/mise/issues/4011

Copilot AI review requested due to automatic review settings July 12, 2025 03:48

This comment was marked as outdated.

@drop-stones drop-stones force-pushed the feature/msys2-path-support branch from abbe418 to c6a0d93 Compare July 12, 2025 04:12
Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

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

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

mise/src/path.rs

Lines 54 to 58 in 5aafb00

let input = "C:\\foo;D:\\bar";
let cygpath_available = std::process::Command::new("cygpath")
.arg("--version")
.output()
.is_ok();

Fix in CursorFix in Web


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 👎

Comment thread src/path.rs Outdated

#[cfg(windows)]
pub(crate) fn to_unix_path_list(path: &str) -> String {
if let Ok(output) = std::process::Command::new("cygpath")
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

shelling out to an external command is probably going to ruin performance on windows

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

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

Comment thread src/shell/nushell.rs Outdated
}
#[cfg(not(windows))]
{
exe.to_string_lossy().replace('\\', r#"\\"#)
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

I think you could reduce some of the duplication by passing an enum, something like path::to_path_list(PathEscape::Foo, ...)

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

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

@drop-stones drop-stones force-pushed the feature/msys2-path-support branch from 5aafb00 to e56d540 Compare July 13, 2025 07:40
@jdx jdx requested review from Copilot and jdx July 13, 2025 12:25
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

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_list with a PathEscape enum in src/path.rs for flexible path transformations.
  • Updates all shell implementations (bash, zsh, fish, xonsh, nushell, elvish) to use to_path_list for executables and PATH environment 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 PathEscape enum and to_path_list function 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::Unix and PathEscape::EscapeBackslash together to verify combined transformations in one call.
    fn test_to_path_list_backslash() {

Comment thread src/path.rs Outdated
Comment on lines +98 to +104
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 {
Copy link

Copilot AI Jul 13, 2025

Choose a reason for hiding this comment

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

The tests define their own CYGPATH_AVAILABLE which duplicates module logic. Consider reusing or exposing the module's static to avoid divergence.

Suggested change
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 {

Copilot uses AI. Check for mistakes.
Comment thread src/path.rs Outdated
Comment on lines +59 to +72
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();
Copy link

Copilot AI Jul 13, 2025

Choose a reason for hiding this comment

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

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.

Suggested change
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;

Copilot uses AI. Check for mistakes.
Comment thread src/path.rs Outdated
static CYGPATH_AVAILABLE: Lazy<bool> = Lazy::new(|| which("cygpath").is_ok());

pub(super) fn to_unix_path_list(path: &str) -> String {
if *CYGPATH_AVAILABLE {
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

this is not an ok heuristic

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Understood, I will update the implementation to check both relevant environment variables and the existence of cygpath.exe for more reliable detection.

Copy link
Copy Markdown
Author

@drop-stones drop-stones Jul 21, 2025

Choose a reason for hiding this comment

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

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.

6f7200c

Comment thread src/path.rs Outdated

pub(super) fn to_unix_path_list(path: &str) -> String {
if *CYGPATH_AVAILABLE {
if let Ok(output) = std::process::Command::new("cygpath")
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

you didn't address my concern about perf

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

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.

https://github.com/jdx/mise/pull/5581/files#diff-4f41ae2071112f0de383f7e16b2b53c4bd1cc49df54b30e665f94e59a2ca755cR88-R107

@drop-stones drop-stones force-pushed the feature/msys2-path-support branch from 94c6ff9 to f2196ca Compare August 2, 2025 09:27
@finalchild
Copy link
Copy Markdown
Contributor

@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. 🙏

@christopher-buss
Copy link
Copy Markdown

This has definitely been a slightly stickler for me with Mise, using Bash on Windows.

Is there a path forward here still?

@jdx
Copy link
Copy Markdown
Owner

jdx commented Sep 5, 2025

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

@maxandersen
Copy link
Copy Markdown
Contributor

fwiw here is my +100 for pushing this even if suboptimal. git bash/wsl2 is quite high % of devs on windows.

@jdx
Copy link
Copy Markdown
Owner

jdx commented Sep 26, 2025

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.

@maxandersen
Copy link
Copy Markdown
Contributor

@jdx so you want a implementation of cygpath in native rust to handle those remaining narrow cases?

@jdx
Copy link
Copy Markdown
Owner

jdx commented Sep 27, 2025

Yeah

@maxandersen
Copy link
Copy Markdown
Contributor

i'm not a rust expert but could loading of cygwin dll dynamically be an option and use the native api ?

@jdx
Copy link
Copy Markdown
Owner

jdx commented Sep 28, 2025

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

@jdx jdx marked this pull request as draft October 11, 2025 13:22
@OneGeek
Copy link
Copy Markdown

OneGeek commented Oct 31, 2025

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

@jdx
Copy link
Copy Markdown
Owner

jdx commented Oct 31, 2025

I don't understand how a function that converts unix paths to cygwin paths would need 10k loc

@OneGeek
Copy link
Copy Markdown

OneGeek commented Oct 31, 2025

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 \\?\ and parses fstab entries to handle mount table logic, falling back to examining the Windows registry if necessary.

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?

@jdx
Copy link
Copy Markdown
Owner

jdx commented Oct 31, 2025

yeah I think that'd be a good approach

@OneGeek
Copy link
Copy Markdown

OneGeek commented Nov 4, 2025

@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 jdx closed this Nov 12, 2025
@maxandersen
Copy link
Copy Markdown
Contributor

@jdx was this replaced by some other PR?

@cspotcode
Copy link
Copy Markdown
Contributor

cspotcode commented Nov 17, 2025

I don't understand how a function that converts unix paths to cygwin paths would need 10k loc

One of the issues is cygwin's mount table, which powers stuff like cygpath -w /bin -> C:\Program Files\Git\usr\bin

The mount table lives in a combination of the windows registry and cygwin's /etc/fstab (cygpath -w /etc/fstab -> C:\Program Files\Git\etc\fstab)

@superuser0
Copy link
Copy Markdown

superuser0 commented Nov 18, 2025

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:

Further Windows support - non-WSL Windows support was added in 2024 but it is not heavily used. There are definitely bugs and gaps with Windows remaining, but we should be able to get Windows much closer to UNIX by the end of the year.

@jdx
Copy link
Copy Markdown
Owner

jdx commented Nov 20, 2025

if someone wants to make another pr with the cygpath fallback I'll reconsider

@jcrben
Copy link
Copy Markdown

jcrben commented Nov 20, 2025

@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

@jdx
Copy link
Copy Markdown
Owner

jdx commented Nov 20, 2025

yeah I think it's just a lot more complex than I thought to support it the way I wanted

@cspotcode
Copy link
Copy Markdown
Contributor

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)

if [ $OSTYPE = ...I forget, did exactly what python venvs do ... ] ; then
export PATH="$(cygpath -up "$PATH")"
fi

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 mise activate, save it in a file, dot source it in your shell profile. Or get fancy with mise activate | sed to do the tweak programmatically, without writing to a file.

@phrechu
Copy link
Copy Markdown

phrechu commented Dec 28, 2025

here is a workaround that i've been using for a week now, it works seamlessly! phrechu/mise-activate-gitbash

@pjeby
Copy link
Copy Markdown
Contributor

pjeby commented Mar 7, 2026

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 case $OSTYPE in cygwin|msys) MISE_EXE="$(cygpath {exe})" ;; *) MISE_EXE={exe} ;; esac at the top of the script and then using "$MISE_EXE" throughout the rest of the script.)

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 __MISE_ORIG_PATH is only converted by msys (Cygwin leaves its original contents intact). When the resulting path is output by mise, however, both cygwin and msys need to convert it to a unix-style path, using cygpath.

If you want to make this more robust, the activation script can check $OSTYPE at runtime to see whether cygpath operations are needed, and then there can be just one standard activation script for bash regardless of platform.

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.

@pjeby
Copy link
Copy Markdown
Contributor

pjeby commented Mar 9, 2026

Claude's plan is massively overscoped for what mise needs, btw: cygpath has over two dozen options, but the only combos mise needs are -u, -up, and -wp. (The -wp is for converting __MISE_ORIGINAL_PATH on read, because it's going to be a unix path that needs mapping back to Windows before diffing.)

And you could reduce the scope even more by requiring mise settings instead of using /etc/fstab. You would need:

  1. A setting for the drive mount prefix (/ is the msys default, /cygdrive the cygwin default, and I've seen some use of /mnt for interop with WSL). Defaulting to / would work with git bash out of the box, or you could use /proc/cygdrive which works with anything except WSL. (OTOH, defaulting to /mnt would work transparently with WSL and is easily fixed by everyone else adding an explicit mise setting.)

  2. A setting for the install root (e.g. C:\cygwin). This can only be defaulted by searching PATH for cygpath.exe (or cygwin1.dll, etc.), then removing either bin or usr/bin from its parent directory path. (This setting is needed so you can strip it from Windows paths before converting them back to unix, so the user doesn't end up with /usr/bin in their existing path being replaced by e.g. /c/Users/whoever/scoop/apps/git/current/usr/bin!)

  3. A setting to disable the emulation and just use cygpath instead, in case there are bugs or limitations in the emulation, or if you need to use non-native symlinks (which should be considered out of scope, since beginning with Windows 10 cygwin and msys can and should use native symlinks).

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.

JamBalaya56562 added a commit to JamBalaya56562/mise that referenced this pull request May 2, 2026
…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.
jdx added a commit that referenced this pull request May 2, 2026
…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]>
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.