install(windows): write bunx.cmd (not bunx.exe) when hard link fails
What does this PR do?
On Windows, bun completions installs bunx.exe as a hard link to the running bun.exe. When CreateHardLinkW fails (exFAT/FAT32/network volumes, or the target cannot be replaced), it falls back to writing a .cmd wrapper:
@%~dp0bun.exe x %*
The fallback built its output path from exe_suffix_z ("bunx.exe\0") instead of cmd_suffix ("bunx.cmd"), so the batch text was written into bunx.exe. Windows then refuses to launch it as a PE, so bunx is unusable on any install where the hard link could not be created. The cmd_suffix local was only used for the upfront delete and never for the create. The bug has been present since the fallback was introduced in #9025 and was carried over verbatim in the Rust rewrite.
Before:
let bunx_cmd_with_z = strings::concat_buf_t::<u16>(
&mut bunx_path_buf,
&[&windows::NT_OBJECT_PREFIX, image_dirname, exe_suffix_z], // bunx.exe
)?;
let bunx_cmd = WStr::from_slice_with_nul(&bunx_cmd_with_z[..]);
let file = File::create_w(bun_sys::Fd::cwd(), bunx_cmd.as_slice())?;
After:
let bunx_cmd = strings::concat_buf_t::<u16>(
&mut bunx_path_buf,
&[&windows::NT_OBJECT_PREFIX, image_dirname, cmd_suffix], // bunx.cmd
)?;
let file = File::create_w(bun_sys::Fd::cwd(), bunx_cmd)?;
File::create_w takes a &[u16] slice (same as DeleteFileBun and the uninstall.ps1 write a few lines below), so the WStr::from_slice_with_nul round-trip is no longer needed.
How did you verify your code works?
New Windows-only test in test/cli/install/bunx.test.ts copies the bun executable into a temp dir, creates a directory at the would-be bunx.exe path (blocks both the non-directory delete and CreateHardLinkW, forcing the fallback), runs completions, and asserts bunx.cmd exists with the expected batch contents.
On Windows x64:
# src/ reverted to main
(fail) completions: writes bunx.cmd (not bunx.exe) when hard link fails
error: expect(received).toBe(expected)
Expected: true
Received: false // bunx.cmd does not exist
# with this change
(pass) completions: writes bunx.cmd (not bunx.exe) when hard link fails [617ms]
The changed function is #[cfg(windows)], so the fail-before/pass-after proof only exists on Windows; the test is it.skipIf(!isWindows) and is a no-op elsewhere.
no test proof · iteration 2 · Platform-specific test(s) that do not run on this machine. Deferring to CI, which covers all platforms: test/cli/install/bunx.test.ts