Skip to content

Commit 2722c75

Browse files
authored
fix(cli): resolve lowercase Windows shims in case-sensitive directories (#2727)
Fixes #2690. pnpm 12 creates lowercase `.cmd` shims. On Windows, uppercase `PATHEXT` entries prevent `which` from finding those shims in case-sensitive directories. Add uppercase and lowercase variants next to each configured `PATHEXT` entry when the global CLI or NAPI binding starts. With the default order, `.EXE` and `.exe` precede `.CMD` and `.cmd`. Vite Task, `fspy`, and child processes use the same extension order with the published `which` crate. Run `windows_case_sensitive_shims` in an isolated NTFS directory on Windows shard `1`. Other snapshots keep their existing temp-directory settings.
1 parent a3035cf commit 2722c75

19 files changed

Lines changed: 312 additions & 3 deletions

File tree

‎.github/workflows/ci.yml‎

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1102,6 +1102,18 @@ jobs:
11021102
with:
11031103
tool: cargo-nextest
11041104

1105+
# Keep fixtures outside runner.temp: setup-node writes an .npmrc there,
1106+
# which Yarn Classic discovers when walking parent directories.
1107+
- name: Create NTFS temp directory for case-sensitive shim snapshots
1108+
if: matrix.shard == 1
1109+
id: snapshot-temp
1110+
shell: pwsh
1111+
run: |
1112+
$volume = [System.IO.Path]::GetPathRoot($env:RUNNER_TEMP)
1113+
$snapshotTemp = Join-Path $volume ('vp-snapshots-' + [guid]::NewGuid().ToString('N'))
1114+
New-Item -ItemType Directory -Path $snapshotTemp | Out-Null
1115+
"directory=$snapshotTemp" >> $env:GITHUB_OUTPUT
1116+
11051117
# `cargo-nextest` is invoked directly so the job never depends on the
11061118
# runner's Rust toolchain. --workspace-remap makes nextest rewrite
11071119
# CARGO_MANIFEST_DIR and CARGO_BIN_EXE_vpt to this checkout, which is
@@ -1121,7 +1133,20 @@ jobs:
11211133
export VP_SNAP_PWSH_BIN="$(cygpath -w "$(command -v pwsh.exe)")"
11221134
# --no-fail-fast: on a snapshot suite every diff is diagnostic
11231135
# signal; cancelling on the first failure hides the rest.
1124-
cargo-nextest nextest run --archive-file windows-snapshot-tests.tar.zst --workspace-remap . --no-fail-fast --partition hash:${{ matrix.shard }}/3
1136+
test_exit=0
1137+
# This fixture requires case-sensitive directory support, which the
1138+
# default temp directory on these runners does not provide.
1139+
cargo-nextest nextest run --archive-file windows-snapshot-tests.tar.zst --workspace-remap . --no-fail-fast --partition hash:${{ matrix.shard }}/3 \
1140+
-E 'not test(windows_case_sensitive_shims)' || test_exit=$?
1141+
1142+
if [[ '${{ matrix.shard }}' == '1' ]]; then
1143+
# Run both flavors on NTFS once. Rust's GetTempPath2 ignores
1144+
# TEMP/TMP under the SYSTEM account, so override SystemTemp.
1145+
SystemTemp='${{ steps.snapshot-temp.outputs.directory }}' \
1146+
cargo-nextest nextest run --archive-file windows-snapshot-tests.tar.zst --workspace-remap . --no-fail-fast \
1147+
-E 'test(windows_case_sensitive_shims)' || test_exit=$?
1148+
fi
1149+
exit "$test_exit"
11251150
env:
11261151
RUST_BACKTRACE: '1'
11271152
# Keep Windows env parity with the `test` recipe in justfile.

‎Cargo.lock‎

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
const fs = require('node:fs');
2+
3+
fs.mkdirSync('node_modules/.bin', { recursive: true });
4+
fs.writeFileSync('node_modules/.bin/astro.CMD', '@echo wrong workspace shim %*\r\n');
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
const { execFileSync } = require('node:child_process');
2+
const path = require('node:path');
3+
4+
const vp = path.join(path.dirname(require.resolve('vite-plus/package.json')), 'bin', 'vp');
5+
// The runner's node shim can expand PATHEXT before this script starts.
6+
// Reset it in a new process that launches real Node directly, so only the
7+
// NAPI initializer can supply the lowercase extension for this CLI lookup.
8+
execFileSync(process.execPath, [vp, 'exec', 'astro', '--version'], {
9+
cwd: path.resolve('packages/app'),
10+
env: { ...process.env, PATHEXT: '.COM;.EXE;.BAT;.CMD' },
11+
stdio: 'inherit',
12+
timeout: 30000,
13+
});
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"name": "case-sensitive-shims",
3+
"private": true,
4+
"packageManager": "pnpm@12.4.1",
5+
"scripts": {
6+
"probe": "vp run --filter @fixture/app probe"
7+
}
8+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"name": "@fixture/app",
3+
"private": true,
4+
"scripts": {
5+
"probe": "astro --version",
6+
"priority": "node-priority -e \"process.exit(0)\""
7+
}
8+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
console.log('local shim', ...process.argv.slice(2));
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
export default {
2+
run: {
3+
tasks: {
4+
cached: {
5+
command: "astro cached",
6+
},
7+
},
8+
},
9+
};
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
packages:
2+
- packages/*
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
const assert = require('node:assert/strict');
2+
const { execFileSync } = require('node:child_process');
3+
const fs = require('node:fs');
4+
const path = require('node:path');
5+
6+
const bin = path.resolve('packages/app/node_modules/.bin');
7+
fs.mkdirSync(bin, { recursive: true });
8+
// The directory must be empty when case sensitivity is enabled. Do not depend
9+
// on the checkout's NTFS flags or the casing chosen by a package manager.
10+
execFileSync('fsutil.exe', ['file', 'setCaseSensitiveInfo', bin, 'enable'], { stdio: 'inherit' });
11+
fs.writeFileSync(path.join(bin, 'astro.cmd'), '@echo off\r\nnode "%~dp0/../../print.cjs" %*\r\n');
12+
// A lowercase executable must win over an uppercase command shim in the same directory.
13+
fs.copyFileSync(process.execPath, path.join(bin, 'node-priority.exe'));
14+
fs.writeFileSync(path.join(bin, 'node-priority.CMD'), '@echo wrong cmd shim\r\n@exit /b 1\r\n');
15+
assert.equal(fs.existsSync(path.join(bin, 'astro.CMD')), false);
16+
assert.ok(process.env.PATHEXT.split(';').includes('.CMD'));

0 commit comments

Comments
 (0)