fix: PATHEXT resolution on Windows for JS/TS and Python tools#492
fix: PATHEXT resolution on Windows for JS/TS and Python tools#492FlorianBruniaux wants to merge 2 commits intodevelopfrom
Conversation
ba9f5d8 to
5133e2a
Compare
There was a problem hiding this comment.
Pull request overview
This PR fixes Windows command spawning failures by resolving tool binaries in a cross-platform way that honors PATHEXT (e.g., .CMD/.BAT shims for Node/Python tools), replacing prior reliance on the external which executable.
Changes:
- Add
whichcrate and introduceutils::resolve_binary()+utils::tool_exists()as the canonical binary resolution/check mechanism. - Update JS/TS, Python, and central passthrough/proxy entry points to spawn commands via
resolve_binary()(and replace ad-hocwhichchecks). - Remove duplicated per-command
which_command()helpers and update docs/version references to0.28.2.
Reviewed changes
Copilot reviewed 21 out of 22 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| src/utils.rs | Adds resolve_binary() / tool_exists() helpers and unit tests; updates package_manager_exec() to use them |
| src/tsc_cmd.rs | Uses tool_exists() + resolve_binary() for tsc/npx |
| src/tree.rs | Replaces which tree check and spawns tree via resolved binary |
| src/ruff_cmd.rs | Spawns ruff via resolved binary |
| src/pytest_cmd.rs | Uses tool_exists() + resolve_binary() and removes local which_command() |
| src/prisma_cmd.rs | Uses tool_exists() + resolve_binary() for prisma/npx |
| src/pnpm_cmd.rs | Spawns pnpm via resolved binary across subcommands/passthrough |
| src/playwright_cmd.rs | Spawns pnpm/yarn/npx via resolved binary |
| src/pip_cmd.rs | Uses tool_exists() + resolve_binary() and removes local which_command() |
| src/npm_cmd.rs | Spawns npm via resolved binary |
| src/next_cmd.rs | Uses tool_exists() + resolve_binary() for next/npx |
| src/mypy_cmd.rs | Uses tool_exists() + resolve_binary() and removes local which_command() |
| src/main.rs | Applies resolve_binary() to fallback execution, proxy mode, and some npx paths |
| src/lint_cmd.rs | Spawns python linters via resolved binary |
| src/golangci_cmd.rs | Spawns golangci-lint via resolved binary |
| src/format_cmd.rs | Spawns non-JS formatters via resolved binary |
| src/ccusage.rs | Uses tool_exists() + resolve_binary() for ccusage/npx fallback |
| README.md | Updates expected rtk --version output |
| Cargo.toml | Bumps version and adds which = "8" dependency |
| Cargo.lock | Locks which dependency |
| CLAUDE.md | Updates expected rtk --version output |
| ARCHITECTURE.md | Updates referenced RTK version |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| #[test] | ||
| fn test_resolve_binary_known() { | ||
| // cargo exists on all dev machines | ||
| let resolved = resolve_binary("cargo"); | ||
| assert!(!resolved.is_empty()); | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_resolve_binary_unknown() { | ||
| let resolved = resolve_binary("nonexistent_tool_xyz_99999"); | ||
| assert_eq!( | ||
| resolved, | ||
| std::ffi::OsString::from("nonexistent_tool_xyz_99999") | ||
| ); | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_tool_exists_positive() { | ||
| assert!(tool_exists("cargo")); | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_tool_exists_negative() { | ||
| assert!(!tool_exists("nonexistent_tool_xyz_99999")); | ||
| } |
There was a problem hiding this comment.
The new unit tests assume cargo is discoverable on PATH (via tool_exists("cargo")). That’s not guaranteed in all test runners (e.g., if cargo test is invoked via an absolute path and PATH doesn’t include the cargo bin dir), which can make these tests flaky. Consider making the tests self-contained by creating a temporary executable/shim in a temp dir, prepending that dir to PATH (and on Windows using a .cmd file to exercise PATHEXT), then asserting tool_exists() and resolve_binary() behave as expected.
| // Try tsc directly first, fallback to npx if not found | ||
| let tsc_exists = Command::new("which") | ||
| .arg("tsc") | ||
| .output() | ||
| .map(|o| o.status.success()) | ||
| .unwrap_or(false); | ||
| let tsc_exists = tool_exists("tsc"); | ||
|
|
||
| let mut cmd = if tsc_exists { | ||
| Command::new("tsc") | ||
| Command::new(resolve_binary("tsc")) | ||
| } else { | ||
| let mut c = Command::new("npx"); | ||
| let mut c = Command::new(resolve_binary("npx")); | ||
| c.arg("tsc"); |
There was a problem hiding this comment.
This pattern does two PATH scans (tool_exists("tsc") and then resolve_binary("tsc"), both calling which::which). Consider avoiding the duplicate lookup by calling the resolver once (e.g., a helper that returns Option<OsString>/PathBuf), and branching on that result.
Add `which` crate (v8) and two central helpers in utils.rs:
- `resolve_binary(name)` resolves a binary to its full path, honoring
PATHEXT on Windows so `.CMD`/`.BAT`/`.PS1` wrappers are found
- `tool_exists(name)` replaces all `Command::new("which")` usages
(which does not exist on Windows) with a cross-platform check
Apply `resolve_binary()` to:
- All JS/TS tool entry points (tsc, next, prisma, playwright, npm, pnpm,
ruff, golangci-lint, vitest via package_manager_exec)
- Python tool entry points (mypy, pytest, pip/uv)
- The 3 central passthrough/TOML/proxy entry points in main.rs
- `package_manager_exec()` in utils.rs (covers vitest, prettier, lint,
format, biome automatically)
Remove 4 local `which_command()` duplicates (mypy_cmd, pytest_cmd,
pip_cmd, ccusage) and the `binary_exists()` helper in ccusage.
Fixes: #212
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Florian BRUNIAUX <florian@bruniaux.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Signed-off-by: Florian BRUNIAUX <florian@bruniaux.com>
5133e2a to
2aff2fa
Compare
|
Nice work. |
|
Great work @FlorianBruniaux! Clean and well-scoped fix. A few things to address: P1 — P1 — P1 — Double PATH scan pattern let mut cmd = match which::which("next") {
Ok(path) => Command::new(path),
Err(_) => {
let mut c = Command::new(resolve_binary("npx"));
c.arg("next");
c
}
};P2 — Otherwise LGTM — good crate choice, DRY cleanup (4 duplicate |
|
Hey @FlorianBruniaux, closing this in favor of #269 by @F0rty-Tw0 which addresses the same issue and was submitted first (Feb 26). It was already reviewed and LGTM'd. Sorry about the duplicate — we have too many PRs in flight right now and this one slipped through. Thanks for understanding! |
Summary
whichcrate (v8) for cross-platform binary resolution honoring PATHEXT on Windowsutils.rs:resolve_binary()andtool_exists()replacing allCommand::new("which")usagesresolve_binary()to all JS/TS and Python tool entry points, plus the 3 central passthrough/TOML/proxy entry points inmain.rswhich_command()functions (mypy, pytest, pip, ccusage)Root cause
On Windows, JS/TS tools (vitest, eslint, tsc, prettier, pnpm, next, playwright, prisma...) are installed as
.CMD/.BAT/.PS1wrappers.std::process::Command::new("vitest")doesn't resolve PATHEXT, so all these commands fail with "program not found". Additionally,Command::new("which")doesn't exist on Windows — it'swhere.Test plan
cargo fmt --all && cargo clippy --all-targets— 0 errorscargo test— 762 tests passresolve_binary()andtool_exists()helpersrtk git statusworksFixes #212
🤖 Generated with Claude Code