diff --git a/Cargo.lock b/Cargo.lock index 5f8513924..f4c3bdf25 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5443,6 +5443,7 @@ dependencies = [ "thiserror 2.0.12", "tracing", "uv-cache-info", + "uv-configuration", "uv-distribution-filename", "uv-fs", "uv-normalize", diff --git a/crates/uv-configuration/src/preview.rs b/crates/uv-configuration/src/preview.rs index 44ba6ef31..b8a776051 100644 --- a/crates/uv-configuration/src/preview.rs +++ b/crates/uv-configuration/src/preview.rs @@ -16,6 +16,7 @@ bitflags::bitflags! { const ADD_BOUNDS = 1 << 4; const PACKAGE_CONFLICTS = 1 << 5; const EXTRA_BUILD_DEPENDENCIES = 1 << 6; + const DETECT_MODULE_CONFLICTS = 1 << 7; } } @@ -32,6 +33,7 @@ impl PreviewFeatures { Self::ADD_BOUNDS => "add-bounds", Self::PACKAGE_CONFLICTS => "package-conflicts", Self::EXTRA_BUILD_DEPENDENCIES => "extra-build-dependencies", + Self::DETECT_MODULE_CONFLICTS => "detect-module-conflicts", _ => panic!("`flag_as_str` can only be used for exactly one feature flag"), } } @@ -76,6 +78,7 @@ impl FromStr for PreviewFeatures { "add-bounds" => Self::ADD_BOUNDS, "package-conflicts" => Self::PACKAGE_CONFLICTS, "extra-build-dependencies" => Self::EXTRA_BUILD_DEPENDENCIES, + "detect-module-conflicts" => Self::DETECT_MODULE_CONFLICTS, _ => { warn_user_once!("Unknown preview feature: `{part}`"); continue; @@ -246,6 +249,10 @@ mod tests { PreviewFeatures::EXTRA_BUILD_DEPENDENCIES.flag_as_str(), "extra-build-dependencies" ); + assert_eq!( + PreviewFeatures::DETECT_MODULE_CONFLICTS.flag_as_str(), + "detect-module-conflicts" + ); } #[test] diff --git a/crates/uv-dispatch/src/lib.rs b/crates/uv-dispatch/src/lib.rs index 12c116910..31b6b8b26 100644 --- a/crates/uv-dispatch/src/lib.rs +++ b/crates/uv-dispatch/src/lib.rs @@ -390,7 +390,7 @@ impl BuildContext for BuildDispatch<'_> { if wheels.len() == 1 { "" } else { "s" }, wheels.iter().map(ToString::to_string).join(", ") ); - wheels = Installer::new(venv) + wheels = Installer::new(venv, self.preview) .with_link_mode(self.link_mode) .with_cache(self.cache) .install(wheels) diff --git a/crates/uv-install-wheel/Cargo.toml b/crates/uv-install-wheel/Cargo.toml index d2d46ac40..06e72af39 100644 --- a/crates/uv-install-wheel/Cargo.toml +++ b/crates/uv-install-wheel/Cargo.toml @@ -22,6 +22,7 @@ name = "uv_install_wheel" [dependencies] uv-cache-info = { workspace = true } +uv-configuration = { workspace = true } uv-distribution-filename = { workspace = true } uv-fs = { workspace = true } uv-normalize = { workspace = true } diff --git a/crates/uv-install-wheel/src/linker.rs b/crates/uv-install-wheel/src/linker.rs index 168dfe3cc..68c58e459 100644 --- a/crates/uv-install-wheel/src/linker.rs +++ b/crates/uv-install-wheel/src/linker.rs @@ -10,20 +10,33 @@ use std::sync::{Arc, Mutex}; use std::time::SystemTime; use tempfile::tempdir_in; use tracing::{debug, instrument, trace}; +use uv_configuration::{Preview, PreviewFeatures}; use uv_distribution_filename::WheelFilename; use uv_fs::Simplified; use uv_warnings::{warn_user, warn_user_once}; use walkdir::WalkDir; +#[allow(clippy::struct_field_names)] #[derive(Debug, Default)] pub struct Locks { /// The parent directory of a file in a synchronized copy copy_dir_locks: Mutex>>>, /// Top level modules (excluding namespaces) we write to. modules: Mutex>, + /// Preview settings for feature flags. + preview: Preview, } impl Locks { + /// Create a new Locks instance with the given preview settings. + pub fn new(preview: Preview) -> Self { + Self { + copy_dir_locks: Mutex::new(FxHashMap::default()), + modules: Mutex::new(FxHashMap::default()), + preview, + } + } + /// Warn when a module exists in multiple packages. fn warn_module_conflict(&self, module: &OsStr, wheel_a: &WheelFilename) { if let Some(wheel_b) = self @@ -32,6 +45,14 @@ impl Locks { .unwrap() .insert(module.to_os_string(), wheel_a.clone()) { + // Only warn if the preview feature is enabled + if !self + .preview + .is_enabled(PreviewFeatures::DETECT_MODULE_CONFLICTS) + { + return; + } + // Sort for consistent output, at least with two packages let (wheel_a, wheel_b) = if wheel_b.name > wheel_a.name { (&wheel_b, wheel_a) diff --git a/crates/uv-installer/src/installer.rs b/crates/uv-installer/src/installer.rs index 3903784ca..70d785216 100644 --- a/crates/uv-installer/src/installer.rs +++ b/crates/uv-installer/src/installer.rs @@ -7,7 +7,7 @@ use tokio::sync::oneshot; use tracing::instrument; use uv_cache::Cache; -use uv_configuration::RAYON_INITIALIZE; +use uv_configuration::{Preview, RAYON_INITIALIZE}; use uv_distribution_types::CachedDist; use uv_install_wheel::{Layout, LinkMode}; use uv_python::PythonEnvironment; @@ -21,11 +21,13 @@ pub struct Installer<'a> { name: Option, /// The metadata associated with the [`Installer`]. metadata: bool, + /// Preview settings for the installer. + preview: Preview, } impl<'a> Installer<'a> { /// Initialize a new installer. - pub fn new(venv: &'a PythonEnvironment) -> Self { + pub fn new(venv: &'a PythonEnvironment, preview: Preview) -> Self { Self { venv, link_mode: LinkMode::default(), @@ -33,6 +35,7 @@ impl<'a> Installer<'a> { reporter: None, name: Some("uv".to_string()), metadata: true, + preview, } } @@ -88,6 +91,7 @@ impl<'a> Installer<'a> { reporter, name: installer_name, metadata: installer_metadata, + preview, } = self; if cache.is_some_and(Cache::is_temporary) { @@ -113,6 +117,7 @@ impl<'a> Installer<'a> { reporter.as_ref(), relocatable, installer_metadata, + preview, ); // This may fail if the main task was cancelled. @@ -143,6 +148,7 @@ impl<'a> Installer<'a> { self.reporter.as_ref(), self.venv.relocatable(), self.metadata, + self.preview, ) } } @@ -157,10 +163,11 @@ fn install( reporter: Option<&Arc>, relocatable: bool, installer_metadata: bool, + preview: Preview, ) -> Result> { // Initialize the threadpool with the user settings. LazyLock::force(&RAYON_INITIALIZE); - let locks = uv_install_wheel::Locks::default(); + let locks = uv_install_wheel::Locks::new(preview); wheels.par_iter().try_for_each(|wheel| { uv_install_wheel::install_wheel( layout, diff --git a/crates/uv/src/commands/pip/install.rs b/crates/uv/src/commands/pip/install.rs index a98845fee..ffba49c4c 100644 --- a/crates/uv/src/commands/pip/install.rs +++ b/crates/uv/src/commands/pip/install.rs @@ -611,6 +611,7 @@ pub(crate) async fn pip_install( installer_metadata, dry_run, printer, + preview, ) .await { diff --git a/crates/uv/src/commands/pip/operations.rs b/crates/uv/src/commands/pip/operations.rs index 710c7f6f6..b2e5e431c 100644 --- a/crates/uv/src/commands/pip/operations.rs +++ b/crates/uv/src/commands/pip/operations.rs @@ -450,6 +450,7 @@ pub(crate) async fn install( installer_metadata: bool, dry_run: DryRun, printer: Printer, + preview: uv_configuration::Preview, ) -> Result { let start = std::time::Instant::now(); @@ -571,7 +572,7 @@ pub(crate) async fn install( let mut installs = wheels.into_iter().chain(cached).collect::>(); if !installs.is_empty() { let start = std::time::Instant::now(); - installs = uv_installer::Installer::new(venv) + installs = uv_installer::Installer::new(venv, preview) .with_link_mode(link_mode) .with_cache(cache) .with_installer_metadata(installer_metadata) diff --git a/crates/uv/src/commands/pip/sync.rs b/crates/uv/src/commands/pip/sync.rs index 35cc485f1..0a124f00b 100644 --- a/crates/uv/src/commands/pip/sync.rs +++ b/crates/uv/src/commands/pip/sync.rs @@ -550,6 +550,7 @@ pub(crate) async fn pip_sync( installer_metadata, dry_run, printer, + preview, ) .await { diff --git a/crates/uv/src/commands/project/mod.rs b/crates/uv/src/commands/project/mod.rs index 279412ec4..47585ca84 100644 --- a/crates/uv/src/commands/project/mod.rs +++ b/crates/uv/src/commands/project/mod.rs @@ -2173,6 +2173,7 @@ pub(crate) async fn sync_environment( installer_metadata, dry_run, printer, + preview, ) .await?; @@ -2434,6 +2435,7 @@ pub(crate) async fn update_environment( installer_metadata, dry_run, printer, + preview, ) .await?; diff --git a/crates/uv/src/commands/project/sync.rs b/crates/uv/src/commands/project/sync.rs index 7cc8a2aad..ea1fa359f 100644 --- a/crates/uv/src/commands/project/sync.rs +++ b/crates/uv/src/commands/project/sync.rs @@ -809,6 +809,7 @@ pub(super) async fn do_sync( installer_metadata, dry_run, printer, + preview, ) .await?; diff --git a/crates/uv/tests/it/pip_install.rs b/crates/uv/tests/it/pip_install.rs index 1ac8eb22e..2103abc1d 100644 --- a/crates/uv/tests/it/pip_install.rs +++ b/crates/uv/tests/it/pip_install.rs @@ -12486,7 +12486,7 @@ fn overlapping_packages_warning() -> Result<()> { .child("__init__.py") .touch()?; - // Check that overlapping packages show a warning + // Check that overlapping packages don't show a warning by default uv_snapshot!(context.filters(), context.pip_install() .arg("--no-deps") .arg(&built_by_uv) @@ -12495,6 +12495,29 @@ fn overlapping_packages_warning() -> Result<()> { exit_code: 0 ----- stdout ----- + ----- stderr ----- + Resolved 2 packages in [TIME] + Prepared 2 packages in [TIME] + Installed 2 packages in [TIME] + + also-built-by-uv==0.1.0 (from file://[TEMP_DIR]/also-built-by-uv) + + built-by-uv==0.1.0 (from file://[WORKSPACE]/scripts/packages/built-by-uv) + " + ); + + // Clean up for the next test + context.venv().arg("--clear").assert().success(); + + // Check that overlapping packages show a warning when preview feature is enabled + uv_snapshot!(context.filters(), context.pip_install() + .arg("--no-deps") + .arg("--preview-features") + .arg("detect-module-conflicts") + .arg(&built_by_uv) + .arg(also_build_by_uv.path()), @r" + success: true + exit_code: 0 + ----- stdout ----- + ----- stderr ----- Resolved 2 packages in [TIME] Prepared 2 packages in [TIME] diff --git a/crates/uv/tests/it/show_settings.rs b/crates/uv/tests/it/show_settings.rs index 0af0db387..bf3d88924 100644 --- a/crates/uv/tests/it/show_settings.rs +++ b/crates/uv/tests/it/show_settings.rs @@ -7720,7 +7720,7 @@ fn preview_features() { show_settings: true, preview: Preview { flags: PreviewFeatures( - PYTHON_INSTALL_DEFAULT | PYTHON_UPGRADE | JSON_OUTPUT | PYLOCK | ADD_BOUNDS | PACKAGE_CONFLICTS | EXTRA_BUILD_DEPENDENCIES, + PYTHON_INSTALL_DEFAULT | PYTHON_UPGRADE | JSON_OUTPUT | PYLOCK | ADD_BOUNDS | PACKAGE_CONFLICTS | EXTRA_BUILD_DEPENDENCIES | DETECT_MODULE_CONFLICTS, ), }, python_preference: Managed, @@ -7946,7 +7946,7 @@ fn preview_features() { show_settings: true, preview: Preview { flags: PreviewFeatures( - PYTHON_INSTALL_DEFAULT | PYTHON_UPGRADE | JSON_OUTPUT | PYLOCK | ADD_BOUNDS | PACKAGE_CONFLICTS | EXTRA_BUILD_DEPENDENCIES, + PYTHON_INSTALL_DEFAULT | PYTHON_UPGRADE | JSON_OUTPUT | PYLOCK | ADD_BOUNDS | PACKAGE_CONFLICTS | EXTRA_BUILD_DEPENDENCIES | DETECT_MODULE_CONFLICTS, ), }, python_preference: Managed,