Skip to content
Merged
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
65 changes: 64 additions & 1 deletion crates/vite_global_cli/src/commands/env/bin_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -137,11 +137,25 @@ impl BinConfig {
Ok(())
}

/// Find all binaries with `Vp` source (installed via `vp install -g`).
///
/// Used during shim refresh to discover package shims that need their
/// trampoline executables updated after a vite-plus upgrade.
#[cfg_attr(not(windows), allow(dead_code))] // Only called from #[cfg(windows)] refresh_package_shims
pub async fn find_all_vp_source() -> Result<Vec<String>, Error> {
Self::find_bins_where(|config| config.source == BinSource::Vp).await
}

/// Find all binaries installed by a package.
///
/// This is used as a fallback during uninstall when PackageMetadata is missing
/// (orphan recovery).
pub async fn find_by_package(package_name: &str) -> Result<Vec<String>, Error> {
Self::find_bins_where(|config| config.package == package_name).await
}

/// Scan `~/.vite-plus/bins/` and return names of binaries matching a predicate.
async fn find_bins_where(predicate: impl Fn(&BinConfig) -> bool) -> Result<Vec<String>, Error> {
let bins_dir = Self::bins_dir()?;
if !tokio::fs::try_exists(&bins_dir).await.unwrap_or(false) {
return Ok(Vec::new());
Expand All @@ -155,7 +169,7 @@ impl BinConfig {
if path.extension().is_some_and(|e| e == "json") {
if let Ok(content) = tokio::fs::read_to_string(&path).await {
if let Ok(config) = serde_json::from_str::<BinConfig>(&content) {
if config.package == package_name {
if predicate(&config) {
bins.push(config.name);
}
}
Expand Down Expand Up @@ -346,4 +360,53 @@ mod tests {
// Delete again should not error
BinConfig::delete_sync("codex").unwrap();
}

#[tokio::test]
async fn test_find_all_vp_source() {
let temp_dir = TempDir::new().unwrap();
let _guard = vite_shared::EnvConfig::test_guard(
vite_shared::EnvConfig::for_test_with_home(temp_dir.path()),
);

// Create Vp-source configs
let tsc = BinConfig::new(
"tsc".to_string(),
"typescript".to_string(),
"5.0.0".to_string(),
"20.18.0".to_string(),
);
tsc.save().await.unwrap();

let corepack = BinConfig::new(
"corepack".to_string(),
"corepack".to_string(),
"0.20.0".to_string(),
"20.18.0".to_string(),
);
corepack.save().await.unwrap();

// Create Npm-source config (should be excluded)
let codex = BinConfig::new_npm(
"codex".to_string(),
"@openai/codex".to_string(),
"22.22.0".to_string(),
);
codex.save().await.unwrap();

let mut vp_bins = BinConfig::find_all_vp_source().await.unwrap();
vp_bins.sort();
assert_eq!(vp_bins.len(), 2);
assert_eq!(vp_bins, vec!["corepack", "tsc"]);
}

#[tokio::test]
async fn test_find_all_vp_source_empty_bins_dir() {
let temp_dir = TempDir::new().unwrap();
let _guard = vite_shared::EnvConfig::test_guard(
vite_shared::EnvConfig::for_test_with_home(temp_dir.path()),
);

let vp_bins = BinConfig::find_all_vp_source().await.unwrap();
assert!(vp_bins.is_empty());
}
}
3 changes: 1 addition & 2 deletions crates/vite_global_cli/src/commands/env/doctor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,7 @@ const KNOWN_VERSION_MANAGERS: &[(&str, &str)] = &[
("n", "N_PREFIX"),
];

/// Tools that should have shims
const SHIM_TOOLS: &[&str] = &["node", "npm", "npx", "vpx", "vpr"];
use super::setup::SHIM_TOOLS;

/// Column width for left-side keys in aligned output
const KEY_WIDTH: usize = 18;
Expand Down
6 changes: 2 additions & 4 deletions crates/vite_global_cli/src/commands/env/global_install.rs
Original file line number Diff line number Diff line change
Expand Up @@ -408,10 +408,8 @@ async fn create_package_shim(
{
let shim_path = bin_dir.join(format!("{}.exe", bin_name));

// Skip if already exists (e.g., re-installing the same package)
if tokio::fs::try_exists(&shim_path).await.unwrap_or(false) {
return Ok(());
}
// Delete before overwrite; falls back to rename if the exe is locked.
super::setup::remove_or_rename_to_old(&shim_path).await;

// Copy the trampoline binary as <bin_name>.exe.
// The trampoline detects the tool name from its own filename and sets
Expand Down
70 changes: 65 additions & 5 deletions crates/vite_global_cli/src/commands/env/setup.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ use super::config::{get_bin_dir, get_vp_home};
use crate::{error::Error, help};

/// Tools to create shims for (node, npm, npx, vpx, vpr)
const SHIM_TOOLS: &[&str] = &["node", "npm", "npx", "vpx", "vpr"];
pub(crate) const SHIM_TOOLS: &[&str] = &["node", "npm", "npx", "vpx", "vpr"];

fn accent_command(command: &str) -> String {
if help::should_style_help() {
Expand Down Expand Up @@ -82,6 +82,13 @@ pub async fn execute(refresh: bool, env_only: bool) -> Result<ExitStatus, Error>
}
}

#[cfg(windows)]
if refresh {
if let Err(e) = refresh_package_shims(&bin_dir).await {
tracing::warn!("Failed to refresh package shims: {}", e);
}
}

// Best-effort cleanup of .old files from rename-before-copy on Windows
#[cfg(windows)]
if refresh {
Expand Down Expand Up @@ -192,11 +199,8 @@ async fn create_shim(
if !refresh {
return Ok(false);
}
// Remove existing shim for refresh.
// On Windows, .exe files may be locked (by antivirus, indexer, or
// still-running processes), so rename to .old first instead of deleting.
#[cfg(windows)]
rename_to_old(&shim_path).await;
remove_or_rename_to_old(&shim_path).await;
#[cfg(not(windows))]
{
tokio::fs::remove_file(&shim_path).await?;
Expand Down Expand Up @@ -273,6 +277,46 @@ async fn create_windows_shim(
Ok(())
}

/// Refresh trampoline `.exe` files for package shims installed via `vp install -g`.
///
/// Discovers all package binaries tracked by BinConfig with `source: Vp`
/// and replaces their `.exe` with the current trampoline.
#[cfg(windows)]
async fn refresh_package_shims(bin_dir: &vite_path::AbsolutePath) -> Result<(), Error> {
use super::bin_config::BinConfig;

let package_bins = BinConfig::find_all_vp_source().await?;

if package_bins.is_empty() {
return Ok(());
}

let trampoline_src = get_trampoline_path()?;

for bin_name in &package_bins {
// Core shims (SHIM_TOOLS + vp) are already refreshed by the main loop.
if bin_name == "vp" || SHIM_TOOLS.contains(&bin_name.as_str()) {
continue;
}

let shim_path = bin_dir.join(format!("{bin_name}.exe"));

remove_or_rename_to_old(&shim_path).await;

if let Err(e) = tokio::fs::copy(trampoline_src.as_path(), &shim_path).await {
tracing::warn!("Failed to refresh package shim {}: {}", bin_name, e);
continue;
}

// Remove legacy .cmd/shell wrappers that could shadow the .exe in Git Bash.
cleanup_legacy_windows_shim(bin_dir, bin_name).await;

tracing::debug!("Refreshed package trampoline shim {:?}", shim_path);
}

Ok(())
}

/// Get the path to the trampoline template binary (vp-shim.exe).
///
/// The trampoline binary is distributed alongside vp.exe in the same directory.
Expand Down Expand Up @@ -309,6 +353,22 @@ pub(crate) fn get_trampoline_path() -> Result<vite_path::AbsolutePathBuf, Error>
.ok_or_else(|| Error::ConfigError("Invalid trampoline path".into()))
}

/// Try to delete an `.exe` file; if deletion fails (e.g., file is locked by a
/// running process), fall back to renaming it to `.old`.
///
/// This avoids accumulating `.old` files when the exe is not in use.
#[cfg(windows)]
pub(crate) async fn remove_or_rename_to_old(path: &vite_path::AbsolutePath) {
Comment thread
fengmk2 marked this conversation as resolved.
match tokio::fs::remove_file(path).await {
Ok(()) => return,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return,
Err(e) => {
tracing::debug!("remove_file failed ({}), attempting rename", e);
}
}
rename_to_old(path).await;
}

/// Rename an existing `.exe` to a timestamped `.old` file instead of deleting.
///
/// On Windows, running `.exe` files can't be deleted or overwritten, but they can
Expand Down
11 changes: 11 additions & 0 deletions rfcs/trampoline-exe-for-shims.md
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,17 @@ When `vp env setup --refresh` is invoked through the trampoline (`~/.vite-plus/b
2. Copy new trampoline to `vp.exe`
3. Best-effort cleanup of all `*.old` files in the bin directory

### Upgrade Refresh

During `vp upgrade`, after the `current` link is swapped to the new version, `vp env setup --refresh` is invoked to regenerate all trampoline `.exe` files. This ensures that when the trampoline binary (`vp-shim.exe`) changes between versions, all shims pick up the new version:

1. **Core shims** (`vp.exe`, `node.exe`, `npm.exe`, `npx.exe`, `vpx.exe`, `vpr.exe`) are refreshed by the standard `--refresh` logic.
2. **Package shims** (e.g., `corepack.exe`, `tsc.exe`, installed via `vp install -g`) are discovered by scanning `~/.vite-plus/bins/` for `BinConfig` entries with `source: Vp`, and each `.exe` is replaced with the new trampoline.

Package shims installed via npm interception (`source: Npm`) use `.cmd` wrappers, not trampoline `.exe` files, and are not affected by this refresh.

Additionally, re-installing a global package (`vp install -g <pkg>`) always re-copies the current trampoline, ensuring the shim stays up to date even without a full upgrade.

### Distribution

The trampoline binary (`vp-shim.exe`) is distributed alongside `vp.exe`:
Expand Down
2 changes: 1 addition & 1 deletion rfcs/upgrade-command.md
Original file line number Diff line number Diff line change
Expand Up @@ -302,7 +302,7 @@ Key differences on Windows:

After the symlink swap (the **point of no return**), post-update operations are treated as non-fatal. Errors are printed to stderr as warnings but do not trigger the outer error handler (which would delete the now-active version directory).

1. **Refresh shims**: Run the equivalent of `vp env setup --refresh` to ensure node/npm/npx shims point to the new version. If this fails, the user can run it manually.
1. **Refresh shims**: Run the equivalent of `vp env setup --refresh` to ensure node/npm/npx shims point to the new version. This also refreshes trampoline `.exe` files for globally installed package shims (e.g., `corepack.exe`, `tsc.exe`) by scanning `BinConfig` entries. If this fails, the user can run it manually.
2. **Cleanup old versions**: Remove old version directories, keeping the 5 most recent by **creation time** (matching `install.sh` behavior). The new version and the previous version are always protected from cleanup, even if they fall outside the top 5 (e.g., after a downgrade via `--rollback`).

#### Step 7: Running Binary Consideration
Expand Down
Loading