install(windows): write bunx.cmd (not bunx.exe) when hard link fails

Closed
RO
1mo ago
+36-8

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