Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"name": "vite-task-bunx-wrapper",
"version": "1.0.0",
"private": true,
"scripts": {
"gate:test": "bun probe",
"probe": "bunx --bun probe"
},
"dependencies": {
"probe-bin": "file:./probe-bin"
},
"devEngines": {
"packageManager": {
"name": "bun",
"version": "1.3.14",
"onFail": "download"
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
#!/usr/bin/env node

console.log('probe binary ran');
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"name": "probe-bin",
"version": "1.0.0",
"bin": {
"probe": "bin.js"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
[[case]]
name = "vite_task_bunx_wrapper"
vp = "local"
steps = [
{ argv = ["vp", "install"], snapshot = false },
{ argv = ["vp", "run", "gate:test"], comment = "managed bunx should execute the package binary without recursively invoking the matching package script" },
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# vite_task_bunx_wrapper

## `vp install`


## `vp run gate:test`

managed bunx should execute the package binary without recursively invoking the matching package script

```
$ bun probe ⊘ cache disabled
$ bunx --bun probe
probe binary ran
```
24 changes: 19 additions & 5 deletions crates/vite_install/src/package_manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -978,9 +978,10 @@ async fn download_bun_package_manager(
let target_dir = home_dir.join("package_manager").join("bun").join(version.as_str());
let install_dir = target_dir.join("bun");

// If shims already exist, return early (same completeness check as the cache
// and the tgz download path)
// If shims already exist, refresh them before returning so wrapper fixes are
// applied to managed Bun versions installed by an older Vite+ release.
if is_package_manager_install_complete(&install_dir, "bun")? {
refresh_bunx_shim_if_needed(&install_dir.join("bin")).await?;
return Ok((install_dir, package_name, version.clone()));
}

Expand Down Expand Up @@ -1049,6 +1050,7 @@ async fn download_bun_package_manager(

if is_package_manager_install_complete(&install_dir, "bun")? {
tracing::debug!("bun install already complete after lock acquisition, skip rename");
refresh_bunx_shim_if_needed(&install_dir.join("bin")).await?;
return Ok((install_dir, package_name, version.clone()));
}

Expand Down Expand Up @@ -1159,15 +1161,27 @@ async fn create_bun_shim_files(bin_prefix: &AbsolutePath) -> Result<(), Error> {

// Create bun shim -> bun.native
let bun_shim = bin_prefix.join("bun");
shim::write_native_shims(&native_bin, &bun_shim).await?;
shim::write_native_shims(&native_bin, &bun_shim, None).await?;

// Create bunx shim -> bun.native (bunx is just bun with different argv[0])
// Native wrappers cannot preserve bunx as argv[0], so select its equivalent x subcommand.
let bunx_shim = bin_prefix.join("bunx");
shim::write_native_shims(&native_bin, &bunx_shim).await?;
shim::write_native_shims(&native_bin, &bunx_shim, Some("x")).await?;
Comment thread
liangmiQwQ marked this conversation as resolved.

Ok(())
}

/// Refresh an older managed Bun installation without touching its native binary
/// or the separate `bun` wrappers.
async fn refresh_bunx_shim_if_needed(bin_prefix: &AbsolutePath) -> Result<(), Error> {

@liangmiQwQ liangmiQwQ Jul 11, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Should we add this refresh logic?

  • If we don't add it, the fix won't work for already-installed Bun. (As Codex's review)
  • If we add it, it will be a legacy patch forever, maintaining this may make code not so clean.

let native_name = if cfg!(windows) { "bun.native.exe" } else { "bun.native" };
let bunx_shim = bin_prefix.join("bunx");
let expected = shim::native_sh_shim(native_name, Some("x"));
if tokio::fs::read_to_string(&bunx_shim).await? != expected {
shim::write_native_shims(bin_prefix.join(native_name), bunx_shim, Some("x")).await?;
}
Ok(())
}

/// Write the resolved package manager into `devEngines.packageManager`.
///
/// Used by auto-pin when detection had no explicit field (rfcs/dev-engines.md):
Expand Down
49 changes: 39 additions & 10 deletions crates/vite_install/src/shim.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ use vite_error::Error;
pub async fn write_native_shims(
source_file: impl AsRef<Path>,
to_bin: impl AsRef<Path>,
subcommand: Option<&str>,
) -> Result<(), Error> {
let to_bin = to_bin.as_ref();
let parent = to_bin
Expand All @@ -23,9 +24,9 @@ pub async fn write_native_shims(
.to_str()
.ok_or_else(|| Error::CannotFindBinaryPath("shim path is not valid UTF-8".into()))?;

write(to_bin, native_sh_shim(relative_file)).await?;
write(to_bin.with_extension("cmd"), native_cmd_shim(relative_file)).await?;
write(to_bin.with_extension("ps1"), native_pwsh_shim(relative_file)).await?;
write(to_bin, native_sh_shim(relative_file, subcommand)).await?;
write(to_bin.with_extension("cmd"), native_cmd_shim(relative_file, subcommand)).await?;
write(to_bin.with_extension("ps1"), native_pwsh_shim(relative_file, subcommand)).await?;

// set executable permission for unix
#[cfg(unix)]
Expand All @@ -39,7 +40,8 @@ pub async fn write_native_shims(
}

/// Unix shell shim for native binaries.
pub fn native_sh_shim(relative_file: &str) -> String {
pub fn native_sh_shim(relative_file: &str, subcommand: Option<&str>) -> String {
let subcommand = subcommand.map_or_else(String::new, |value| format!(" {value}"));
formatdoc! {
r#"
#!/bin/sh
Expand All @@ -53,25 +55,27 @@ pub fn native_sh_shim(relative_file: &str) -> String {
;;
esac

exec "$basedir/{relative_file}" "$@"
exec "$basedir/{relative_file}"{subcommand} "$@"
"#
}
}

/// Windows Command Prompt shim for native binaries.
pub fn native_cmd_shim(relative_file: &str) -> String {
pub fn native_cmd_shim(relative_file: &str, subcommand: Option<&str>) -> String {
let subcommand = subcommand.map_or_else(String::new, |value| format!(" {value}"));
formatdoc! {
r#"
@SETLOCAL
@"%~dp0\{relative_file}" %*
@"%~dp0\{relative_file}"{subcommand} %*
"#,
relative_file = relative_file.replace('/', "\\")
}
.replace('\n', "\r\n")
}

/// `PowerShell` shim for native binaries.
pub fn native_pwsh_shim(relative_file: &str) -> String {
pub fn native_pwsh_shim(relative_file: &str, subcommand: Option<&str>) -> String {
let subcommand = subcommand.map_or_else(String::new, |value| format!(" {value}"));
formatdoc! {
r#"
#!/usr/bin/env pwsh
Expand All @@ -80,9 +84,9 @@ pub fn native_pwsh_shim(relative_file: &str) -> String {
$ret=0
# Support pipeline input
if ($MyInvocation.ExpectingInput) {{
$input | & "$basedir/{relative_file}" $args
$input | & "$basedir/{relative_file}"{subcommand} $args
}} else {{
& "$basedir/{relative_file}" $args
& "$basedir/{relative_file}"{subcommand} $args
}}
$ret=$LASTEXITCODE
exit $ret
Expand Down Expand Up @@ -197,6 +201,8 @@ pub fn pwsh_shim(relative_file: &str) -> String {
#[cfg(test)]
#[cfg(not(windows))] // FIXME
mod tests {
use std::{os::unix::fs::PermissionsExt, process::Command};

use tempfile::TempDir;
use tokio::fs::read_to_string;

Expand All @@ -206,6 +212,29 @@ mod tests {
shim.replace(' ', "·")
}

#[tokio::test]
async fn test_native_shim_forwards_subcommand() {
let temp_dir = TempDir::new().unwrap();
let source = temp_dir.path().join("bin").join("bun.native");
let target = temp_dir.path().join("bin").join("bunx");

tokio::fs::create_dir_all(source.parent().unwrap()).await.unwrap();
tokio::fs::write(&source, "#!/bin/sh\nprintf '%s\\n' \"$@\"\n").await.unwrap();
tokio::fs::set_permissions(&source, std::fs::Permissions::from_mode(0o755)).await.unwrap();

write_native_shims(&source, &target, Some("x")).await.unwrap();

let output = Command::new(&target).args(["--bun", "vitest"]).output().unwrap();
assert!(output.status.success());
assert_eq!(String::from_utf8(output.stdout).unwrap(), "x\n--bun\nvitest\n");

let cmd = read_to_string(target.with_extension("cmd")).await.unwrap();
assert!(cmd.contains("@\"%~dp0\\bun.native\" x %*"));

let pwsh = read_to_string(target.with_extension("ps1")).await.unwrap();
assert!(pwsh.contains("& \"$basedir/bun.native\" x $args"));
}

#[test]
fn test_sh_shim() {
let shim = sh_shim("pnpm.js");
Expand Down
Loading