From 028590593ec8adab32bf0f5542ff410c1d8bc5d3 Mon Sep 17 00:00:00 2001 From: Liang Mi Date: Sat, 27 Jun 2026 17:38:00 +0800 Subject: [PATCH 1/8] wip --- Cargo.lock | 1 + Cargo.toml | 1 + crates/vite_global_cli/Cargo.toml | 1 + crates/vite_global_cli/src/shim/mod.rs | 38 +++++++++++++++++++++----- 4 files changed, 34 insertions(+), 7 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7407ba2b66..b50d05f1e5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8365,6 +8365,7 @@ dependencies = [ "node-semver", "owo-colors", "oxc_resolver", + "same-file", "serde", "serde_json", "serial_test", diff --git a/Cargo.toml b/Cargo.toml index 96ff8ef816..fc863d2fa1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -258,6 +258,7 @@ rolldown-notify-debouncer-full = "0.7.5" rusqlite = { version = "0.39.0", features = ["bundled"] } rustc-hash = "2.1.1" rustls = { version = "0.23", default-features = false, features = ["ring", "std", "tls12"] } +same-file = "1.0.6" schemars = "1.0.0" self_cell = "1.2.0" node-semver = "2.2.0" diff --git a/crates/vite_global_cli/Cargo.toml b/crates/vite_global_cli/Cargo.toml index 321f7ee45b..852ac3696e 100644 --- a/crates/vite_global_cli/Cargo.toml +++ b/crates/vite_global_cli/Cargo.toml @@ -19,6 +19,7 @@ directories = { workspace = true } dialoguer = { workspace = true } futures = { workspace = true } flate2 = { workspace = true } +same-file = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } node-semver = { workspace = true } diff --git a/crates/vite_global_cli/src/shim/mod.rs b/crates/vite_global_cli/src/shim/mod.rs index e602199107..09843d46c8 100644 --- a/crates/vite_global_cli/src/shim/mod.rs +++ b/crates/vite_global_cli/src/shim/mod.rs @@ -14,11 +14,16 @@ pub(crate) mod corepack; pub(crate) mod dispatch; pub(crate) mod exec; +use std::fs; + pub(crate) use cache::invalidate_cache; pub use dispatch::dispatch; pub(crate) use dispatch::find_system_tool; +use same_file::is_same_file; use vite_shared::env_vars; +use crate::commands::env::config::get_bin_dir; + /// Core shim tools (node, npm, npx). /// /// `corepack` is also a default shim (see `commands::env::setup::SHIM_TOOLS`) @@ -28,6 +33,7 @@ use vite_shared::env_vars; pub const CORE_SHIM_TOOLS: &[&str] = &["node", "npm", "npx"]; /// Extract the tool name from argv[0]. +/// We hope all /// /// Handles various formats: /// - `node` (Unix) @@ -36,11 +42,29 @@ pub const CORE_SHIM_TOOLS: &[&str] = &["node", "npm", "npx"]; /// - `C:\path\node.exe` (Windows full path) pub fn extract_tool_name(argv0: &str) -> String { let path = std::path::Path::new(argv0); - let stem = path.file_stem().unwrap_or_default().to_string_lossy(); + let file_name = path.file_name().unwrap_or_default(); + + let bin_dir = get_bin_dir(); + if let Ok(bin_dir) = bin_dir { + let path = bin_dir.join(file_name); + + if let Ok(read_dir) = fs::read_dir(&bin_dir) { + for bin in read_dir.flatten() { + if is_same_file(bin_dir.join(bin.file_name()), &path).unwrap_or(false) { + return bin + .path() + // Handle Windows: strip .exe, .cmd extensions if present in stem + // (file_stem already strips the extension) + .file_stem() + .unwrap_or_default() + .to_string_lossy() + .to_string(); + } + } + } + } - // Handle Windows: strip .exe, .cmd extensions if present in stem - // (file_stem already strips the extension) - stem.to_lowercase() + path.file_stem().unwrap_or_default().to_string_lossy().to_string() } /// Check if the given tool name is a core shim tool (node/npm/npx). @@ -140,10 +164,10 @@ pub fn detect_shim_tool(argv0: &str) -> Option { // (so argv[0] would be "vp"), but the env var carries the real tool name. if let Some(tool) = env_tool { if !tool.is_empty() { - let tool_lower = tool.to_lowercase(); + let tool = extract_tool_name(&tool); // Accept any tool from env var (could be core or package binary) - if tool_lower != "vp" { - return Some(tool_lower); + if tool != "vp" { + return Some(tool); } } } From ee08f590d9927649167039a41644871b7e937983 Mon Sep 17 00:00:00 2001 From: Liang Mi Date: Sat, 27 Jun 2026 17:57:29 +0800 Subject: [PATCH 2/8] wip --- .../snap-tests-global/env-install-mixed-case-bin/cli.js | 1 + .../env-install-mixed-case-bin/package.json | 7 +++++++ .../snap-tests-global/env-install-mixed-case-bin/snap.txt | 7 +++++++ .../env-install-mixed-case-bin/steps.json | 4 ++++ 4 files changed, 19 insertions(+) create mode 100644 packages/cli/snap-tests-global/env-install-mixed-case-bin/cli.js create mode 100644 packages/cli/snap-tests-global/env-install-mixed-case-bin/package.json create mode 100644 packages/cli/snap-tests-global/env-install-mixed-case-bin/snap.txt create mode 100644 packages/cli/snap-tests-global/env-install-mixed-case-bin/steps.json diff --git a/packages/cli/snap-tests-global/env-install-mixed-case-bin/cli.js b/packages/cli/snap-tests-global/env-install-mixed-case-bin/cli.js new file mode 100644 index 0000000000..1414203c3c --- /dev/null +++ b/packages/cli/snap-tests-global/env-install-mixed-case-bin/cli.js @@ -0,0 +1 @@ +console.log('mixed-case bin executed'); diff --git a/packages/cli/snap-tests-global/env-install-mixed-case-bin/package.json b/packages/cli/snap-tests-global/env-install-mixed-case-bin/package.json new file mode 100644 index 0000000000..d696975b9e --- /dev/null +++ b/packages/cli/snap-tests-global/env-install-mixed-case-bin/package.json @@ -0,0 +1,7 @@ +{ + "name": "env-install-mixed-case-bin", + "version": "1.0.0", + "bin": { + "MixedCaseBin": "./cli.js" + } +} diff --git a/packages/cli/snap-tests-global/env-install-mixed-case-bin/snap.txt b/packages/cli/snap-tests-global/env-install-mixed-case-bin/snap.txt new file mode 100644 index 0000000000..eedf810cee --- /dev/null +++ b/packages/cli/snap-tests-global/env-install-mixed-case-bin/snap.txt @@ -0,0 +1,7 @@ +> vp install -g . +info: Installing 1 global package with Node.js +✓ Installed env-install-mixed-case-bin + Bins: MixedCaseBin + +> MixedCaseBin +mixed-case bin executed diff --git a/packages/cli/snap-tests-global/env-install-mixed-case-bin/steps.json b/packages/cli/snap-tests-global/env-install-mixed-case-bin/steps.json new file mode 100644 index 0000000000..6705dca3b0 --- /dev/null +++ b/packages/cli/snap-tests-global/env-install-mixed-case-bin/steps.json @@ -0,0 +1,4 @@ +{ + "commands": ["vp install -g .", "MixedCaseBin"], + "after": ["vp remove -g env-install-mixed-case-bin"] +} From ed9e8bade9c8a6d3dc9a736080e2ec4462c9e811 Mon Sep 17 00:00:00 2001 From: Liang Mi Date: Sat, 27 Jun 2026 18:32:38 +0800 Subject: [PATCH 3/8] simplify --- Cargo.lock | 1 - Cargo.toml | 1 - crates/vite_global_cli/Cargo.toml | 1 - crates/vite_global_cli/src/shim/mod.rs | 43 +++++++++++++++----------- 4 files changed, 25 insertions(+), 21 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b50d05f1e5..7407ba2b66 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8365,7 +8365,6 @@ dependencies = [ "node-semver", "owo-colors", "oxc_resolver", - "same-file", "serde", "serde_json", "serial_test", diff --git a/Cargo.toml b/Cargo.toml index fc863d2fa1..96ff8ef816 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -258,7 +258,6 @@ rolldown-notify-debouncer-full = "0.7.5" rusqlite = { version = "0.39.0", features = ["bundled"] } rustc-hash = "2.1.1" rustls = { version = "0.23", default-features = false, features = ["ring", "std", "tls12"] } -same-file = "1.0.6" schemars = "1.0.0" self_cell = "1.2.0" node-semver = "2.2.0" diff --git a/crates/vite_global_cli/Cargo.toml b/crates/vite_global_cli/Cargo.toml index 852ac3696e..321f7ee45b 100644 --- a/crates/vite_global_cli/Cargo.toml +++ b/crates/vite_global_cli/Cargo.toml @@ -19,7 +19,6 @@ directories = { workspace = true } dialoguer = { workspace = true } futures = { workspace = true } flate2 = { workspace = true } -same-file = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } node-semver = { workspace = true } diff --git a/crates/vite_global_cli/src/shim/mod.rs b/crates/vite_global_cli/src/shim/mod.rs index 09843d46c8..d1c4a59f5a 100644 --- a/crates/vite_global_cli/src/shim/mod.rs +++ b/crates/vite_global_cli/src/shim/mod.rs @@ -19,7 +19,6 @@ use std::fs; pub(crate) use cache::invalidate_cache; pub use dispatch::dispatch; pub(crate) use dispatch::find_system_tool; -use same_file::is_same_file; use vite_shared::env_vars; use crate::commands::env::config::get_bin_dir; @@ -33,7 +32,7 @@ use crate::commands::env::config::get_bin_dir; pub const CORE_SHIM_TOOLS: &[&str] = &["node", "npm", "npx"]; /// Extract the tool name from argv[0]. -/// We hope all +/// We hope all bins should be put under $VP_HOME/bin /// /// Handles various formats: /// - `node` (Unix) @@ -44,27 +43,35 @@ pub fn extract_tool_name(argv0: &str) -> String { let path = std::path::Path::new(argv0); let file_name = path.file_name().unwrap_or_default(); - let bin_dir = get_bin_dir(); - if let Ok(bin_dir) = bin_dir { - let path = bin_dir.join(file_name); - - if let Ok(read_dir) = fs::read_dir(&bin_dir) { - for bin in read_dir.flatten() { - if is_same_file(bin_dir.join(bin.file_name()), &path).unwrap_or(false) { - return bin - .path() - // Handle Windows: strip .exe, .cmd extensions if present in stem - // (file_stem already strips the extension) - .file_stem() - .unwrap_or_default() + // Handle Windows: strip .exe, .cmd extensions if present in stem + // (file_stem already strips the extension) + let stem = path.file_stem().unwrap_or_default().to_string_lossy().to_string(); + if cfg!(target_os = "linux") { + stem + } else { + let bin_dir = get_bin_dir(); + if let Ok(bin_dir) = bin_dir { + if let Ok(read_dir) = fs::read_dir(&bin_dir) { + for bin in read_dir.flatten() { + if bin + .file_name() .to_string_lossy() - .to_string(); + .to_lowercase() + .starts_with(&file_name.to_string_lossy().to_lowercase()) + { + return bin + .path() + .file_stem() + .unwrap_or_default() + .to_string_lossy() + .to_string(); + } } } } - } - path.file_stem().unwrap_or_default().to_string_lossy().to_string() + stem + } } /// Check if the given tool name is a core shim tool (node/npm/npx). From 8fc1e747c308d443e361836d89a0e720716820ab Mon Sep 17 00:00:00 2001 From: Liang Mi Date: Sat, 27 Jun 2026 18:43:32 +0800 Subject: [PATCH 4/8] fix --- crates/vite_global_cli/src/shim/mod.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/crates/vite_global_cli/src/shim/mod.rs b/crates/vite_global_cli/src/shim/mod.rs index d1c4a59f5a..a372d02289 100644 --- a/crates/vite_global_cli/src/shim/mod.rs +++ b/crates/vite_global_cli/src/shim/mod.rs @@ -41,7 +41,6 @@ pub const CORE_SHIM_TOOLS: &[&str] = &["node", "npm", "npx"]; /// - `C:\path\node.exe` (Windows full path) pub fn extract_tool_name(argv0: &str) -> String { let path = std::path::Path::new(argv0); - let file_name = path.file_name().unwrap_or_default(); // Handle Windows: strip .exe, .cmd extensions if present in stem // (file_stem already strips the extension) @@ -54,10 +53,12 @@ pub fn extract_tool_name(argv0: &str) -> String { if let Ok(read_dir) = fs::read_dir(&bin_dir) { for bin in read_dir.flatten() { if bin - .file_name() + .path() + .file_stem() + .unwrap_or_default() .to_string_lossy() .to_lowercase() - .starts_with(&file_name.to_string_lossy().to_lowercase()) + .starts_with(&stem.to_lowercase()) { return bin .path() From d81f597e88e57428f792118107d4bdfc8e5cae99 Mon Sep 17 00:00:00 2001 From: Liang Mi Date: Sat, 27 Jun 2026 18:46:49 +0800 Subject: [PATCH 5/8] eq --- crates/vite_global_cli/src/shim/mod.rs | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/crates/vite_global_cli/src/shim/mod.rs b/crates/vite_global_cli/src/shim/mod.rs index a372d02289..d213901361 100644 --- a/crates/vite_global_cli/src/shim/mod.rs +++ b/crates/vite_global_cli/src/shim/mod.rs @@ -52,13 +52,8 @@ pub fn extract_tool_name(argv0: &str) -> String { if let Ok(bin_dir) = bin_dir { if let Ok(read_dir) = fs::read_dir(&bin_dir) { for bin in read_dir.flatten() { - if bin - .path() - .file_stem() - .unwrap_or_default() - .to_string_lossy() - .to_lowercase() - .starts_with(&stem.to_lowercase()) + if bin.path().file_stem().unwrap_or_default().to_string_lossy().to_lowercase() + == stem.to_lowercase() { return bin .path() From 2d0a40bc8928ed798ccd9c0848b839df0fee81da Mon Sep 17 00:00:00 2001 From: Liang Mi Date: Sat, 27 Jun 2026 19:34:03 +0800 Subject: [PATCH 6/8] wip --- crates/vite_global_cli/src/commands/global/install.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/crates/vite_global_cli/src/commands/global/install.rs b/crates/vite_global_cli/src/commands/global/install.rs index f750221a4d..54db924ee3 100644 --- a/crates/vite_global_cli/src/commands/global/install.rs +++ b/crates/vite_global_cli/src/commands/global/install.rs @@ -73,6 +73,7 @@ pub(crate) fn is_vp_shim_target( /// may take BinConfig ownership of the corepack shim (see /// `create_package_shim`). pub(crate) fn is_protected_shim(bin_name: &str) -> bool { + let bin_name = if cfg!(target_os = "linux") { bin_name } else { &bin_name.to_lowercase() }; CORE_SHIMS.contains(&bin_name) || crate::commands::env::setup::SHIM_TOOLS.contains(&bin_name) } @@ -1228,6 +1229,13 @@ mod tests { // Regular bins are unrestricted assert!(package_may_own_bin("typescript", "tsc")); + + #[cfg(any(windows, target_os = "macos"))] + assert!(!package_may_own_bin("some-package", "NPM")); + #[cfg(any(windows, target_os = "macos"))] + assert!(!package_may_own_bin("some-package", "Node")); + #[cfg(any(windows, target_os = "macos"))] + assert!(!package_may_own_bin("some-package", "VP")); } #[tokio::test] From af473d8369e1753dfab2c09c68477370c59219d0 Mon Sep 17 00:00:00 2001 From: Liang Mi Date: Sat, 27 Jun 2026 20:04:47 +0800 Subject: [PATCH 7/8] finish --- crates/vite_global_cli/src/commands/global/install.rs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/crates/vite_global_cli/src/commands/global/install.rs b/crates/vite_global_cli/src/commands/global/install.rs index 54db924ee3..83e14b4587 100644 --- a/crates/vite_global_cli/src/commands/global/install.rs +++ b/crates/vite_global_cli/src/commands/global/install.rs @@ -72,8 +72,9 @@ pub(crate) fn is_vp_shim_target( /// created for packages either, with one exception: `vp install -g corepack` /// may take BinConfig ownership of the corepack shim (see /// `create_package_shim`). -pub(crate) fn is_protected_shim(bin_name: &str) -> bool { - let bin_name = if cfg!(target_os = "linux") { bin_name } else { &bin_name.to_lowercase() }; +pub(crate) fn is_protected_shim(bin_name: &str, remove: bool) -> bool { + let bin_name = + if cfg!(target_os = "linux") || remove { bin_name } else { &bin_name.to_lowercase() }; CORE_SHIMS.contains(&bin_name) || crate::commands::env::setup::SHIM_TOOLS.contains(&bin_name) } @@ -83,7 +84,7 @@ pub(crate) fn is_protected_shim(bin_name: &str) -> bool { /// resolution order. The exemption is scoped to the package name; any other /// package declaring a `corepack` bin must not take BinConfig ownership. pub(crate) fn package_may_own_bin(package_name: &str, bin_name: &str) -> bool { - !is_protected_shim(bin_name) || (bin_name == "corepack" && package_name == "corepack") + !is_protected_shim(bin_name, false) || (bin_name == "corepack" && package_name == "corepack") } /// Options for [`install`]. @@ -903,7 +904,7 @@ pub async fn uninstall(package_name: &str, dry_run: bool) -> Result<(), Error> { output::raw(&format!("Would uninstall {}:", package_name)); for bin_name in &bins { // Protected shims survive the real uninstall; keep dry-run honest. - if is_protected_shim(bin_name) { + if is_protected_shim(bin_name, true) { output::raw(&format!( " - shim: {} (kept: default shim)", bin_dir.join(bin_name).as_path().display() @@ -1094,7 +1095,7 @@ async fn remove_package_shim( // Don't remove protected shims (e.g., `vp remove -g corepack` must keep // the default corepack shim so it falls back to the Node-bundled or // auto-installed corepack). - if is_protected_shim(bin_name) { + if is_protected_shim(bin_name, true) { return Ok(()); } From df61ee608bccdb818ddb2bee9d5872cd139c206e Mon Sep 17 00:00:00 2001 From: Liang Mi Date: Sat, 27 Jun 2026 20:19:53 +0800 Subject: [PATCH 8/8] wip --- crates/vite_global_cli/src/commands/global/install.rs | 10 +++++----- crates/vite_global_cli/src/shim/dispatch.rs | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/crates/vite_global_cli/src/commands/global/install.rs b/crates/vite_global_cli/src/commands/global/install.rs index 83e14b4587..0b09e52493 100644 --- a/crates/vite_global_cli/src/commands/global/install.rs +++ b/crates/vite_global_cli/src/commands/global/install.rs @@ -72,9 +72,9 @@ pub(crate) fn is_vp_shim_target( /// created for packages either, with one exception: `vp install -g corepack` /// may take BinConfig ownership of the corepack shim (see /// `create_package_shim`). -pub(crate) fn is_protected_shim(bin_name: &str, remove: bool) -> bool { +pub(crate) fn is_protected_shim(bin_name: &str, ignore_case: bool) -> bool { let bin_name = - if cfg!(target_os = "linux") || remove { bin_name } else { &bin_name.to_lowercase() }; + if cfg!(target_os = "linux") || !ignore_case { bin_name } else { &bin_name.to_lowercase() }; CORE_SHIMS.contains(&bin_name) || crate::commands::env::setup::SHIM_TOOLS.contains(&bin_name) } @@ -84,7 +84,7 @@ pub(crate) fn is_protected_shim(bin_name: &str, remove: bool) -> bool { /// resolution order. The exemption is scoped to the package name; any other /// package declaring a `corepack` bin must not take BinConfig ownership. pub(crate) fn package_may_own_bin(package_name: &str, bin_name: &str) -> bool { - !is_protected_shim(bin_name, false) || (bin_name == "corepack" && package_name == "corepack") + !is_protected_shim(bin_name, true) || (bin_name == "corepack" && package_name == "corepack") } /// Options for [`install`]. @@ -904,7 +904,7 @@ pub async fn uninstall(package_name: &str, dry_run: bool) -> Result<(), Error> { output::raw(&format!("Would uninstall {}:", package_name)); for bin_name in &bins { // Protected shims survive the real uninstall; keep dry-run honest. - if is_protected_shim(bin_name, true) { + if is_protected_shim(bin_name, false) { output::raw(&format!( " - shim: {} (kept: default shim)", bin_dir.join(bin_name).as_path().display() @@ -1095,7 +1095,7 @@ async fn remove_package_shim( // Don't remove protected shims (e.g., `vp remove -g corepack` must keep // the default corepack shim so it falls back to the Node-bundled or // auto-installed corepack). - if is_protected_shim(bin_name, true) { + if is_protected_shim(bin_name, false) { return Ok(()); } diff --git a/crates/vite_global_cli/src/shim/dispatch.rs b/crates/vite_global_cli/src/shim/dispatch.rs index 5c4fecfa89..47776f7a31 100644 --- a/crates/vite_global_cli/src/shim/dispatch.rs +++ b/crates/vite_global_cli/src/shim/dispatch.rs @@ -268,7 +268,7 @@ fn check_npm_global_install_result( // Skip protected shims (core shims and default env shims). Tell // the user for the non-core names (e.g. `npm i -g corepack`): // npm installed the package, but the binary stays unlinked. - if is_protected_shim(&bin_name) { + if is_protected_shim(&bin_name, false) { if !crate::commands::global::CORE_SHIMS.contains(&bin_name.as_str()) { let hint = if bin_name == "corepack" { " Use `vp install -g corepack` to manage its version." @@ -530,7 +530,7 @@ fn remove_npm_global_uninstall_links(bin_entries: &[(String, String)], npm_prefi // Skip protected shims: a stale Npm BinConfig (e.g. a pre-default-shim // `npm install -g corepack`) must not let `npm uninstall -g` delete a // default shim that `vp env setup` now owns. - if is_protected_shim(bin_name) { + if is_protected_shim(bin_name, false) { continue; }