Skip to main content

uv_fs/
lib.rs

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