Skip to main content

uv_fs/
lib.rs

1use std::io;
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    // Attempt to create the symlink directly.
321    match fs_err::os::unix::fs::symlink(src.as_ref(), dst.as_ref()) {
322        Ok(()) => Ok(()),
323        Err(err) if err.kind() == std::io::ErrorKind::AlreadyExists => {
324            // Create a symlink, using a temporary file to ensure atomicity.
325            let temp_dir = tempfile::tempdir_in(dst.as_ref().parent().unwrap())?;
326            let temp_file = temp_dir.path().join("link");
327            fs_err::os::unix::fs::symlink(src, &temp_file)?;
328
329            // Move the symlink into the target location.
330            fs_err::rename(&temp_file, dst.as_ref())?;
331
332            Ok(())
333        }
334        Err(err) => Err(err),
335    }
336}
337
338/// Create a directory link at `dst` pointing to `src`.
339///
340/// On Windows, this normally creates an NTFS junction, falling back to a Windows
341/// directory symbolic link when running under Wine. See [`replace_symlink`] for
342/// the rationale.
343///
344/// Note that the source must be a directory.
345///
346/// Changes to this function should be reflected in [`replace_symlink`].
347#[cfg(windows)]
348pub fn create_symlink(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> std::io::Result<()> {
349    let src = src.as_ref();
350    let dst = dst.as_ref();
351
352    if src.is_file() {
353        return Err(std::io::Error::new(
354            std::io::ErrorKind::InvalidInput,
355            format!(
356                "Cannot create a directory link for {}: is not a directory",
357                src.display()
358            ),
359        ));
360    }
361
362    if uv_windows::is_wine() {
363        fs_err::os::windows::fs::symlink_dir(dunce::simplified(src), dunce::simplified(dst))
364    } else {
365        create_junction(src, dst)
366    }
367}
368
369/// Create a symlink at `dst` pointing to `src`.
370#[cfg(unix)]
371pub fn create_symlink(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> std::io::Result<()> {
372    fs_err::os::unix::fs::symlink(src.as_ref(), dst.as_ref())
373}
374
375/// Remove a symbolic link at `path` without following its target.
376pub fn remove_symlink(path: impl AsRef<Path>) -> io::Result<()> {
377    let path = path.as_ref();
378
379    #[cfg(windows)]
380    {
381        use std::os::windows::fs::FileTypeExt;
382
383        if fs_err::symlink_metadata(path)?.file_type().is_symlink_dir() {
384            return fs_err::remove_dir(path);
385        }
386    }
387
388    fs_err::remove_file(path)
389}
390
391#[cfg(all(test, windows))]
392mod windows_tests {
393    use std::assert_matches;
394    use std::os::windows::ffi::OsStrExt;
395
396    use super::*;
397
398    #[test]
399    fn fs_err_read_link_reads_created_directory_link() -> std::io::Result<()> {
400        let tempdir = tempfile::tempdir()?;
401        let target = tempdir.path().join("target");
402        fs_err::create_dir(&target)?;
403        let link = tempdir.path().join("link");
404
405        create_symlink(&target, &link)?;
406
407        assert_eq!(
408            verbatim_path(&fs_err::read_link(&link)?),
409            verbatim_path(&target)
410        );
411        Ok(())
412    }
413
414    #[test]
415    fn fs_err_read_link_reads_long_junction_target() -> std::io::Result<()> {
416        let tempdir = tempfile::tempdir()?;
417        let mut target = tempdir.path().join("target");
418        while target.as_os_str().encode_wide().count() < 257 {
419            target.push("long-path-component");
420        }
421        fs_err::create_dir_all(&target)?;
422        let link = tempdir.path().join("link");
423
424        create_symlink(&target, &link)?;
425
426        let link_target = fs_err::read_link(&link)?;
427        assert_eq!(verbatim_path(&link_target), verbatim_path(&target));
428        Ok(())
429    }
430
431    #[test]
432    fn create_junction_from_smb_failure_removes_directory() -> std::io::Result<()> {
433        #[expect(clippy::print_stderr)]
434        let Some(smb_fs) = std::env::var(uv_static::EnvVars::UV_INTERNAL__TEST_SMB_FS).ok() else {
435            eprintln!("Skipping: UV_INTERNAL__TEST_SMB_FS not set");
436            return Ok(());
437        };
438        fs_err::create_dir_all(&smb_fs)?;
439        let alt_tempdir = tempfile::tempdir_in(smb_fs)?;
440        let tempdir = tempfile::tempdir()?;
441        let link = tempdir.path().join("link");
442        let target = alt_tempdir.path().join("target");
443        fs_err::create_dir(&target)?;
444
445        let err = create_junction(&target, &link).unwrap_err();
446        assert_eq!(err.kind(), std::io::ErrorKind::InvalidFilename);
447        assert_matches!(
448            fs_err::symlink_metadata(&link),
449            Err(err) if err.kind() == std::io::ErrorKind::NotFound
450        );
451        Ok(())
452    }
453}
454
455/// Create a symlink at `dst` pointing to `src` on Unix or copy `src` to `dst` on Windows
456///
457/// This does not replace an existing symlink or file at `dst`.
458///
459/// This does not fallback to copying on Unix.
460///
461/// This function should only be used for files. If targeting a directory, use [`replace_symlink`]
462/// instead; it will use a junction on Windows, which is more performant.
463pub fn symlink_or_copy_file(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> std::io::Result<()> {
464    cfg_select! {
465        windows => {
466            fs_err::copy(src.as_ref(), dst.as_ref())?;
467        },
468        unix => {
469            fs_err::os::unix::fs::symlink(src.as_ref(), dst.as_ref())?;
470        },
471    }
472
473    Ok(())
474}
475
476/// Return a [`NamedTempFile`] in the specified directory.
477///
478/// Sets the permissions of the temporary file to `0o666`, to match the non-temporary file default.
479/// ([`NamedTempfile`] defaults to `0o600`.)
480#[cfg(unix)]
481pub fn tempfile_in(path: &Path) -> std::io::Result<NamedTempFile> {
482    use std::os::unix::fs::PermissionsExt;
483    tempfile::Builder::new()
484        .permissions(std::fs::Permissions::from_mode(0o666))
485        .tempfile_in(path)
486}
487
488/// Return a [`NamedTempFile`] in the specified directory.
489#[cfg(not(unix))]
490pub fn tempfile_in(path: &Path) -> std::io::Result<NamedTempFile> {
491    tempfile::Builder::new().tempfile_in(path)
492}
493
494/// Write `data` to `path` atomically using a temporary file and atomic rename.
495#[cfg(feature = "tokio")]
496pub async fn write_atomic(path: impl AsRef<Path>, data: impl AsRef<[u8]>) -> std::io::Result<()> {
497    let temp_file = tempfile_in(
498        path.as_ref()
499            .parent()
500            .expect("Write path must have a parent"),
501    )?;
502    fs_err::tokio::write(&temp_file, &data).await?;
503    persist_with_retry(temp_file, path.as_ref()).await
504}
505
506/// Write `data` to `path` atomically using a temporary file and atomic rename.
507pub fn write_atomic_sync(path: impl AsRef<Path>, data: impl AsRef<[u8]>) -> std::io::Result<()> {
508    let temp_file = tempfile_in(
509        path.as_ref()
510            .parent()
511            .expect("Write path must have a parent"),
512    )?;
513    fs_err::write(&temp_file, &data)?;
514    persist_with_retry_sync(temp_file, path.as_ref())
515}
516
517/// Copy `from` to `to` atomically using a temporary file and atomic rename.
518pub fn copy_atomic_sync(from: impl AsRef<Path>, to: impl AsRef<Path>) -> std::io::Result<()> {
519    let temp_file = tempfile_in(to.as_ref().parent().expect("Write path must have a parent"))?;
520    fs_err::copy(from.as_ref(), &temp_file)?;
521    persist_with_retry_sync(temp_file, to.as_ref())
522}
523
524#[cfg(windows)]
525fn backoff_file_move() -> backon::ExponentialBackoff {
526    use backon::BackoffBuilder;
527    // This amounts to 10 total seconds of trying the operation.
528    // We retry 10 times, starting at 10*(2^0) milliseconds for the first retry, doubling with each
529    // retry, so the last (10th) one will take about 10*(2^9) milliseconds ~= 5 seconds. All other
530    // attempts combined should equal the length of the last attempt (because it's a sum of powers
531    // of 2), so 10 seconds overall.
532    backon::ExponentialBuilder::default()
533        .with_min_delay(std::time::Duration::from_millis(10))
534        .with_max_times(10)
535        .build()
536}
537
538/// Rename a file, retrying (on Windows) if it fails due to transient operating system errors.
539#[cfg(feature = "tokio")]
540pub async fn rename_with_retry(
541    from: impl AsRef<Path>,
542    to: impl AsRef<Path>,
543) -> Result<(), std::io::Error> {
544    #[cfg(windows)]
545    {
546        use backon::Retryable;
547        // On Windows, antivirus software can lock files temporarily, making them inaccessible.
548        // This is most common for DLLs, and the common suggestion is to retry the operation with
549        // some backoff.
550        //
551        // See: <https://github.com/astral-sh/uv/issues/1491> & <https://github.com/astral-sh/uv/issues/9531>
552        let from = from.as_ref();
553        let to = to.as_ref();
554
555        let rename = async || fs_err::rename(from, to);
556
557        rename
558            .retry(backoff_file_move())
559            .sleep(tokio::time::sleep)
560            .when(|e| e.kind() == std::io::ErrorKind::PermissionDenied)
561            .notify(|err, _dur| {
562                warn!(
563                    "Retrying rename from {} to {} due to transient error: {}",
564                    from.display(),
565                    to.display(),
566                    err
567                );
568            })
569            .await
570    }
571    #[cfg(not(windows))]
572    {
573        fs_err::tokio::rename(from, to).await
574    }
575}
576
577// TODO(zanieb): Look into reusing this code?
578/// Wrap an arbitrary operation on two files, e.g., copying, with retries on transient operating
579/// system errors.
580#[cfg_attr(not(windows), allow(unused_variables))]
581pub fn with_retry_sync(
582    from: impl AsRef<Path>,
583    to: impl AsRef<Path>,
584    operation_name: &str,
585    operation: impl Fn() -> Result<(), std::io::Error>,
586) -> Result<(), std::io::Error> {
587    #[cfg(windows)]
588    {
589        use backon::BlockingRetryable;
590        // On Windows, antivirus software can lock files temporarily, making them inaccessible.
591        // This is most common for DLLs, and the common suggestion is to retry the operation with
592        // some backoff.
593        //
594        // See: <https://github.com/astral-sh/uv/issues/1491> & <https://github.com/astral-sh/uv/issues/9531>
595        let from = from.as_ref();
596        let to = to.as_ref();
597
598        operation
599            .retry(backoff_file_move())
600            .sleep(std::thread::sleep)
601            .when(|err| err.kind() == std::io::ErrorKind::PermissionDenied)
602            .notify(|err, _dur| {
603                warn!(
604                    "Retrying {} from {} to {} due to transient error: {}",
605                    operation_name,
606                    from.display(),
607                    to.display(),
608                    err
609                );
610            })
611            .call()
612            .map_err(|err| {
613                std::io::Error::other(format!(
614                    "Failed {} {} to {}: {}",
615                    operation_name,
616                    from.display(),
617                    to.display(),
618                    err
619                ))
620            })
621    }
622    #[cfg(not(windows))]
623    {
624        operation()
625    }
626}
627
628/// Why a file persist failed
629#[cfg(windows)]
630enum PersistRetryError {
631    /// Something went wrong while persisting, maybe retry (contains error message)
632    Persist(String),
633    /// Something went wrong trying to retrieve the file to persist, we must bail
634    LostState,
635}
636
637/// Persist a `NamedTempFile`, retrying (on Windows) if it fails due to transient operating system
638/// errors.
639#[cfg(feature = "tokio")]
640async fn persist_with_retry(
641    from: NamedTempFile,
642    to: impl AsRef<Path>,
643) -> Result<(), std::io::Error> {
644    #[cfg(windows)]
645    {
646        use backon::Retryable;
647        // On Windows, antivirus software can lock files temporarily, making them inaccessible.
648        // This is most common for DLLs, and the common suggestion is to retry the operation with
649        // some backoff.
650        //
651        // See: <https://github.com/astral-sh/uv/issues/1491> & <https://github.com/astral-sh/uv/issues/9531>
652        let to = to.as_ref();
653
654        // Ok there's a lot of complex ownership stuff going on here.
655        //
656        // the `NamedTempFile` `persist` method consumes `self`, and returns it back inside
657        // the Error in case of `PersistError`:
658        // https://docs.rs/tempfile/latest/tempfile/struct.NamedTempFile.html#method.persist
659        // So every time we fail, we need to reset the `NamedTempFile` to try again.
660        //
661        // Every time we (re)try we call this outer closure (`let persist = ...`), so it needs to
662        // be at least a `FnMut` (as opposed to `Fnonce`). However the closure needs to return a
663        // totally owned `Future` (so effectively it returns a `FnOnce`).
664        //
665        // But if the `Future` is totally owned it *necessarily* can't write back the `NamedTempFile`
666        // to somewhere the outer `FnMut` can see using references. So we need to use `Arc`s
667        // with interior mutability (`Mutex`) to have the closure and all the Futures it creates share
668        // a single memory location that the `NamedTempFile` can be shuttled in and out of.
669        //
670        // In spite of the Mutex all of this code will run logically serially, so there shouldn't be a
671        // chance for a race where we try to get the `NamedTempFile` but it's actually None. The code
672        // is just written pedantically/robustly.
673        let from = std::sync::Arc::new(std::sync::Mutex::new(Some(from)));
674        let persist = || {
675            // Turn our by-ref-captured Arc into an owned Arc that the Future can capture by-value
676            let from2 = from.clone();
677
678            async move {
679                let maybe_file: Option<NamedTempFile> = from2
680                    .lock()
681                    .map_err(|_| PersistRetryError::LostState)?
682                    .take();
683                if let Some(file) = maybe_file {
684                    file.persist(to).map_err(|err| {
685                        let error_message: String = err.to_string();
686                        // Set back the `NamedTempFile` returned back by the Error
687                        if let Ok(mut guard) = from2.lock() {
688                            *guard = Some(err.file);
689                            PersistRetryError::Persist(error_message)
690                        } else {
691                            PersistRetryError::LostState
692                        }
693                    })
694                } else {
695                    Err(PersistRetryError::LostState)
696                }
697            }
698        };
699
700        let persisted = persist
701            .retry(backoff_file_move())
702            .sleep(tokio::time::sleep)
703            .when(|err| matches!(err, PersistRetryError::Persist(_)))
704            .notify(|err, _dur| {
705                if let PersistRetryError::Persist(error_message) = err {
706                    warn!(
707                        "Retrying to persist temporary file to {}: {}",
708                        to.display(),
709                        error_message,
710                    );
711                }
712            })
713            .await;
714
715        match persisted {
716            Ok(_) => Ok(()),
717            Err(PersistRetryError::Persist(error_message)) => Err(std::io::Error::other(format!(
718                "Failed to persist temporary file to {}: {}",
719                to.display(),
720                error_message,
721            ))),
722            Err(PersistRetryError::LostState) => Err(std::io::Error::other(format!(
723                "Failed to retrieve temporary file while trying to persist to {}",
724                to.display()
725            ))),
726        }
727    }
728    #[cfg(not(windows))]
729    {
730        async { fs_err::rename(from, to) }.await
731    }
732}
733
734/// Persist a `NamedTempFile`, retrying (on Windows) if it fails due to transient operating system
735/// errors.
736///
737/// This is a synchronous implementation of [`persist_with_retry`].
738pub fn persist_with_retry_sync(
739    from: NamedTempFile,
740    to: impl AsRef<Path>,
741) -> Result<(), std::io::Error> {
742    #[cfg(windows)]
743    {
744        use backon::BlockingRetryable;
745        // On Windows, antivirus software can lock files temporarily, making them inaccessible.
746        // This is most common for DLLs, and the common suggestion is to retry the operation with
747        // some backoff.
748        //
749        // See: <https://github.com/astral-sh/uv/issues/1491> & <https://github.com/astral-sh/uv/issues/9531>
750        let to = to.as_ref();
751
752        // the `NamedTempFile` `persist` method consumes `self`, and returns it back inside the Error in case of `PersistError`
753        // https://docs.rs/tempfile/latest/tempfile/struct.NamedTempFile.html#method.persist
754        // So we will update the `from` optional value in safe and borrow-checker friendly way every retry
755        // Allows us to use the NamedTempFile inside a FnMut closure used for backoff::retry
756        let mut from = Some(from);
757        let persist = || {
758            // Needed because we cannot move out of `from`, a captured variable in an `FnMut` closure, and then pass it to the async move block
759            if let Some(file) = from.take() {
760                file.persist(to).map_err(|err| {
761                    let error_message = err.to_string();
762                    // Set back the NamedTempFile returned back by the Error
763                    from = Some(err.file);
764                    PersistRetryError::Persist(error_message)
765                })
766            } else {
767                Err(PersistRetryError::LostState)
768            }
769        };
770
771        let persisted = persist
772            .retry(backoff_file_move())
773            .sleep(std::thread::sleep)
774            .when(|err| matches!(err, PersistRetryError::Persist(_)))
775            .notify(|err, _dur| {
776                if let PersistRetryError::Persist(error_message) = err {
777                    warn!(
778                        "Retrying to persist temporary file to {}: {}",
779                        to.display(),
780                        error_message,
781                    );
782                }
783            })
784            .call();
785
786        match persisted {
787            Ok(_) => Ok(()),
788            Err(PersistRetryError::Persist(error_message)) => Err(std::io::Error::other(format!(
789                "Failed to persist temporary file to {}: {}",
790                to.display(),
791                error_message,
792            ))),
793            Err(PersistRetryError::LostState) => Err(std::io::Error::other(format!(
794                "Failed to retrieve temporary file while trying to persist to {}",
795                to.display()
796            ))),
797        }
798    }
799    #[cfg(not(windows))]
800    {
801        fs_err::rename(from, to)
802    }
803}
804
805/// Iterate over the subdirectories of a directory.
806///
807/// If the directory does not exist, returns an empty iterator.
808pub fn directories(
809    path: impl AsRef<Path>,
810) -> Result<impl Iterator<Item = PathBuf>, std::io::Error> {
811    let entries = match path.as_ref().read_dir() {
812        Ok(entries) => Some(entries),
813        Err(err) if err.kind() == std::io::ErrorKind::NotFound => None,
814        Err(err) => return Err(err),
815    };
816    Ok(entries
817        .into_iter()
818        .flatten()
819        .filter_map(|entry| match entry {
820            Ok(entry) => Some(entry),
821            Err(err) => {
822                warn!("Failed to read entry: {err}");
823                None
824            }
825        })
826        .filter(|entry| entry.file_type().is_ok_and(|file_type| file_type.is_dir()))
827        .map(|entry| entry.path()))
828}
829
830/// Iterate over the entries in a directory.
831///
832/// If the directory does not exist, returns an empty iterator.
833pub fn entries(path: impl AsRef<Path>) -> Result<impl Iterator<Item = PathBuf>, std::io::Error> {
834    let entries = match path.as_ref().read_dir() {
835        Ok(entries) => Some(entries),
836        Err(err) if err.kind() == std::io::ErrorKind::NotFound => None,
837        Err(err) => return Err(err),
838    };
839    Ok(entries
840        .into_iter()
841        .flatten()
842        .filter_map(|entry| match entry {
843            Ok(entry) => Some(entry),
844            Err(err) => {
845                warn!("Failed to read entry: {err}");
846                None
847            }
848        })
849        .map(|entry| entry.path()))
850}
851
852/// Iterate over the files in a directory.
853///
854/// If the directory does not exist, returns an empty iterator.
855pub fn files(path: impl AsRef<Path>) -> Result<impl Iterator<Item = PathBuf>, std::io::Error> {
856    let entries = match path.as_ref().read_dir() {
857        Ok(entries) => Some(entries),
858        Err(err) if err.kind() == std::io::ErrorKind::NotFound => None,
859        Err(err) => return Err(err),
860    };
861    Ok(entries
862        .into_iter()
863        .flatten()
864        .filter_map(|entry| match entry {
865            Ok(entry) => Some(entry),
866            Err(err) => {
867                warn!("Failed to read entry: {err}");
868                None
869            }
870        })
871        .filter(|entry| entry.file_type().is_ok_and(|file_type| file_type.is_file()))
872        .map(|entry| entry.path()))
873}
874
875/// Returns `true` if a path is a temporary file or directory.
876pub fn is_temporary(path: impl AsRef<Path>) -> bool {
877    path.as_ref()
878        .file_name()
879        .and_then(|name| name.to_str())
880        .is_some_and(|name| name.starts_with(".tmp"))
881}
882
883/// Checks if the grandparent directory of the given executable is the base
884/// of a virtual environment.
885///
886/// The procedure described in PEP 405 includes checking both the parent and
887/// grandparent directory of an executable, but in practice we've found this to
888/// be unnecessary.
889pub fn is_virtualenv_executable(executable: impl AsRef<Path>) -> bool {
890    executable
891        .as_ref()
892        .parent()
893        .and_then(Path::parent)
894        .is_some_and(is_virtualenv_base)
895}
896
897/// Returns `true` if a path is the base path of a virtual environment,
898/// indicated by the presence of a `pyvenv.cfg` file.
899///
900/// The procedure described in PEP 405 includes scanning `pyvenv.cfg`
901/// for a `home` key, but in practice we've found this to be
902/// unnecessary.
903pub fn is_virtualenv_base(path: impl AsRef<Path>) -> bool {
904    path.as_ref().join("pyvenv.cfg").is_file()
905}
906
907/// Whether the error is due to a lock being held.
908fn is_known_already_locked_error(err: &std::fs::TryLockError) -> bool {
909    match err {
910        std::fs::TryLockError::WouldBlock => true,
911        std::fs::TryLockError::Error(err) => {
912            // 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." }
913            if cfg!(windows) && err.raw_os_error() == Some(33) {
914                return true;
915            }
916            false
917        }
918    }
919}
920
921/// An asynchronous reader that reports progress as bytes are read.
922#[cfg(feature = "tokio")]
923pub struct ProgressReader<Reader: tokio::io::AsyncRead + Unpin, Callback: Fn(usize) + Unpin> {
924    reader: Reader,
925    callback: Callback,
926}
927
928#[cfg(feature = "tokio")]
929impl<Reader: tokio::io::AsyncRead + Unpin, Callback: Fn(usize) + Unpin>
930    ProgressReader<Reader, Callback>
931{
932    /// Create a new [`ProgressReader`] that wraps another reader.
933    pub fn new(reader: Reader, callback: Callback) -> Self {
934        Self { reader, callback }
935    }
936}
937
938#[cfg(feature = "tokio")]
939impl<Reader: tokio::io::AsyncRead + Unpin, Callback: Fn(usize) + Unpin> tokio::io::AsyncRead
940    for ProgressReader<Reader, Callback>
941{
942    fn poll_read(
943        mut self: std::pin::Pin<&mut Self>,
944        cx: &mut std::task::Context<'_>,
945        buf: &mut tokio::io::ReadBuf<'_>,
946    ) -> std::task::Poll<std::io::Result<()>> {
947        std::pin::Pin::new(&mut self.as_mut().reader)
948            .poll_read(cx, buf)
949            .map_ok(|()| {
950                (self.callback)(buf.filled().len());
951            })
952    }
953}
954
955/// Recursively copy a directory and its contents.
956pub fn copy_dir_all(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> std::io::Result<()> {
957    fs_err::create_dir_all(&dst)?;
958    for entry in fs_err::read_dir(src.as_ref())? {
959        let entry = entry?;
960        let ty = entry.file_type()?;
961        if ty.is_dir() {
962            copy_dir_all(entry.path(), dst.as_ref().join(entry.file_name()))?;
963        } else {
964            fs_err::copy(entry.path(), dst.as_ref().join(entry.file_name()))?;
965        }
966    }
967    Ok(())
968}
969
970/// Perform a safe removal of a virtual environment.
971///
972/// The link or file at `location` is removed without following it.
973pub fn remove_virtualenv(location: &Path) -> io::Result<()> {
974    if !fs_err::symlink_metadata(location)?.is_dir() {
975        return remove_symlink(location);
976    }
977
978    // On Windows, if the current executable is in the directory, defer self-deletion since Windows
979    // won't let you unlink a running executable.
980    #[cfg(windows)]
981    if let Ok(itself) = std::env::current_exe() {
982        let target = std::path::absolute(location)?;
983        if itself.starts_with(&target) {
984            debug!("Detected self-delete of executable: {}", itself.display());
985            self_replace::self_delete_outside_path(location)?;
986        }
987    }
988
989    // We defer removal of the `pyvenv.cfg` until the end, so if we fail to remove the environment,
990    // uv can still identify it as a Python virtual environment that can be deleted.
991    for entry in fs_err::read_dir(location)? {
992        let entry = entry?;
993        let path = entry.path();
994        if path == location.join("pyvenv.cfg") {
995            continue;
996        }
997        if path.is_dir() {
998            fs_err::remove_dir_all(&path)?;
999        } else {
1000            fs_err::remove_file(&path)?;
1001        }
1002    }
1003
1004    match fs_err::remove_file(location.join("pyvenv.cfg")) {
1005        Ok(()) => {}
1006        Err(err) if err.kind() == io::ErrorKind::NotFound => {}
1007        Err(err) => return Err(err),
1008    }
1009
1010    // Remove the virtual environment directory itself
1011    match fs_err::remove_dir_all(location) {
1012        Ok(()) => {}
1013        Err(err) if err.kind() == io::ErrorKind::NotFound => {}
1014        // If the virtual environment is a mounted file system, e.g., in a Docker container, we
1015        // cannot delete it — but that doesn't need to be a fatal error
1016        Err(err) if err.kind() == io::ErrorKind::ResourceBusy => {
1017            debug!(
1018                "Skipping removal of `{}` directory due to {err}",
1019                location.display(),
1020            );
1021        }
1022        Err(err) => return Err(err),
1023    }
1024
1025    Ok(())
1026}
1027
1028/// Prepare an empty virtual environment directory, resolving links when possible.
1029///
1030/// Returns whether an existing entry was found.
1031pub fn clear_virtualenv(location: &Path) -> io::Result<bool> {
1032    let location = location
1033        .canonicalize()
1034        .unwrap_or_else(|_| location.to_path_buf());
1035    let cleared = match remove_virtualenv(&location) {
1036        Ok(()) => true,
1037        Err(err) if err.kind() == io::ErrorKind::NotFound => false,
1038        Err(err) => return Err(err),
1039    };
1040    fs_err::create_dir_all(location)?;
1041    Ok(cleared)
1042}
1043
1044#[cfg(test)]
1045mod tests {
1046    use std::assert_matches;
1047
1048    use super::*;
1049
1050    #[test]
1051    fn remove_symlink_removes_directory_link_without_removing_target() -> io::Result<()> {
1052        let tempdir = tempfile::tempdir()?;
1053        let target = tempdir.path().join("target");
1054        fs_err::create_dir(&target)?;
1055        fs_err::write(target.join("file"), "content")?;
1056        let link = tempdir.path().join("link");
1057
1058        create_symlink(&target, &link)?;
1059        remove_symlink(&link)?;
1060
1061        assert_matches!(
1062            fs_err::symlink_metadata(&link),
1063            Err(err) if err.kind() == io::ErrorKind::NotFound
1064        );
1065        assert_eq!(fs_err::read_to_string(target.join("file"))?, "content");
1066        Ok(())
1067    }
1068
1069    #[test]
1070    fn remove_virtualenv_removes_directory_link_without_removing_target() -> io::Result<()> {
1071        let tempdir = tempfile::tempdir()?;
1072        let target = tempdir.path().join("target");
1073        fs_err::create_dir(&target)?;
1074        let marker = target.join("marker");
1075        fs_err::write(&marker, "")?;
1076        let environment = tempdir.path().join("environment");
1077        create_symlink(&target, &environment)?;
1078
1079        remove_virtualenv(&environment)?;
1080
1081        assert_matches!(
1082            fs_err::symlink_metadata(environment),
1083            Err(err) if err.kind() == io::ErrorKind::NotFound
1084        );
1085        assert!(marker.is_file());
1086        Ok(())
1087    }
1088
1089    #[test]
1090    fn clear_virtualenv_recreates_missing_directory() -> io::Result<()> {
1091        let tempdir = tempfile::tempdir()?;
1092        let environment = tempdir.path().join("environment");
1093
1094        assert!(!clear_virtualenv(&environment)?);
1095        assert!(environment.is_dir());
1096        Ok(())
1097    }
1098}