@@ -7,6 +7,9 @@ use std::{
77 process:: { ExitStatus , Stdio } ,
88} ;
99
10+ use fs4:: fs_std:: FileExt ;
11+ #[ cfg( not( target_os = "windows" ) ) ]
12+ use nix:: sys:: statfs:: statfs;
1013use semver:: { Version , VersionReq } ;
1114use serde:: { Deserialize , Serialize } ;
1215use tokio:: { fs:: remove_dir_all, process:: Command } ;
@@ -320,6 +323,65 @@ async fn get_latest_version(package_manager_type: PackageManagerType) -> Result<
320323 Ok ( package_json. version )
321324}
322325
326+ /// Check if a path is on an NFS filesystem.
327+ /// NFS file locking can be unreliable, so we skip locking on NFS.
328+ #[ cfg( not( target_os = "windows" ) ) ]
329+ fn is_on_nfs ( path : & Path ) -> bool {
330+ match statfs ( path) {
331+ Ok ( stat) => {
332+ // NFS magic numbers:
333+ // - Linux: 0x6969 (NFS_SUPER_MAGIC)
334+ // - macOS/BSD: Different approach - check f_fstypename
335+ #[ cfg( target_os = "linux" ) ]
336+ {
337+ stat. filesystem_type ( ) . 0 == nix:: sys:: statfs:: NFS_SUPER_MAGIC . 0
338+ }
339+ #[ cfg( not( target_os = "linux" ) ) ]
340+ {
341+ // On macOS and BSD, check the filesystem type name
342+ let fs_type = stat. filesystem_type_name ( ) ;
343+ fs_type == "nfs"
344+ }
345+ }
346+ Err ( _) => {
347+ // If we can't determine, assume it's not NFS and try to lock
348+ false
349+ }
350+ }
351+ }
352+
353+ #[ cfg( target_os = "windows" ) ]
354+ fn is_on_nfs ( _path : & Path ) -> bool {
355+ // On Windows, we could check for network drives, but for simplicity
356+ // we'll assume local filesystem. Windows file locking is generally reliable.
357+ false
358+ }
359+
360+ /// A file lock guard that holds the lock until dropped.
361+ /// If the filesystem is NFS, no actual lock is acquired.
362+ struct FileLockGuard {
363+ _file : Option < File > ,
364+ }
365+
366+ /// Acquire a file lock at the specified path.
367+ /// Automatically detects NFS filesystems and skips locking if on NFS.
368+ /// Returns a guard that releases the lock when dropped.
369+ fn acquire_file_lock ( lock_path : & Path ) -> Result < FileLockGuard , Error > {
370+ // Check if the path is on NFS
371+ if is_on_nfs ( lock_path) {
372+ tracing:: debug!( "Path {:?} is on NFS filesystem, skipping file lock" , lock_path) ;
373+ return Ok ( FileLockGuard { _file : None } ) ;
374+ }
375+
376+ tracing:: debug!( "Acquiring lock file: {:?}" , lock_path) ;
377+ let lock_file = File :: create ( lock_path) ?;
378+ // Acquire exclusive lock (blocks until available)
379+ lock_file. lock_exclusive ( ) ?;
380+ tracing:: debug!( "Lock acquired: {:?}" , lock_path) ;
381+
382+ Ok ( FileLockGuard { _file : Some ( lock_file) } )
383+ }
384+
323385/// Download the package manager and extract it to the cache directory.
324386/// Return the install directory, e.g. $`CACHE_DIR/vite/package_manager/pnpm/10.0.0/pnpm`
325387async fn download_package_manager (
@@ -374,9 +436,17 @@ async fn download_package_manager(
374436 tracing:: debug!( "Rename package dir to {}" , bin_name) ;
375437 tokio:: fs:: rename ( & target_dir_tmp. join ( "package" ) , & target_dir_tmp. join ( & bin_name) ) . await ?;
376438
377- // check bin_file again, for the concurrent download cases
439+ // Use a file-based lock to ensure atomicity of remove + rename operations
440+ // This prevents DirectoryNotEmpty exceptions when multiple processes/threads
441+ // try to install the same package manager version concurrently.
442+ // The lock is automatically skipped on NFS filesystems where locking is unreliable.
443+ let lock_path = parent_dir. join ( format ! ( "{version}.lock" ) ) ;
444+ let _lock_guard = acquire_file_lock ( lock_path. as_ref ( ) ) ?;
445+
446+ // Check again after acquiring the lock, in case another thread completed
447+ // the installation while we were downloading
378448 if is_exists_file ( & bin_file) ? {
379- tracing:: debug!( "bin_file already exists, skip rename" ) ;
449+ tracing:: debug!( "bin_file already exists after lock acquisition , skip rename" ) ;
380450 return Ok ( install_dir) ;
381451 }
382452
@@ -395,12 +465,13 @@ async fn download_package_manager(
395465/// Remove the directory and all its contents.
396466/// Ignore the error if the directory is not found.
397467async fn remove_dir_all_force ( path : impl AsRef < Path > ) -> Result < ( ) , std:: io:: Error > {
398- match remove_dir_all ( path) . await {
468+ match remove_dir_all ( path. as_ref ( ) ) . await {
399469 Ok ( ( ) ) => Ok ( ( ) ) ,
400470 Err ( e) => {
401471 if e. kind ( ) == std:: io:: ErrorKind :: NotFound {
402472 Ok ( ( ) )
403473 } else {
474+ tracing:: error!( "remove_dir_all_force path: {:?} error: {e:?}" , path. as_ref( ) ) ;
404475 Err ( e)
405476 }
406477 }
0 commit comments