Skip to content

Commit 2d37595

Browse files
committed
fix(env): regenerate all trampoline .exe files after upgrade
During `vp upgrade`, only core shims (vp, node, npm, npx, vpx, vpr) were refreshed. Package shims installed via `vp install -g` (e.g., corepack.exe, tsc.exe) kept stale trampoline binaries from the previous version. - Add `BinConfig::find_all_vp_source()` to discover package shims - Add `refresh_package_shims()` to `vp env setup --refresh` (Windows) - Fix `create_package_shim()` to re-copy trampoline on re-install instead of returning early when .exe exists - Make `rename_to_old` pub(crate) for reuse in global_install Closes #1349
1 parent eb7370a commit 2d37595

File tree

6 files changed

+142
-7
lines changed

6 files changed

+142
-7
lines changed

crates/vite_global_cli/src/commands/env/bin_config.rs

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,11 +137,25 @@ impl BinConfig {
137137
Ok(())
138138
}
139139

140+
/// Find all binaries with `Vp` source (installed via `vp install -g`).
141+
///
142+
/// Used during shim refresh to discover package shims that need their
143+
/// trampoline executables updated after a vite-plus upgrade.
144+
#[cfg_attr(not(windows), allow(dead_code))] // Only called from #[cfg(windows)] refresh_package_shims
145+
pub async fn find_all_vp_source() -> Result<Vec<String>, Error> {
146+
Self::find_bins_where(|config| config.source == BinSource::Vp).await
147+
}
148+
140149
/// Find all binaries installed by a package.
141150
///
142151
/// This is used as a fallback during uninstall when PackageMetadata is missing
143152
/// (orphan recovery).
144153
pub async fn find_by_package(package_name: &str) -> Result<Vec<String>, Error> {
154+
Self::find_bins_where(|config| config.package == package_name).await
155+
}
156+
157+
/// Scan `~/.vite-plus/bins/` and return names of binaries matching a predicate.
158+
async fn find_bins_where(predicate: impl Fn(&BinConfig) -> bool) -> Result<Vec<String>, Error> {
145159
let bins_dir = Self::bins_dir()?;
146160
if !tokio::fs::try_exists(&bins_dir).await.unwrap_or(false) {
147161
return Ok(Vec::new());
@@ -155,7 +169,7 @@ impl BinConfig {
155169
if path.extension().is_some_and(|e| e == "json") {
156170
if let Ok(content) = tokio::fs::read_to_string(&path).await {
157171
if let Ok(config) = serde_json::from_str::<BinConfig>(&content) {
158-
if config.package == package_name {
172+
if predicate(&config) {
159173
bins.push(config.name);
160174
}
161175
}
@@ -346,4 +360,53 @@ mod tests {
346360
// Delete again should not error
347361
BinConfig::delete_sync("codex").unwrap();
348362
}
363+
364+
#[tokio::test]
365+
async fn test_find_all_vp_source() {
366+
let temp_dir = TempDir::new().unwrap();
367+
let _guard = vite_shared::EnvConfig::test_guard(
368+
vite_shared::EnvConfig::for_test_with_home(temp_dir.path()),
369+
);
370+
371+
// Create Vp-source configs
372+
let tsc = BinConfig::new(
373+
"tsc".to_string(),
374+
"typescript".to_string(),
375+
"5.0.0".to_string(),
376+
"20.18.0".to_string(),
377+
);
378+
tsc.save().await.unwrap();
379+
380+
let corepack = BinConfig::new(
381+
"corepack".to_string(),
382+
"corepack".to_string(),
383+
"0.20.0".to_string(),
384+
"20.18.0".to_string(),
385+
);
386+
corepack.save().await.unwrap();
387+
388+
// Create Npm-source config (should be excluded)
389+
let codex = BinConfig::new_npm(
390+
"codex".to_string(),
391+
"@openai/codex".to_string(),
392+
"22.22.0".to_string(),
393+
);
394+
codex.save().await.unwrap();
395+
396+
let mut vp_bins = BinConfig::find_all_vp_source().await.unwrap();
397+
vp_bins.sort();
398+
assert_eq!(vp_bins.len(), 2);
399+
assert_eq!(vp_bins, vec!["corepack", "tsc"]);
400+
}
401+
402+
#[tokio::test]
403+
async fn test_find_all_vp_source_empty_bins_dir() {
404+
let temp_dir = TempDir::new().unwrap();
405+
let _guard = vite_shared::EnvConfig::test_guard(
406+
vite_shared::EnvConfig::for_test_with_home(temp_dir.path()),
407+
);
408+
409+
let vp_bins = BinConfig::find_all_vp_source().await.unwrap();
410+
assert!(vp_bins.is_empty());
411+
}
349412
}

crates/vite_global_cli/src/commands/env/doctor.rs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,8 +62,7 @@ const KNOWN_VERSION_MANAGERS: &[(&str, &str)] = &[
6262
("n", "N_PREFIX"),
6363
];
6464

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

6867
/// Column width for left-side keys in aligned output
6968
const KEY_WIDTH: usize = 18;

crates/vite_global_cli/src/commands/env/global_install.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -408,9 +408,9 @@ async fn create_package_shim(
408408
{
409409
let shim_path = bin_dir.join(format!("{}.exe", bin_name));
410410

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

416416
// Copy the trampoline binary as <bin_name>.exe.

crates/vite_global_cli/src/commands/env/setup.rs

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ use super::config::{get_bin_dir, get_vp_home};
2323
use crate::{error::Error, help};
2424

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

2828
fn accent_command(command: &str) -> String {
2929
if help::should_style_help() {
@@ -82,6 +82,14 @@ pub async fn execute(refresh: bool, env_only: bool) -> Result<ExitStatus, Error>
8282
}
8383
}
8484

85+
// Refresh package shims (trampoline .exe files from vp install -g)
86+
#[cfg(windows)]
87+
if refresh {
88+
if let Err(e) = refresh_package_shims(&bin_dir).await {
89+
tracing::warn!("Failed to refresh package shims: {}", e);
90+
}
91+
}
92+
8593
// Best-effort cleanup of .old files from rename-before-copy on Windows
8694
#[cfg(windows)]
8795
if refresh {
@@ -273,6 +281,48 @@ async fn create_windows_shim(
273281
Ok(())
274282
}
275283

284+
/// Refresh trampoline `.exe` files for package shims installed via `vp install -g`.
285+
///
286+
/// Discovers all package binaries tracked by BinConfig with `source: Vp`
287+
/// and replaces their `.exe` with the current trampoline.
288+
#[cfg(windows)]
289+
async fn refresh_package_shims(bin_dir: &vite_path::AbsolutePath) -> Result<(), Error> {
290+
use super::bin_config::BinConfig;
291+
292+
let package_bins = BinConfig::find_all_vp_source().await?;
293+
294+
if package_bins.is_empty() {
295+
return Ok(());
296+
}
297+
298+
let trampoline_src = get_trampoline_path()?;
299+
300+
for bin_name in &package_bins {
301+
// Core shims (SHIM_TOOLS + vp) are already refreshed by the main loop.
302+
if bin_name == "vp" || SHIM_TOOLS.contains(&bin_name.as_str()) {
303+
continue;
304+
}
305+
306+
let shim_path = bin_dir.join(format!("{bin_name}.exe"));
307+
308+
if tokio::fs::try_exists(&shim_path).await.unwrap_or(false) {
309+
remove_or_rename_to_old(&shim_path).await;
310+
}
311+
312+
if let Err(e) = tokio::fs::copy(trampoline_src.as_path(), &shim_path).await {
313+
tracing::warn!("Failed to refresh package shim {}: {}", bin_name, e);
314+
continue;
315+
}
316+
317+
// Remove legacy .cmd/shell wrappers that could shadow the .exe in Git Bash.
318+
cleanup_legacy_windows_shim(bin_dir, bin_name).await;
319+
320+
tracing::debug!("Refreshed package trampoline shim {:?}", shim_path);
321+
}
322+
323+
Ok(())
324+
}
325+
276326
/// Get the path to the trampoline template binary (vp-shim.exe).
277327
///
278328
/// The trampoline binary is distributed alongside vp.exe in the same directory.
@@ -309,6 +359,18 @@ pub(crate) fn get_trampoline_path() -> Result<vite_path::AbsolutePathBuf, Error>
309359
.ok_or_else(|| Error::ConfigError("Invalid trampoline path".into()))
310360
}
311361

362+
/// Try to delete an `.exe` file; if deletion fails (e.g., file is locked by a
363+
/// running process), fall back to renaming it to `.old`.
364+
///
365+
/// This avoids accumulating `.old` files when the exe is not in use.
366+
#[cfg(windows)]
367+
pub(crate) async fn remove_or_rename_to_old(path: &vite_path::AbsolutePath) {
368+
if tokio::fs::remove_file(path).await.is_ok() {
369+
return;
370+
}
371+
rename_to_old(path).await;
372+
}
373+
312374
/// Rename an existing `.exe` to a timestamped `.old` file instead of deleting.
313375
///
314376
/// On Windows, running `.exe` files can't be deleted or overwritten, but they can

rfcs/trampoline-exe-for-shims.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,17 @@ When `vp env setup --refresh` is invoked through the trampoline (`~/.vite-plus/b
207207
2. Copy new trampoline to `vp.exe`
208208
3. Best-effort cleanup of all `*.old` files in the bin directory
209209

210+
### Upgrade Refresh
211+
212+
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:
213+
214+
1. **Core shims** (`vp.exe`, `node.exe`, `npm.exe`, `npx.exe`, `vpx.exe`, `vpr.exe`) are refreshed by the standard `--refresh` logic.
215+
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.
216+
217+
Package shims installed via npm interception (`source: Npm`) use `.cmd` wrappers, not trampoline `.exe` files, and are not affected by this refresh.
218+
219+
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.
220+
210221
### Distribution
211222

212223
The trampoline binary (`vp-shim.exe`) is distributed alongside `vp.exe`:

rfcs/upgrade-command.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -302,7 +302,7 @@ Key differences on Windows:
302302

303303
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).
304304

305-
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.
305+
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.
306306
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`).
307307

308308
#### Step 7: Running Binary Consideration

0 commit comments

Comments
 (0)