1use std::fs::TryLockError;
11use std::fs::{File, OpenOptions};
12use std::io;
13use std::io::{Read, Seek, SeekFrom, Write};
14use std::path::{Display, Path, PathBuf};
15
16use crate::util::GlobalContext;
17use crate::util::errors::CargoResult;
18use crate::util::style;
19use anyhow::Context as _;
20use cargo_util::paths;
21
22pub(crate) use self::imp::lock_exclusive;
23pub(crate) use self::imp::lock_shared;
24#[expect(unused_imports, reason = "for non-blocking lock callers")]
25pub(crate) use self::imp::try_lock_exclusive;
26#[expect(unused_imports, reason = "for non-blocking lock callers")]
27pub(crate) use self::imp::try_lock_shared;
28pub(crate) use self::imp::unlock;
29
30#[derive(Debug)]
43pub struct FileLock {
44 f: Option<File>,
45 path: PathBuf,
46}
47
48impl FileLock {
49 pub fn file(&self) -> &File {
51 self.f.as_ref().unwrap()
52 }
53
54 pub fn path(&self) -> &Path {
59 &self.path
60 }
61
62 pub fn parent(&self) -> &Path {
64 self.path.parent().unwrap()
65 }
66
67 pub fn remove_siblings(&self) -> CargoResult<()> {
72 let path = self.path();
73 for entry in path.parent().unwrap().read_dir()? {
74 let entry = entry?;
75 if Some(&entry.file_name()[..]) == path.file_name() {
76 continue;
77 }
78 let kind = entry.file_type()?;
79 if kind.is_dir() {
80 paths::remove_dir_all(entry.path())?;
81 } else {
82 paths::remove_file(entry.path())?;
83 }
84 }
85 Ok(())
86 }
87
88 pub fn rename<P: AsRef<Path>>(&mut self, new_path: P) -> CargoResult<()> {
99 let new_path = new_path.as_ref();
100 std::fs::rename(&self.path, new_path).with_context(|| {
101 format!(
102 "failed to rename {} to {}",
103 self.path.display(),
104 new_path.display()
105 )
106 })?;
107 self.path = new_path.to_path_buf();
108 Ok(())
109 }
110}
111
112impl Read for FileLock {
113 fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
114 self.file().read(buf)
115 }
116}
117
118impl Seek for FileLock {
119 fn seek(&mut self, to: SeekFrom) -> io::Result<u64> {
120 self.file().seek(to)
121 }
122}
123
124impl Write for FileLock {
125 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
126 self.file().write(buf)
127 }
128
129 fn flush(&mut self) -> io::Result<()> {
130 self.file().flush()
131 }
132}
133
134impl Drop for FileLock {
135 fn drop(&mut self) {
136 if let Some(f) = self.f.take() {
137 if let Err(e) = imp::unlock(&f) {
138 tracing::warn!("failed to release lock: {e:?}");
139 }
140 }
141 }
142}
143
144#[derive(Clone, Debug, PartialEq, Eq)]
177pub struct Filesystem {
178 root: PathBuf,
179}
180
181impl Filesystem {
182 pub fn new(path: PathBuf) -> Filesystem {
184 Filesystem { root: path }
185 }
186
187 pub fn join<T: AsRef<Path>>(&self, other: T) -> Filesystem {
190 Filesystem::new(self.root.join(other))
191 }
192
193 pub fn push<T: AsRef<Path>>(&mut self, other: T) {
195 self.root.push(other);
196 }
197
198 pub fn into_path_unlocked(self) -> PathBuf {
203 self.root
204 }
205
206 pub fn as_path_unlocked(&self) -> &Path {
211 &self.root
212 }
213
214 pub fn create_dir(&self) -> CargoResult<()> {
219 paths::create_dir_all(&self.root)
220 }
221
222 pub fn display(&self) -> Display<'_> {
225 self.root.display()
226 }
227
228 pub fn open_rw_exclusive_create<P>(
239 &self,
240 path: P,
241 gctx: &GlobalContext,
242 msg: &str,
243 ) -> CargoResult<FileLock>
244 where
245 P: AsRef<Path>,
246 {
247 let mut opts = OpenOptions::new();
248 opts.read(true).write(true).create(true);
249 let (path, f) = self.open(path.as_ref(), &opts, true)?;
250 acquire(gctx, msg, &path, &|| imp::try_lock_exclusive(&f), &|| {
251 imp::lock_exclusive(&f)
252 })?;
253 Ok(FileLock { f: Some(f), path })
254 }
255
256 pub fn try_open_rw_exclusive_create<P: AsRef<Path>>(
261 &self,
262 path: P,
263 ) -> CargoResult<Option<FileLock>> {
264 let mut opts = OpenOptions::new();
265 opts.read(true).write(true).create(true);
266 let (path, f) = self.open(path.as_ref(), &opts, true)?;
267 if try_acquire(&path, &|| imp::try_lock_exclusive(&f))? {
268 Ok(Some(FileLock { f: Some(f), path }))
269 } else {
270 Ok(None)
271 }
272 }
273
274 pub fn open_ro_shared<P>(
284 &self,
285 path: P,
286 gctx: &GlobalContext,
287 msg: &str,
288 ) -> CargoResult<FileLock>
289 where
290 P: AsRef<Path>,
291 {
292 let (path, f) = self.open(path.as_ref(), &OpenOptions::new().read(true), false)?;
293 acquire(gctx, msg, &path, &|| imp::try_lock_shared(&f), &|| {
294 imp::lock_shared(&f)
295 })?;
296 Ok(FileLock { f: Some(f), path })
297 }
298
299 pub fn open_ro_shared_create<P: AsRef<Path>>(
305 &self,
306 path: P,
307 gctx: &GlobalContext,
308 msg: &str,
309 ) -> CargoResult<FileLock> {
310 let mut opts = OpenOptions::new();
311 opts.read(true).write(true).create(true);
312 let (path, f) = self.open(path.as_ref(), &opts, true)?;
313 acquire(gctx, msg, &path, &|| imp::try_lock_shared(&f), &|| {
314 imp::lock_shared(&f)
315 })?;
316 Ok(FileLock { f: Some(f), path })
317 }
318
319 pub fn try_open_ro_shared_create<P: AsRef<Path>>(
324 &self,
325 path: P,
326 ) -> CargoResult<Option<FileLock>> {
327 let mut opts = OpenOptions::new();
328 opts.read(true).write(true).create(true);
329 let (path, f) = self.open(path.as_ref(), &opts, true)?;
330 if try_acquire(&path, &|| imp::try_lock_shared(&f))? {
331 Ok(Some(FileLock { f: Some(f), path }))
332 } else {
333 Ok(None)
334 }
335 }
336
337 fn open(&self, path: &Path, opts: &OpenOptions, create: bool) -> CargoResult<(PathBuf, File)> {
338 let path = self.root.join(path);
339 let f = opts
340 .open(&path)
341 .or_else(|e| {
342 if e.kind() == io::ErrorKind::NotFound && create {
346 paths::create_dir_all(path.parent().unwrap())?;
347 Ok(opts.open(&path)?)
348 } else {
349 Err(anyhow::Error::from(e))
350 }
351 })
352 .with_context(|| format!("failed to open: {}", path.display()))?;
353 Ok((path, f))
354 }
355}
356
357impl PartialEq<Path> for Filesystem {
358 fn eq(&self, other: &Path) -> bool {
359 self.root == other
360 }
361}
362
363impl PartialEq<Filesystem> for Path {
364 fn eq(&self, other: &Filesystem) -> bool {
365 self == other.root
366 }
367}
368
369fn try_acquire(path: &Path, lock_try: &dyn Fn() -> Result<(), TryLockError>) -> CargoResult<bool> {
370 if is_on_nfs_mount(path) {
381 tracing::debug!("{path:?} appears to be an NFS mount, not trying to lock");
382 return Ok(true);
383 }
384
385 match lock_try() {
386 Ok(()) => Ok(true),
387
388 Err(TryLockError::Error(e)) if error_unsupported(&e) => Ok(true),
392
393 Err(TryLockError::Error(e)) => {
394 let e = anyhow::Error::from(e);
395 let cx = format!("failed to lock file: {}", path.display());
396 Err(e.context(cx))
397 }
398
399 Err(TryLockError::WouldBlock) => Ok(false),
400 }
401}
402
403fn acquire(
419 gctx: &GlobalContext,
420 msg: &str,
421 path: &Path,
422 lock_try: &dyn Fn() -> Result<(), TryLockError>,
423 lock_block: &dyn Fn() -> io::Result<()>,
424) -> CargoResult<()> {
425 gctx.debug_assert_shell_not_borrowed();
428 if try_acquire(path, lock_try)? {
429 return Ok(());
430 }
431
432 let msg = if gctx.extra_verbose() {
433 format!("waiting for file lock on {} ({})", msg, path.display())
434 } else {
435 format!("waiting for file lock on {}", msg)
436 };
437
438 gctx.shell()
439 .status_with_color("Blocking", &msg, &style::NOTE)?;
440
441 lock_block().with_context(|| format!("failed to lock file: {}", path.display()))?;
442 Ok(())
443}
444
445#[cfg(all(target_os = "linux", not(target_env = "musl")))]
446pub fn is_on_nfs_mount(path: &Path) -> bool {
447 use std::ffi::CString;
448 use std::mem;
449 use std::os::unix::prelude::*;
450
451 let Ok(path) = CString::new(path.as_os_str().as_bytes()) else {
452 return false;
453 };
454
455 unsafe {
456 let mut buf: libc::statfs = mem::zeroed();
457 let r = libc::statfs(path.as_ptr(), &mut buf);
458
459 r == 0 && buf.f_type as u32 == libc::NFS_SUPER_MAGIC as u32
460 }
461}
462
463#[cfg(any(not(target_os = "linux"), target_env = "musl"))]
464pub fn is_on_nfs_mount(_path: &Path) -> bool {
465 false
466}
467
468#[cfg(unix)]
469fn error_unsupported(err: &std::io::Error) -> bool {
470 match err.raw_os_error() {
471 #[allow(unreachable_patterns)]
474 Some(libc::ENOTSUP | libc::EOPNOTSUPP) => true,
475 Some(libc::ENOSYS) => true,
476 _ => err.kind() == std::io::ErrorKind::Unsupported,
477 }
478}
479
480#[cfg(windows)]
481fn error_unsupported(err: &std::io::Error) -> bool {
482 use windows_sys::Win32::Foundation::ERROR_INVALID_FUNCTION;
483 match err.raw_os_error() {
484 Some(code) if code == ERROR_INVALID_FUNCTION as i32 => true,
485 _ => err.kind() == std::io::ErrorKind::Unsupported,
486 }
487}
488
489#[cfg(not(target_os = "solaris"))]
492#[expect(
493 clippy::disallowed_methods,
494 reason = "the OS doesn't need the fcntl shim"
495)]
496mod imp {
497 use super::*;
498
499 pub fn try_lock_exclusive(file: &File) -> Result<(), TryLockError> {
500 file.try_lock()
501 }
502
503 pub fn lock_exclusive(file: &File) -> io::Result<()> {
504 file.lock()
505 }
506
507 pub fn try_lock_shared(file: &File) -> Result<(), TryLockError> {
508 file.try_lock_shared()
509 }
510
511 pub fn lock_shared(file: &File) -> io::Result<()> {
512 file.lock_shared()
513 }
514
515 pub fn unlock(file: &File) -> io::Result<()> {
516 file.unlock()
517 }
518}
519
520#[cfg(target_os = "solaris")]
521mod imp {
522 use super::*;
523 use std::mem;
524 use std::os::unix::io::AsRawFd;
525
526 pub fn try_lock_exclusive(file: &File) -> Result<(), TryLockError> {
527 match fcntl_lock(file, libc::F_WRLCK, libc::F_SETLK) {
528 Ok(()) => Ok(()),
529 Err(e) if is_would_block(&e) => Err(TryLockError::WouldBlock),
530 Err(e) => Err(TryLockError::Error(e)),
531 }
532 }
533
534 pub fn lock_exclusive(file: &File) -> io::Result<()> {
535 fcntl_lock(file, libc::F_WRLCK, libc::F_SETLKW)
536 }
537
538 pub fn try_lock_shared(file: &File) -> Result<(), TryLockError> {
539 match fcntl_lock(file, libc::F_RDLCK, libc::F_SETLK) {
540 Ok(()) => Ok(()),
541 Err(e) if is_would_block(&e) => Err(TryLockError::WouldBlock),
542 Err(e) => Err(TryLockError::Error(e)),
543 }
544 }
545
546 pub fn lock_shared(file: &File) -> io::Result<()> {
547 fcntl_lock(file, libc::F_RDLCK, libc::F_SETLKW)
548 }
549
550 pub fn unlock(file: &File) -> io::Result<()> {
551 fcntl_lock_raw(file, libc::F_UNLCK, libc::F_SETLK)
552 }
553
554 fn fcntl_lock(file: &File, lock_type: libc::c_short, cmd: libc::c_int) -> io::Result<()> {
555 fcntl_lock_raw(file, lock_type, cmd)
556 }
557
558 fn fcntl_lock_raw(file: &File, lock_type: libc::c_short, cmd: libc::c_int) -> io::Result<()> {
559 let mut lock = flock_for_whole_file(lock_type);
560 loop {
561 let result = unsafe { libc::fcntl(file.as_raw_fd(), cmd, &mut lock) };
562 if result != -1 {
563 return Ok(());
564 }
565
566 let error = io::Error::last_os_error();
567 if cmd == libc::F_SETLKW && error.kind() == io::ErrorKind::Interrupted {
568 continue;
569 }
570 return Err(error);
571 }
572 }
573
574 fn flock_for_whole_file(lock_type: libc::c_short) -> libc::flock {
575 let mut lock = unsafe { mem::zeroed::<libc::flock>() };
576 lock.l_type = lock_type;
577 lock.l_whence = libc::SEEK_SET as libc::c_short;
578 lock.l_start = 0;
579 lock.l_len = 0;
580 lock
581 }
582
583 fn is_would_block(error: &io::Error) -> bool {
584 matches!(error.raw_os_error(), Some(libc::EACCES | libc::EAGAIN))
585 || error.kind() == io::ErrorKind::WouldBlock
586 }
587}