diff --git a/crates/install-wheel-rs/src/linker.rs b/crates/install-wheel-rs/src/linker.rs index 3db66e1fa..923270670 100644 --- a/crates/install-wheel-rs/src/linker.rs +++ b/crates/install-wheel-rs/src/linker.rs @@ -3,6 +3,7 @@ use std::path::Path; use std::str::FromStr; +use std::time::SystemTime; use fs_err as fs; use fs_err::{DirEntry, File}; @@ -263,6 +264,31 @@ fn clone_wheel_files( count += 1; } + // The directory mtime is not updated when cloning and the mtime is used by CPython's + // import mechanisms to determine if it should look for new packages in a directory. + // Here, we force the mtime to be updated to ensure that packages are importable without + // manual cache invalidation. + // + // + let now = SystemTime::now(); + + // `File.set_modified` is not available in `fs_err` yet + #[allow(clippy::disallowed_types)] + match std::fs::File::open(site_packages.as_ref()) { + Ok(dir) => { + if let Err(err) = dir.set_modified(now) { + debug!( + "Failed to update mtime for {}: {err}", + site_packages.as_ref().display() + ); + } + } + Err(err) => debug!( + "Failed to open {} to update mtime: {err}", + site_packages.as_ref().display() + ), + } + Ok(count) } diff --git a/crates/uv/tests/pip_install.rs b/crates/uv/tests/pip_install.rs index 05819d851..155c20a49 100644 --- a/crates/uv/tests/pip_install.rs +++ b/crates/uv/tests/pip_install.rs @@ -1,13 +1,12 @@ #![cfg(all(feature = "python", feature = "pypi"))] -use std::process::Command; - use anyhow::Result; use assert_cmd::prelude::*; use assert_fs::prelude::*; use base64::{prelude::BASE64_STANDARD as base64, Engine}; use indoc::indoc; use itertools::Itertools; +use std::process::Command; use url::Url; use common::{uv_snapshot, TestContext, EXCLUDE_NEWER, INSTA_FILTERS}; @@ -2843,3 +2842,48 @@ fn install_index_with_relative_links_authenticated() { context.assert_command("import anyio").success(); } + +/// The modified time of `site-packages` should change on package installation. +#[cfg(unix)] +#[test] +fn install_site_packages_mtime_updated() -> Result<()> { + use std::os::unix::fs::MetadataExt; + + let context = TestContext::new("3.12"); + + let site_packages = context.site_packages(); + + // `mtime` is only second-resolution so we include the nanoseconds as well + let metadata = site_packages.metadata()?; + let pre_mtime = metadata.mtime(); + let pre_mtime_ns = metadata.mtime_nsec(); + + // Install a package. + uv_snapshot!(command(&context) + .arg("anyio") + .arg("--strict"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 3 packages in [TIME] + Downloaded 3 packages in [TIME] + Installed 3 packages in [TIME] + + anyio==4.0.0 + + idna==3.4 + + sniffio==1.3.0 + "### + ); + + let metadata = site_packages.metadata()?; + let post_mtime = metadata.mtime(); + let post_mtime_ns = metadata.mtime_nsec(); + + assert!( + (post_mtime, post_mtime_ns) > (pre_mtime, pre_mtime_ns), + "Expected newer mtime than {pre_mtime}.{pre_mtime_ns} but got {post_mtime}.{post_mtime_ns}" + ); + + Ok(()) +}