@@ -79,6 +79,12 @@ pub async fn execute(refresh: bool, env_only: bool) -> Result<ExitStatus, Error>
7979 }
8080 }
8181
82+ // Best-effort cleanup of .old files from rename-before-copy on Windows
83+ #[ cfg( windows) ]
84+ if refresh {
85+ cleanup_old_files ( & bin_dir) . await ;
86+ }
87+
8288 // Print results
8389 if !created. is_empty ( ) {
8490 println ! ( "{}" , help:: render_heading( "Created Shims" ) ) ;
@@ -142,15 +148,7 @@ async fn setup_vp_wrapper(bin_dir: &vite_path::AbsolutePath, refresh: bool) -> R
142148 // that launched us). Windows prevents overwriting a running exe, so we
143149 // rename it to a timestamped .old file first, then copy the new one.
144150 if tokio:: fs:: try_exists ( & bin_vp_exe) . await . unwrap_or ( false ) {
145- let timestamp = std:: time:: SystemTime :: now ( )
146- . duration_since ( std:: time:: UNIX_EPOCH )
147- . unwrap_or_default ( )
148- . as_secs ( ) ;
149- let old_name = format ! ( "vp.exe.{timestamp}.old" ) ;
150- let old_path = bin_dir. join ( & old_name) ;
151- if let Err ( e) = tokio:: fs:: rename ( & bin_vp_exe, & old_path) . await {
152- tracing:: warn!( "Failed to rename running vp.exe to {}: {}" , old_name, e) ;
153- }
151+ rename_to_old ( & bin_vp_exe) . await ;
154152 }
155153
156154 tokio:: fs:: copy ( trampoline_src. as_path ( ) , & bin_vp_exe) . await ?;
@@ -194,8 +192,15 @@ async fn create_shim(
194192 if !refresh {
195193 return Ok ( false ) ;
196194 }
197- // Remove existing shim for refresh
198- tokio:: fs:: remove_file ( & shim_path) . await ?;
195+ // Remove existing shim for refresh.
196+ // On Windows, .exe files may be locked (by antivirus, indexer, or
197+ // still-running processes), so rename to .old first instead of deleting.
198+ #[ cfg( windows) ]
199+ rename_to_old ( & shim_path) . await ;
200+ #[ cfg( not( windows) ) ]
201+ {
202+ tokio:: fs:: remove_file ( & shim_path) . await ?;
203+ }
199204 }
200205
201206 #[ cfg( unix) ]
@@ -304,6 +309,25 @@ pub(crate) fn get_trampoline_path() -> Result<vite_path::AbsolutePathBuf, Error>
304309 . ok_or_else ( || Error :: ConfigError ( "Invalid trampoline path" . into ( ) ) )
305310}
306311
312+ /// Rename an existing `.exe` to a timestamped `.old` file instead of deleting.
313+ ///
314+ /// On Windows, running `.exe` files can't be deleted or overwritten, but they can
315+ /// be renamed. The `.old` files are cleaned up by `cleanup_old_files()`.
316+ #[ cfg( windows) ]
317+ async fn rename_to_old ( path : & vite_path:: AbsolutePath ) {
318+ let timestamp = std:: time:: SystemTime :: now ( )
319+ . duration_since ( std:: time:: UNIX_EPOCH )
320+ . unwrap_or_default ( )
321+ . as_secs ( ) ;
322+ if let Some ( name) = path. as_path ( ) . file_name ( ) . and_then ( |n| n. to_str ( ) ) {
323+ let old_name = format ! ( "{name}.{timestamp}.old" ) ;
324+ let old_path = path. as_path ( ) . with_file_name ( & old_name) ;
325+ if let Err ( e) = tokio:: fs:: rename ( path, & old_path) . await {
326+ tracing:: warn!( "Failed to rename {} to {}: {}" , name, old_name, e) ;
327+ }
328+ }
329+ }
330+
307331/// Best-effort cleanup of accumulated `.old` files from previous rename-before-copy operations.
308332///
309333/// When refreshing `bin/vp.exe` on Windows, the running trampoline is renamed to a
0 commit comments