Skip to main content

uv_fs/
lib.rs

1use std::io::{self, Write};
2use std::path::{Path, PathBuf};
3use std::time::SystemTime;
4
5#[cfg(unix)]
6use std::os::unix::fs::MetadataExt;
7#[cfg(windows)]
8use std::os::windows::io::AsRawHandle;
9
10#[cfg(target_os = "linux")]
11use std::time::{Duration, UNIX_EPOCH};
12
13#[cfg(feature = "tokio")]
14use std::io::Read;
15
16#[cfg(feature = "tokio")]
17use encoding_rs_io::DecodeReaderBytes;
18#[cfg(target_os = "linux")]
19use rustix::fs::{AtFlags, CWD as RUSTIX_CWD, StatxFlags, statx};
20use tempfile::NamedTempFile;
21use tracing::{debug, warn};
22#[cfg(windows)]
23use windows::Win32::Foundation::HANDLE;
24#[cfg(windows)]
25use windows::Win32::Storage::FileSystem::{BY_HANDLE_FILE_INFORMATION, GetFileInformationByHandle};
26
27pub use crate::locked_file::*;
28pub use crate::path::*;
29pub use crate::read::ValidatedReader;
30pub use crate::space::{PhysicalSpaceError, physical_space, supports_fine_grained_accounting};
31
32pub mod cachedir;
33#[cfg(target_os = "macos")]
34mod hardlink_macos;
35pub mod link;
36mod locked_file;
37mod path;
38mod read;
39mod space;
40pub mod which;
41
42/// Return the number of hardlinks to a file.
43#[cfg(unix)]
44pub fn hardlink_count(path: &Path) -> io::Result<u64> {
45    Ok(fs_err::metadata(path)?.nlink())
46}
47
48/// Return the number of hardlinks to a file.
49#[cfg(windows)]
50#[expect(unsafe_code)]
51pub fn hardlink_count(path: &Path) -> io::Result<u64> {
52    let file = fs_err::File::open(path)?;
53    let mut information = BY_HANDLE_FILE_INFORMATION::default();
54    // SAFETY: The file handle remains open for the duration of the call, and `information`
55    // points to a valid, writable structure of the type expected by the Windows API.
56    unsafe { GetFileInformationByHandle(HANDLE(file.as_raw_handle()), &raw mut information) }?;
57    Ok(u64::from(information.nNumberOfLinks))
58}
59
60/// Return an error on platforms that cannot report hardlink counts.
61#[cfg(not(any(unix, windows)))]
62pub fn hardlink_count(_path: &Path) -> io::Result<u64> {
63    Err(io::Error::new(
64        io::ErrorKind::Unsupported,
65        "hardlink counts are not supported on this platform",
66    ))
67}
68
69/// Collect regular files whose only hardlink is their entry in this directory.
70///
71/// Ignores symlink entries and uses bulk metadata reads on macOS. Returns `None` when the fast path
72/// is unavailable, required attributes are missing, or subdirectories need a recursive walk.
73/// No candidates are returned unless the entire directory can use the fast path.
74///
75/// Callers deleting these files must prevent concurrent changes to the directory and hardlink
76/// counts throughout both the scan and deletion.
77pub fn files_with_one_hardlink(path: &Path) -> io::Result<Option<Vec<PathBuf>>> {
78    #[cfg(target_os = "macos")]
79    {
80        hardlink_macos::files_with_one_hardlink(path)
81    }
82    #[cfg(not(target_os = "macos"))]
83    {
84        let _ = path;
85        Ok(None)
86    }
87}
88
89/// Return a path's creation time, including on Linux targets where [`std::fs::Metadata::created`]
90/// does not expose the filesystem birth time.
91pub fn created_time(path: &Path, metadata: &std::fs::Metadata) -> io::Result<SystemTime> {
92    #[cfg(target_os = "linux")]
93    {
94        let _ = metadata;
95
96        let metadata = statx(
97            RUSTIX_CWD,
98            path,
99            AtFlags::empty(),
100            StatxFlags::BASIC_STATS | StatxFlags::BTIME,
101        )?;
102
103        if metadata.stx_mask & StatxFlags::BTIME.bits() == 0 {
104            return Err(io::Error::new(
105                io::ErrorKind::Unsupported,
106                "creation time is not available for the filesystem",
107            ));
108        }
109
110        let birth_time = metadata.stx_btime;
111        let seconds = Duration::from_secs(birth_time.tv_sec.unsigned_abs());
112        let created = if birth_time.tv_sec < 0 {
113            UNIX_EPOCH.checked_sub(seconds)
114        } else {
115            UNIX_EPOCH.checked_add(seconds)
116        };
117
118        created
119            .filter(|_| birth_time.tv_nsec < 1_000_000_000)
120            .and_then(|created| {
121                created.checked_add(Duration::from_nanos(u64::from(birth_time.tv_nsec)))
122            })
123            .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "invalid creation time"))
124    }
125
126    #[cfg(not(target_os = "linux"))]
127    {
128        let _ = path;
129        metadata.created()
130    }
131}
132
133/// Attempt to check if the two paths refer to the same file.
134///
135/// Returns `Some(true)` if the files are missing, but would be the same if they existed.
136pub fn is_same_file_allow_missing(left: &Path, right: &Path) -> Option<bool> {
137    // First, check an exact path comparison.
138    if left == right {
139        return Some(true);
140    }
141
142    // Second, check the files directly.
143    if let Ok(value) = same_file::is_same_file(left, right) {
144        return Some(value);
145    }
146
147    // Often, one of the directories won't exist yet so perform the comparison up a level.
148    if let (Some(left_parent), Some(right_parent), Some(left_name), Some(right_name)) = (
149        left.parent(),
150        right.parent(),
151        left.file_name(),
152        right.file_name(),
153    ) {
154        match same_file::is_same_file(left_parent, right_parent) {
155            Ok(true) => return Some(left_name == right_name),
156            Ok(false) => return Some(false),
157            _ => (),
158        }
159    }
160
161    // We couldn't determine if they're the same.
162    None
163}
164
165/// Reads data from the path and requires that it be valid UTF-8 or UTF-16.
166///
167/// This uses BOM sniffing to determine if the data should be transcoded from UTF-16 to Rust's
168/// `String` type (which uses UTF-8).
169///
170/// This should generally only be used when one specifically wants to support reading UTF-16
171/// transparently.
172///
173/// If the file path is `-`, then contents are read from stdin instead.
174#[cfg(feature = "tokio")]
175pub async fn read_to_string_transcode(path: impl AsRef<Path>) -> std::io::Result<String> {
176    let path = path.as_ref();
177    let raw = if path == Path::new("-") {
178        let mut buf = Vec::with_capacity(1024);
179        std::io::stdin().read_to_end(&mut buf)?;
180        buf
181    } else {
182        fs_err::tokio::read(path).await?
183    };
184    let mut buf = String::with_capacity(1024);
185    DecodeReaderBytes::new(&*raw)
186        .read_to_string(&mut buf)
187        .map_err(|err| {
188            let path = path.display();
189            std::io::Error::other(format!("failed to decode file {path}: {err}"))
190        })?;
191    Ok(buf)
192}
193
194/// Create a junction at `path` pointing to `target`.
195///
196/// Junctions can be silently broken when involving network paths or non-NTFS filesystems.
197///
198/// If creation fails but leaves behind an empty directory, it is cleaned up and the original
199/// creation error is propagated.
200#[cfg(windows)]
201fn create_junction(target: &Path, path: &Path) -> std::io::Result<()> {
202    use windows::Win32::Foundation::{
203        ERROR_ALREADY_EXISTS, ERROR_INVALID_NAME, ERROR_INVALID_PARAMETER,
204        ERROR_INVALID_REPARSE_DATA, ERROR_NOT_A_REPARSE_POINT, WIN32_ERROR,
205    };
206
207    let create_result = junction::create(target, path);
208
209    match path.metadata() {
210        Ok(_) if create_result.is_ok() => Ok(()),
211        Ok(_) => {
212            // Creation failed but left behind an empty directory. Only clean
213            // it up if the directory wasn't already there before we tried.
214            if let Err(ref create_err) = create_result {
215                if !matches!(
216                    create_err
217                        .raw_os_error()
218                        .map(|err| WIN32_ERROR(err.cast_unsigned())),
219                    Some(ERROR_ALREADY_EXISTS)
220                ) {
221                    // Not a junction (metadata succeeded normally), just
222                    // an empty directory left behind by junction::create.
223                    let _ = fs_err::remove_dir(path);
224                }
225            }
226            create_result
227        }
228        Err(err)
229            if matches!(
230                err.raw_os_error()
231                    .map(|err| WIN32_ERROR(err.cast_unsigned())),
232                Some(
233                    ERROR_INVALID_PARAMETER
234                        | ERROR_INVALID_NAME
235                        | ERROR_NOT_A_REPARSE_POINT
236                        | ERROR_INVALID_REPARSE_DATA
237                )
238            ) =>
239        {
240            // Broken reparse point.
241            let _ = fs_err::remove_dir(path);
242            Err(create_result.err().unwrap_or(err))
243        }
244        Err(err) => Err(create_result.err().unwrap_or(err)),
245    }
246}
247
248/// Create a directory link at `dst` pointing to `src`, replacing any existing link.
249///
250/// On Windows, this normally creates an NTFS junction, since junctions don't
251/// require elevated privileges. When running under Wine, which doesn't implement
252/// the reparse-point ioctl that junction creation depends on, this transparently
253/// creates a Windows directory symbolic link instead via `CreateSymbolicLinkW`
254/// (Wine maps that to a Unix symlink, so it succeeds without privileges).
255///
256/// The operation is _not_ atomic: any existing entry at `dst` is removed first,
257/// then the new link is created at the same path.
258///
259/// Note that the source must be a directory.
260///
261/// Changes to this function should be reflected in [`create_symlink`].
262#[cfg(windows)]
263pub fn replace_symlink(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> std::io::Result<()> {
264    let src = src.as_ref();
265    let dst = dst.as_ref();
266
267    if src.is_file() {
268        return Err(std::io::Error::new(
269            std::io::ErrorKind::InvalidInput,
270            format!(
271                "Cannot create a directory link for {}: is not a directory",
272                src.display()
273            ),
274        ));
275    }
276
277    if uv_windows::is_wine() {
278        replace_with_symlink_dir(src, dst)
279    } else {
280        replace_with_junction(src, dst)
281    }
282}
283
284#[cfg(windows)]
285fn replace_with_junction(src: &Path, dst: &Path) -> std::io::Result<()> {
286    // Remove the existing junction, if any.
287    match fs_err::remove_dir(dst) {
288        Ok(()) => {}
289        Err(err) if err.kind() == std::io::ErrorKind::NotFound => {}
290        Err(err) => return Err(err),
291    }
292
293    // Replace it with a new junction.
294    create_junction(src, dst)
295}
296
297#[cfg(windows)]
298fn replace_with_symlink_dir(src: &Path, dst: &Path) -> std::io::Result<()> {
299    // Best-effort removal of any existing entry. The destination may be a
300    // directory, file, or symlink, so try the directory removal first and
301    // fall back to file removal if that fails.
302    match fs_err::remove_dir_all(dst) {
303        Ok(()) => {}
304        Err(err) if err.kind() == std::io::ErrorKind::NotFound => {}
305        Err(_) => match fs_err::remove_file(dst) {
306            Ok(()) => {}
307            Err(err) if err.kind() == std::io::ErrorKind::NotFound => {}
308            Err(err) => return Err(err),
309        },
310    }
311
312    fs_err::os::windows::fs::symlink_dir(dunce::simplified(src), dunce::simplified(dst))
313}
314
315/// Create a symlink at `dst` pointing to `src`, replacing any existing symlink if necessary.
316///
317/// On Unix, this method creates a temporary file, then moves it into place.
318#[cfg(unix)]
319pub fn replace_symlink(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> std::io::Result<()> {
320    match fs_err::os::unix::fs::symlink(src.as_ref(), dst.as_ref()) {
321        Ok(()) => Ok(()),
322        Err(err) if err.kind() == std::io::ErrorKind::AlreadyExists => {
323            let temp_file = tempfile::Builder::new().make_in(
324                dst.as_ref()
325                    .parent()
326                    .expect("Symlink path must have a parent"),
327                |path| fs_err::os::unix::fs::symlink(src.as_ref(), path),
328            )?;
329            fs_err::rename(temp_file.path(), dst.as_ref())?;
330
331            Ok(())
332        }
333        Err(err) => Err(err),
334    }
335}
336
337/// Create a directory link at `dst` pointing to `src`.
338///
339/// On Windows, this normally creates an NTFS junction, falling back to a Windows
340/// directory symbolic link when running under Wine. See [`replace_symlink`] for
341/// the rationale.
342///
343/// Note that the source must be a directory.
344///
345/// Changes to this function should be reflected in [`replace_symlink`].
346#[cfg(windows)]
347pub fn create_symlink(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> std::io::Result<()> {
348    let src = src.as_ref();
349    let dst = dst.as_ref();
350
351    if src.is_file() {
352        return Err(std::io::Error::new(
353            std::io::ErrorKind::InvalidInput,
354            format!(
355                "Cannot create a directory link for {}: is not a directory",
356                src.display()
357            ),
358        ));
359    }
360
361    if uv_windows::is_wine() {
362        fs_err::os::windows::fs::symlink_dir(dunce::simplified(src), dunce::simplified(dst))
363    } else {
364        create_junction(src, dst)
365    }
366}
367
368/// Create a symlink at `dst` pointing to `src`.
369#[cfg(unix)]
370pub fn create_symlink(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> std::io::Result<()> {
371    fs_err::os::unix::fs::symlink(src.as_ref(), dst.as_ref())
372}
373
374/// Remove a symbolic link at `path` without following its target.
375pub fn remove_symlink(path: impl AsRef<Path>) -> io::Result<()> {
376    let path = path.as_ref();
377
378    #[cfg(windows)]
379    {
380        use std::os::windows::fs::FileTypeExt;
381
382        if fs_err::symlink_metadata(path)?.file_type().is_symlink_dir() {
383            return fs_err::remove_dir(path);
384        }
385    }
386
387    fs_err::remove_file(path)
388}
389
390#[cfg(all(test, windows))]
391mod windows_tests {
392    use std::assert_matches;
393    use std::os::windows::ffi::OsStrExt;
394
395    use super::*;
396
397    #[test]
398    fn fs_err_read_link_reads_created_directory_link() -> std::io::Result<()> {
399        let tempdir = tempfile::tempdir()?;
400        let target = tempdir.path().join("target");
401        fs_err::create_dir(&target)?;
402        let link = tempdir.path().join("link");
403
404        create_symlink(&target, &link)?;
405
406        assert_eq!(
407            verbatim_path(&fs_err::read_link(&link)?),
408            verbatim_path(&target)
409        );
410        Ok(())
411    }
412
413    #[test]
414    fn fs_err_read_link_reads_long_junction_target() -> std::io::Result<()> {
415        let tempdir = tempfile::tempdir()?;
416        let mut target = tempdir.path().join("target");
417        while target.as_os_str().encode_wide().count() < 257 {
418            target.push("long-path-component");
419        }
420        fs_err::create_dir_all(&target)?;
421        let link = tempdir.path().join("link");
422
423        create_symlink(&target, &link)?;
424
425        let link_target = fs_err::read_link(&link)?;
426        assert_eq!(verbatim_path(&link_target), verbatim_path(&target));
427        Ok(())
428    }
429
430    #[test]
431    fn create_junction_from_smb_failure_removes_directory() -> std::io::Result<()> {
432        #[expect(clippy::print_stderr)]
433        let Some(smb_fs) = std::env::var(uv_static::EnvVars::UV_INTERNAL__TEST_SMB_FS).ok() else {
434            eprintln!("Skipping: UV_INTERNAL__TEST_SMB_FS not set");
435            return Ok(());
436        };
437        fs_err::create_dir_all(&smb_fs)?;
438        let alt_tempdir = tempfile::tempdir_in(smb_fs)?;
439        let tempdir = tempfile::tempdir()?;
440        let link = tempdir.path().join("link");
441        let target = alt_tempdir.path().join("target");
442        fs_err::create_dir(&target)?;
443
444        let err = create_junction(&target, &link).unwrap_err();
445        assert_eq!(err.kind(), std::io::ErrorKind::InvalidFilename);
446        assert_matches!(
447            fs_err::symlink_metadata(&link),
448            Err(err) if err.kind() == std::io::ErrorKind::NotFound
449        );
450        Ok(())
451    }
452}
453
454/// Create a symlink at `dst` pointing to `src` on Unix or copy `src` to `dst` on Windows
455///
456/// This does not replace an existing symlink or file at `dst`.
457///
458/// This does not fallback to copying on Unix.
459///
460/// This function should only be used for files. If targeting a directory, use [`replace_symlink`]
461/// instead; it will use a junction on Windows, which is more performant.
462pub fn symlink_or_copy_file(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> std::io::Result<()> {
463    cfg_select! {
464        windows => {
465            fs_err::copy(src.as_ref(), dst.as_ref())?;
466        },
467        unix => {
468            fs_err::os::unix::fs::symlink(src.as_ref(), dst.as_ref())?;
469        },
470    }
471
472    Ok(())
473}
474
475/// Return a [`NamedTempFile`] in the specified directory.
476///
477/// Sets the permissions of the temporary file to `0o666`, to match the non-temporary file default.
478/// ([`NamedTempfile`] defaults to `0o600`.)
479#[cfg(unix)]
480pub fn tempfile_in(path: &Path) -> std::io::Result<NamedTempFile> {
481    use std::os::unix::fs::PermissionsExt;
482    tempfile::Builder::new()
483        .permissions(std::fs::Permissions::from_mode(0o666))
484        .tempfile_in(path)
485}
486
487/// Return a [`NamedTempFile`] in the specified directory.
488#[cfg(not(unix))]
489pub fn tempfile_in(path: &Path) -> std::io::Result<NamedTempFile> {
490    tempfile::Builder::new().tempfile_in(path)
491}
492
493/// Write `data` to `path` atomically using a temporary file and atomic rename.
494#[cfg(feature = "tokio")]
495pub async fn write_atomic(path: impl AsRef<Path>, data: impl AsRef<[u8]>) -> std::io::Result<()> {
496    let temp_file = tempfile_in(
497        path.as_ref()
498            .parent()
499            .expect("Write path must have a parent"),
500    )?;
501    fs_err::tokio::write(&temp_file, &data).await?;
502    persist_with_retry(temp_file, path.as_ref()).await
503}
504
505/// Write `data` to `path` atomically using a temporary file and atomic rename.
506pub fn write_atomic_sync(path: impl AsRef<Path>, data: impl AsRef<[u8]>) -> std::io::Result<()> {
507    let mut temp_file = tempfile_in(
508        path.as_ref()
509            .parent()
510            .expect("Write path must have a parent"),
511    )?;
512    temp_file.write_all(data.as_ref())?;
513    persist_with_retry_sync(temp_file, path.as_ref())
514}
515
516/// Copy `from` to `to` atomically using a temporary file and atomic rename.
517pub fn copy_atomic_sync(from: impl AsRef<Path>, to: impl AsRef<Path>) -> std::io::Result<()> {
518    let temp_file = tempfile_in(to.as_ref().parent().expect("Write path must have a parent"))?;
519    fs_err::copy(from.as_ref(), &temp_file)?;
520    persist_with_retry_sync(temp_file, to.as_ref())
521}
522
523#[cfg(windows)]
524fn backoff_file_move() -> backon::ExponentialBackoff {
525    use backon::BackoffBuilder;
526    // This amounts to 10 total seconds of trying the operation.
527    // We retry 10 times, starting at 10*(2^0) milliseconds for the first retry, doubling with each
528    // retry, so the last (10th) one will take about 10*(2^9) milliseconds ~= 5 seconds. All other
529    // attempts combined should equal the length of the last attempt (because it's a sum of powers
530    // of 2), so 10 seconds overall.
531    backon::ExponentialBuilder::default()
532        .with_min_delay(std::time::Duration::from_millis(10))
533        .with_max_times(10)
534        .build()
535}
536
537/// Rename a file, retrying (on Windows) if it fails due to transient operating system errors.
538#[cfg(feature = "tokio")]
539pub async fn rename_with_retry(
540    from: impl AsRef<Path>,
541    to: impl AsRef<Path>,
542) -> Result<(), std::io::Error> {
543    #[cfg(windows)]
544    {
545        use backon::Retryable;
546        // On Windows, antivirus software can lock files temporarily, making them inaccessible.
547        // This is most common for DLLs, and the common suggestion is to retry the operation with
548        // some backoff.
549        //
550        // See: <https://github.com/astral-sh/uv/issues/1491> & <https://github.com/astral-sh/uv/issues/9531>
551        let from = from.as_ref();
552        let to = to.as_ref();
553
554        let rename = async || fs_err::rename(from, to);
555
556        rename
557            .retry(backoff_file_move())
558            .sleep(tokio::time::sleep)
559            .when(|e| e.kind() == std::io::ErrorKind::PermissionDenied)
560            .notify(|err, _dur| {
561                warn!(
562                    "Retrying rename from {} to {} due to transient error: {}",
563                    from.display(),
564                    to.display(),
565                    err
566                );
567            })
568            .await
569    }
570    #[cfg(not(windows))]
571    {
572        fs_err::tokio::rename(from, to).await
573    }
574}
575
576// TODO(zanieb): Look into reusing this code?
577/// Wrap an arbitrary operation on two files, e.g., copying, with retries on transient operating
578/// system errors.
579#[cfg_attr(not(windows), allow(unused_variables))]
580pub fn with_retry_sync(
581    from: impl AsRef<Path>,
582    to: impl AsRef<Path>,
583    operation_name: &str,
584    operation: impl Fn() -> Result<(), std::io::Error>,
585) -> Result<(), std::io::Error> {
586    #[cfg(windows)]
587    {
588        use backon::BlockingRetryable;
589        // On Windows, antivirus software can lock files temporarily, making them inaccessible.
590        // This is most common for DLLs, and the common suggestion is to retry the operation with
591        // some backoff.
592        //
593        // See: <https://github.com/astral-sh/uv/issues/1491> & <https://github.com/astral-sh/uv/issues/9531>
594        let from = from.as_ref();
595        let to = to.as_ref();
596
597        operation
598            .retry(backoff_file_move())
599            .sleep(std::thread::sleep)
600            .when(|err| err.kind() == std::io::ErrorKind::PermissionDenied)
601            .notify(|err, _dur| {
602                warn!(
603                    "Retrying {} from {} to {} due to transient error: {}",
604                    operation_name,
605                    from.display(),
606                    to.display(),
607                    err
608                );
609            })
610            .call()
611            .map_err(|err| {
612                std::io::Error::other(format!(
613                    "Failed {} {} to {}: {}",
614                    operation_name,
615                    from.display(),
616                    to.display(),
617                    err
618                ))
619            })
620    }
621    #[cfg(not(windows))]
622    {
623        operation()
624    }
625}
626
627/// Why a file persist failed
628#[cfg(windows)]
629enum PersistRetryError {
630    /// Something went wrong while persisting, maybe retry (contains error message)
631    Persist(String),
632    /// Something went wrong trying to retrieve the file to persist, we must bail
633    LostState,
634}
635
636/// Persist a `NamedTempFile`, retrying (on Windows) if it fails due to transient operating system
637/// errors.
638#[cfg(feature = "tokio")]
639async fn persist_with_retry(
640    from: NamedTempFile,
641    to: impl AsRef<Path>,
642) -> Result<(), std::io::Error> {
643    #[cfg(windows)]
644    {
645        use backon::Retryable;
646        // On Windows, antivirus software can lock files temporarily, making them inaccessible.
647        // This is most common for DLLs, and the common suggestion is to retry the operation with
648        // some backoff.
649        //
650        // See: <https://github.com/astral-sh/uv/issues/1491> & <https://github.com/astral-sh/uv/issues/9531>
651        let to = to.as_ref();
652
653        // Ok there's a lot of complex ownership stuff going on here.
654        //
655        // the `NamedTempFile` `persist` method consumes `self`, and returns it back inside
656        // the Error in case of `PersistError`:
657        // https://docs.rs/tempfile/latest/tempfile/struct.NamedTempFile.html#method.persist
658        // So every time we fail, we need to reset the `NamedTempFile` to try again.
659        //
660        // Every time we (re)try we call this outer closure (`let persist = ...`), so it needs to
661        // be at least a `FnMut` (as opposed to `Fnonce`). However the closure needs to return a
662        // totally owned `Future` (so effectively it returns a `FnOnce`).
663        //
664        // But if the `Future` is totally owned it *necessarily* can't write back the `NamedTempFile`
665        // to somewhere the outer `FnMut` can see using references. So we need to use `Arc`s
666        // with interior mutability (`Mutex`) to have the closure and all the Futures it creates share
667        // a single memory location that the `NamedTempFile` can be shuttled in and out of.
668        //
669        // In spite of the Mutex all of this code will run logically serially, so there shouldn't be a
670        // chance for a race where we try to get the `NamedTempFile` but it's actually None. The code
671        // is just written pedantically/robustly.
672        let from = std::sync::Arc::new(std::sync::Mutex::new(Some(from)));
673        let persist = || {
674            // Turn our by-ref-captured Arc into an owned Arc that the Future can capture by-value
675            let from2 = from.clone();
676
677            async move {
678                let maybe_file: Option<NamedTempFile> = from2
679                    .lock()
680                    .map_err(|_| PersistRetryError::LostState)?
681                    .take();
682                if let Some(file) = maybe_file {
683                    file.persist(to).map_err(|err| {
684                        let error_message: String = err.to_string();
685                        // Set back the `NamedTempFile` returned back by the Error
686                        if let Ok(mut guard) = from2.lock() {
687                            *guard = Some(err.file);
688                            PersistRetryError::Persist(error_message)
689                        } else {
690                            PersistRetryError::LostState
691                        }
692                    })
693                } else {
694                    Err(PersistRetryError::LostState)
695                }
696            }
697        };
698
699        let persisted = persist
700            .retry(backoff_file_move())
701            .sleep(tokio::time::sleep)
702            .when(|err| matches!(err, PersistRetryError::Persist(_)))
703            .notify(|err, _dur| {
704                if let PersistRetryError::Persist(error_message) = err {
705                    warn!(
706                        "Retrying to persist temporary file to {}: {}",
707                        to.display(),
708                        error_message,
709                    );
710                }
711            })
712            .await;
713
714        match persisted {
715            Ok(_) => Ok(()),
716            Err(PersistRetryError::Persist(error_message)) => Err(std::io::Error::other(format!(
717                "Failed to persist temporary file to {}: {}",
718                to.display(),
719                error_message,
720            ))),
721            Err(PersistRetryError::LostState) => Err(std::io::Error::other(format!(
722                "Failed to retrieve temporary file while trying to persist to {}",
723                to.display()
724            ))),
725        }
726    }
727    #[cfg(not(windows))]
728    {
729        async { fs_err::rename(from, to) }.await
730    }
731}
732
733/// Persist a `NamedTempFile`, retrying (on Windows) if it fails due to transient operating system
734/// errors.
735///
736/// This is a synchronous implementation of [`persist_with_retry`].
737pub fn persist_with_retry_sync(
738    from: NamedTempFile,
739    to: impl AsRef<Path>,
740) -> Result<(), std::io::Error> {
741    #[cfg(windows)]
742    {
743        use backon::BlockingRetryable;
744        // On Windows, antivirus software can lock files temporarily, making them inaccessible.
745        // This is most common for DLLs, and the common suggestion is to retry the operation with
746        // some backoff.
747        //
748        // See: <https://github.com/astral-sh/uv/issues/1491> & <https://github.com/astral-sh/uv/issues/9531>
749        let to = to.as_ref();
750
751        // the `NamedTempFile` `persist` method consumes `self`, and returns it back inside the Error in case of `PersistError`
752        // https://docs.rs/tempfile/latest/tempfile/struct.NamedTempFile.html#method.persist
753        // So we will update the `from` optional value in safe and borrow-checker friendly way every retry
754        // Allows us to use the NamedTempFile inside a FnMut closure used for backoff::retry
755        let mut from = Some(from);
756        let persist = || {
757            // Needed because we cannot move out of `from`, a captured variable in an `FnMut` closure, and then pass it to the async move block
758            if let Some(file) = from.take() {
759                file.persist(to).map_err(|err| {
760                    let error_message = err.to_string();
761                    // Set back the NamedTempFile returned back by the Error
762                    from = Some(err.file);
763                    PersistRetryError::Persist(error_message)
764                })
765            } else {
766                Err(PersistRetryError::LostState)
767            }
768        };
769
770        let persisted = persist
771            .retry(backoff_file_move())
772            .sleep(std::thread::sleep)
773            .when(|err| matches!(err, PersistRetryError::Persist(_)))
774            .notify(|err, _dur| {
775                if let PersistRetryError::Persist(error_message) = err {
776                    warn!(
777                        "Retrying to persist temporary file to {}: {}",
778                        to.display(),
779                        error_message,
780                    );
781                }
782            })
783            .call();
784
785        match persisted {
786            Ok(_) => Ok(()),
787            Err(PersistRetryError::Persist(error_message)) => Err(std::io::Error::other(format!(
788                "Failed to persist temporary file to {}: {}",
789                to.display(),
790                error_message,
791            ))),
792            Err(PersistRetryError::LostState) => Err(std::io::Error::other(format!(
793                "Failed to retrieve temporary file while trying to persist to {}",
794                to.display()
795            ))),
796        }
797    }
798    #[cfg(not(windows))]
799    {
800        fs_err::rename(from, to)
801    }
802}
803
804/// Iterate over the subdirectories of a directory.
805///
806/// If the directory does not exist, returns an empty iterator.
807pub fn directories(
808    path: impl AsRef<Path>,
809) -> Result<impl Iterator<Item = PathBuf>, std::io::Error> {
810    let entries = match path.as_ref().read_dir() {
811        Ok(entries) => Some(entries),
812        Err(err) if err.kind() == std::io::ErrorKind::NotFound => None,
813        Err(err) => return Err(err),
814    };
815    Ok(entries
816        .into_iter()
817        .flatten()
818        .filter_map(|entry| match entry {
819            Ok(entry) => Some(entry),
820            Err(err) => {
821                warn!("Failed to read entry: {err}");
822                None
823            }
824        })
825        .filter(|entry| entry.file_type().is_ok_and(|file_type| file_type.is_dir()))
826        .map(|entry| entry.path()))
827}
828
829/// Iterate over the entries in a directory.
830///
831/// If the directory does not exist, returns an empty iterator.
832pub fn entries(path: impl AsRef<Path>) -> Result<impl Iterator<Item = PathBuf>, std::io::Error> {
833    let entries = match path.as_ref().read_dir() {
834        Ok(entries) => Some(entries),
835        Err(err) if err.kind() == std::io::ErrorKind::NotFound => None,
836        Err(err) => return Err(err),
837    };
838    Ok(entries
839        .into_iter()
840        .flatten()
841        .filter_map(|entry| match entry {
842            Ok(entry) => Some(entry),
843            Err(err) => {
844                warn!("Failed to read entry: {err}");
845                None
846            }
847        })
848        .map(|entry| entry.path()))
849}
850
851/// Iterate over the files in a directory.
852///
853/// If the directory does not exist, returns an empty iterator.
854pub fn files(path: impl AsRef<Path>) -> Result<impl Iterator<Item = PathBuf>, std::io::Error> {
855    let entries = match path.as_ref().read_dir() {
856        Ok(entries) => Some(entries),
857        Err(err) if err.kind() == std::io::ErrorKind::NotFound => None,
858        Err(err) => return Err(err),
859    };
860    Ok(entries
861        .into_iter()
862        .flatten()
863        .filter_map(|entry| match entry {
864            Ok(entry) => Some(entry),
865            Err(err) => {
866                warn!("Failed to read entry: {err}");
867                None
868            }
869        })
870        .filter(|entry| entry.file_type().is_ok_and(|file_type| file_type.is_file()))
871        .map(|entry| entry.path()))
872}
873
874/// Returns `true` if a path is a temporary file or directory.
875pub fn is_temporary(path: impl AsRef<Path>) -> bool {
876    path.as_ref()
877        .file_name()
878        .and_then(|name| name.to_str())
879        .is_some_and(|name| name.starts_with(".tmp"))
880}
881
882/// Checks if the grandparent directory of the given executable is the base
883/// of a virtual environment.
884///
885/// The procedure described in PEP 405 includes checking both the parent and
886/// grandparent directory of an executable, but in practice we've found this to
887/// be unnecessary.
888pub fn is_virtualenv_executable(executable: impl AsRef<Path>) -> bool {
889    executable
890        .as_ref()
891        .parent()
892        .and_then(Path::parent)
893        .is_some_and(is_virtualenv_base)
894}
895
896/// Returns `true` if a path is the base path of a virtual environment,
897/// indicated by the presence of a `pyvenv.cfg` file.
898///
899/// The procedure described in PEP 405 includes scanning `pyvenv.cfg`
900/// for a `home` key, but in practice we've found this to be
901/// unnecessary.
902pub fn is_virtualenv_base(path: impl AsRef<Path>) -> bool {
903    path.as_ref().join("pyvenv.cfg").is_file()
904}
905
906/// Whether the error is due to a lock being held.
907fn is_known_already_locked_error(err: &std::fs::TryLockError) -> bool {
908    match err {
909        std::fs::TryLockError::WouldBlock => true,
910        std::fs::TryLockError::Error(err) => {
911            // On Windows, we've seen: Os { code: 33, kind: Uncategorized, message: "The process cannot access the file because another process has locked a portion of the file." }
912            if cfg!(windows) && err.raw_os_error() == Some(33) {
913                return true;
914            }
915            false
916        }
917    }
918}
919
920/// An asynchronous reader that reports progress as bytes are read.
921#[cfg(feature = "tokio")]
922pub struct ProgressReader<Reader: tokio::io::AsyncRead + Unpin, Callback: Fn(usize) + Unpin> {
923    reader: Reader,
924    callback: Callback,
925}
926
927#[cfg(feature = "tokio")]
928impl<Reader: tokio::io::AsyncRead + Unpin, Callback: Fn(usize) + Unpin>
929    ProgressReader<Reader, Callback>
930{
931    /// Create a new [`ProgressReader`] that wraps another reader.
932    pub fn new(reader: Reader, callback: Callback) -> Self {
933        Self { reader, callback }
934    }
935}
936
937#[cfg(feature = "tokio")]
938impl<Reader: tokio::io::AsyncRead + Unpin, Callback: Fn(usize) + Unpin> tokio::io::AsyncRead
939    for ProgressReader<Reader, Callback>
940{
941    fn poll_read(
942        mut self: std::pin::Pin<&mut Self>,
943        cx: &mut std::task::Context<'_>,
944        buf: &mut tokio::io::ReadBuf<'_>,
945    ) -> std::task::Poll<std::io::Result<()>> {
946        std::pin::Pin::new(&mut self.as_mut().reader)
947            .poll_read(cx, buf)
948            .map_ok(|()| {
949                (self.callback)(buf.filled().len());
950            })
951    }
952}
953
954/// Recursively copy a directory and its contents.
955pub fn copy_dir_all(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> std::io::Result<()> {
956    fs_err::create_dir_all(&dst)?;
957    for entry in fs_err::read_dir(src.as_ref())? {
958        let entry = entry?;
959        let ty = entry.file_type()?;
960        if ty.is_dir() {
961            copy_dir_all(entry.path(), dst.as_ref().join(entry.file_name()))?;
962        } else {
963            fs_err::copy(entry.path(), dst.as_ref().join(entry.file_name()))?;
964        }
965    }
966    Ok(())
967}
968
969/// Perform a safe removal of a virtual environment.
970///
971/// The link or file at `location` is removed without following it.
972pub fn remove_virtualenv(location: &Path) -> io::Result<()> {
973    if !fs_err::symlink_metadata(location)?.is_dir() {
974        return remove_symlink(location);
975    }
976
977    // On Windows, if the current executable is in the directory, defer self-deletion since Windows
978    // won't let you unlink a running executable.
979    #[cfg(windows)]
980    if let Ok(itself) = std::env::current_exe() {
981        let target = std::path::absolute(location)?;
982        if itself.starts_with(&target) {
983            debug!("Detected self-delete of executable: {}", itself.display());
984            self_replace::self_delete_outside_path(location)?;
985        }
986    }
987
988    // We defer removal of the `pyvenv.cfg` until the end, so if we fail to remove the environment,
989    // uv can still identify it as a Python virtual environment that can be deleted.
990    for entry in fs_err::read_dir(location)? {
991        let entry = entry?;
992        let path = entry.path();
993        if path == location.join("pyvenv.cfg") {
994            continue;
995        }
996        if path.is_dir() {
997            fs_err::remove_dir_all(&path)?;
998        } else {
999            fs_err::remove_file(&path)?;
1000        }
1001    }
1002
1003    match fs_err::remove_file(location.join("pyvenv.cfg")) {
1004        Ok(()) => {}
1005        Err(err) if err.kind() == io::ErrorKind::NotFound => {}
1006        Err(err) => return Err(err),
1007    }
1008
1009    // Remove the virtual environment directory itself
1010    match fs_err::remove_dir_all(location) {
1011        Ok(()) => {}
1012        Err(err) if err.kind() == io::ErrorKind::NotFound => {}
1013        // If the virtual environment is a mounted file system, e.g., in a Docker container, we
1014        // cannot delete it — but that doesn't need to be a fatal error
1015        Err(err) if err.kind() == io::ErrorKind::ResourceBusy => {
1016            debug!(
1017                "Skipping removal of `{}` directory due to {err}",
1018                location.display(),
1019            );
1020        }
1021        Err(err) => return Err(err),
1022    }
1023
1024    Ok(())
1025}
1026
1027/// Prepare an empty virtual environment directory, resolving links when possible.
1028///
1029/// Returns whether an existing entry was found.
1030pub fn clear_virtualenv(location: &Path) -> io::Result<bool> {
1031    let location = location
1032        .canonicalize()
1033        .unwrap_or_else(|_| location.to_path_buf());
1034    let cleared = match remove_virtualenv(&location) {
1035        Ok(()) => true,
1036        Err(err) if err.kind() == io::ErrorKind::NotFound => false,
1037        Err(err) => return Err(err),
1038    };
1039    fs_err::create_dir_all(location)?;
1040    Ok(cleared)
1041}
1042
1043#[cfg(test)]
1044mod tests {
1045    use std::assert_matches;
1046
1047    use super::*;
1048
1049    #[test]
1050    fn remove_symlink_removes_directory_link_without_removing_target() -> io::Result<()> {
1051        let tempdir = tempfile::tempdir()?;
1052        let target = tempdir.path().join("target");
1053        fs_err::create_dir(&target)?;
1054        fs_err::write(target.join("file"), "content")?;
1055        let link = tempdir.path().join("link");
1056
1057        create_symlink(&target, &link)?;
1058        remove_symlink(&link)?;
1059
1060        assert_matches!(
1061            fs_err::symlink_metadata(&link),
1062            Err(err) if err.kind() == io::ErrorKind::NotFound
1063        );
1064        assert_eq!(fs_err::read_to_string(target.join("file"))?, "content");
1065        Ok(())
1066    }
1067
1068    #[test]
1069    fn remove_virtualenv_removes_directory_link_without_removing_target() -> io::Result<()> {
1070        let tempdir = tempfile::tempdir()?;
1071        let target = tempdir.path().join("target");
1072        fs_err::create_dir(&target)?;
1073        let marker = target.join("marker");
1074        fs_err::write(&marker, "")?;
1075        let environment = tempdir.path().join("environment");
1076        create_symlink(&target, &environment)?;
1077
1078        remove_virtualenv(&environment)?;
1079
1080        assert_matches!(
1081            fs_err::symlink_metadata(environment),
1082            Err(err) if err.kind() == io::ErrorKind::NotFound
1083        );
1084        assert!(marker.is_file());
1085        Ok(())
1086    }
1087
1088    #[test]
1089    fn clear_virtualenv_recreates_missing_directory() -> io::Result<()> {
1090        let tempdir = tempfile::tempdir()?;
1091        let environment = tempdir.path().join("environment");
1092
1093        assert!(!clear_virtualenv(&environment)?);
1094        assert!(environment.is_dir());
1095        Ok(())
1096    }
1097}