From e724ddc63f14b9378672c16433dbfba534c6cb84 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Thu, 17 Jul 2025 21:27:54 -0400 Subject: [PATCH] Allow `--config-settings-package` to apply configuration settings at the package level (#14573) ## Summary Closes https://github.com/astral-sh/uv/issues/14564. Closes https://github.com/astral-sh/uv/issues/10940. --- crates/uv-bench/benches/uv.rs | 6 +- crates/uv-cli/src/lib.rs | 37 +++- crates/uv-cli/src/options.rs | 32 ++- .../uv-configuration/src/config_settings.rs | 184 ++++++++++++++++++ crates/uv-dispatch/src/lib.rs | 25 ++- .../src/index/built_wheel_index.rs | 42 ++-- crates/uv-distribution/src/source/mod.rs | 51 +++-- crates/uv-installer/src/plan.rs | 11 +- crates/uv-settings/src/combine.rs | 15 +- crates/uv-settings/src/settings.rs | 34 +++- crates/uv-types/src/traits.rs | 7 +- crates/uv/src/commands/build_frontend.rs | 6 +- crates/uv/src/commands/pip/compile.rs | 5 +- crates/uv/src/commands/pip/install.rs | 6 +- crates/uv/src/commands/pip/operations.rs | 4 +- crates/uv/src/commands/pip/sync.rs | 6 +- crates/uv/src/commands/project/add.rs | 1 + crates/uv/src/commands/project/lock.rs | 2 + crates/uv/src/commands/project/mod.rs | 10 + crates/uv/src/commands/project/sync.rs | 3 + crates/uv/src/commands/project/tree.rs | 1 + crates/uv/src/commands/venv.rs | 4 +- crates/uv/src/lib.rs | 3 + crates/uv/src/settings.rs | 22 ++- crates/uv/tests/it/pip_install.rs | 119 ++++++++++- crates/uv/tests/it/show_settings.rs | 108 +++++++++- crates/uv/tests/it/sync.rs | 143 ++++++++++++++ docs/reference/cli.md | 15 ++ docs/reference/settings.md | 54 +++++ uv.schema.json | 29 +++ 30 files changed, 927 insertions(+), 58 deletions(-) diff --git a/crates/uv-bench/benches/uv.rs b/crates/uv-bench/benches/uv.rs index 9bdd7adb9..8380ccd60 100644 --- a/crates/uv-bench/benches/uv.rs +++ b/crates/uv-bench/benches/uv.rs @@ -86,8 +86,8 @@ mod resolver { use uv_cache::Cache; use uv_client::RegistryClient; use uv_configuration::{ - BuildOptions, Concurrency, ConfigSettings, Constraints, IndexStrategy, PreviewMode, - SourceStrategy, + BuildOptions, Concurrency, ConfigSettings, Constraints, IndexStrategy, + PackageConfigSettings, PreviewMode, SourceStrategy, }; use uv_dispatch::{BuildDispatch, SharedState}; use uv_distribution::DistributionDatabase; @@ -144,6 +144,7 @@ mod resolver { let build_options = BuildOptions::default(); let concurrency = Concurrency::default(); let config_settings = ConfigSettings::default(); + let config_settings_package = PackageConfigSettings::default(); let exclude_newer = Some( jiff::civil::date(2024, 9, 1) .to_zoned(jiff::tz::TimeZone::UTC) @@ -184,6 +185,7 @@ mod resolver { state, IndexStrategy::default(), &config_settings, + &config_settings_package, build_isolation, LinkMode::default(), &build_options, diff --git a/crates/uv-cli/src/lib.rs b/crates/uv-cli/src/lib.rs index 9d7cfa6e0..d6560014f 100644 --- a/crates/uv-cli/src/lib.rs +++ b/crates/uv-cli/src/lib.rs @@ -10,8 +10,9 @@ use clap::{Args, Parser, Subcommand}; use uv_cache::CacheArgs; use uv_configuration::{ - ConfigSettingEntry, ExportFormat, IndexStrategy, KeyringProviderType, PackageNameSpecifier, - ProjectBuildBackend, TargetTriple, TrustedHost, TrustedPublishing, VersionControlSystem, + ConfigSettingEntry, ConfigSettingPackageEntry, ExportFormat, IndexStrategy, + KeyringProviderType, PackageNameSpecifier, ProjectBuildBackend, TargetTriple, TrustedHost, + TrustedPublishing, VersionControlSystem, }; use uv_distribution_types::{Index, IndexUrl, Origin, PipExtraIndex, PipFindLinks, PipIndex}; use uv_normalize::{ExtraName, GroupName, PackageName, PipGroupName}; @@ -4693,6 +4694,14 @@ pub struct ToolUpgradeArgs { )] pub config_setting: Option>, + /// Settings to pass to the PEP 517 build backend for a specific package, specified as `PACKAGE:KEY=VALUE` pairs. + #[arg( + long, + alias = "config-settings-package", + help_heading = "Build options" + )] + pub config_setting_package: Option>, + /// Disable isolation when building source distributions. /// /// Assumes that build dependencies specified by PEP 518 are already installed. @@ -5484,6 +5493,14 @@ pub struct InstallerArgs { )] pub config_setting: Option>, + /// Settings to pass to the PEP 517 build backend for a specific package, specified as `PACKAGE:KEY=VALUE` pairs. + #[arg( + long, + alias = "config-settings-package", + help_heading = "Build options" + )] + pub config_settings_package: Option>, + /// Disable isolation when building source distributions. /// /// Assumes that build dependencies specified by PEP 518 are already installed. @@ -5671,6 +5688,14 @@ pub struct ResolverArgs { )] pub config_setting: Option>, + /// Settings to pass to the PEP 517 build backend for a specific package, specified as `PACKAGE:KEY=VALUE` pairs. + #[arg( + long, + alias = "config-settings-package", + help_heading = "Build options" + )] + pub config_settings_package: Option>, + /// Disable isolation when building source distributions. /// /// Assumes that build dependencies specified by PEP 518 are already installed. @@ -5860,6 +5885,14 @@ pub struct ResolverInstallerArgs { )] pub config_setting: Option>, + /// Settings to pass to the PEP 517 build backend for a specific package, specified as `PACKAGE:KEY=VALUE` pairs. + #[arg( + long, + alias = "config-settings-package", + help_heading = "Build options" + )] + pub config_settings_package: Option>, + /// Disable isolation when building source distributions. /// /// Assumes that build dependencies specified by PEP 518 are already installed. diff --git a/crates/uv-cli/src/options.rs b/crates/uv-cli/src/options.rs index f522022a1..d2e651a19 100644 --- a/crates/uv-cli/src/options.rs +++ b/crates/uv-cli/src/options.rs @@ -1,7 +1,7 @@ use anstream::eprintln; use uv_cache::Refresh; -use uv_configuration::ConfigSettings; +use uv_configuration::{ConfigSettings, PackageConfigSettings}; use uv_resolver::PrereleaseMode; use uv_settings::{Combine, PipOptions, ResolverInstallerOptions, ResolverOptions}; use uv_warnings::owo_colors::OwoColorize; @@ -62,6 +62,7 @@ impl From for PipOptions { pre, fork_strategy, config_setting, + config_settings_package, no_build_isolation, no_build_isolation_package, build_isolation, @@ -84,6 +85,11 @@ impl From for PipOptions { }, config_settings: config_setting .map(|config_settings| config_settings.into_iter().collect::()), + config_settings_package: config_settings_package.map(|config_settings| { + config_settings + .into_iter() + .collect::() + }), no_build_isolation: flag(no_build_isolation, build_isolation, "build-isolation"), no_build_isolation_package: Some(no_build_isolation_package), exclude_newer, @@ -104,6 +110,7 @@ impl From for PipOptions { index_strategy, keyring_provider, config_setting, + config_settings_package, no_build_isolation, build_isolation, exclude_newer, @@ -120,6 +127,11 @@ impl From for PipOptions { keyring_provider, config_settings: config_setting .map(|config_settings| config_settings.into_iter().collect::()), + config_settings_package: config_settings_package.map(|config_settings| { + config_settings + .into_iter() + .collect::() + }), no_build_isolation: flag(no_build_isolation, build_isolation, "build-isolation"), exclude_newer, link_mode, @@ -147,6 +159,7 @@ impl From for PipOptions { pre, fork_strategy, config_setting, + config_settings_package, no_build_isolation, no_build_isolation_package, build_isolation, @@ -173,6 +186,11 @@ impl From for PipOptions { fork_strategy, config_settings: config_setting .map(|config_settings| config_settings.into_iter().collect::()), + config_settings_package: config_settings_package.map(|config_settings| { + config_settings + .into_iter() + .collect::() + }), no_build_isolation: flag(no_build_isolation, build_isolation, "build-isolation"), no_build_isolation_package: Some(no_build_isolation_package), exclude_newer, @@ -260,6 +278,7 @@ pub fn resolver_options( pre, fork_strategy, config_setting, + config_settings_package, no_build_isolation, no_build_isolation_package, build_isolation, @@ -321,6 +340,11 @@ pub fn resolver_options( dependency_metadata: None, config_settings: config_setting .map(|config_settings| config_settings.into_iter().collect::()), + config_settings_package: config_settings_package.map(|config_settings| { + config_settings + .into_iter() + .collect::() + }), no_build_isolation: flag(no_build_isolation, build_isolation, "build-isolation"), no_build_isolation_package: Some(no_build_isolation_package), exclude_newer, @@ -353,6 +377,7 @@ pub fn resolver_installer_options( pre, fork_strategy, config_setting, + config_settings_package, no_build_isolation, no_build_isolation_package, build_isolation, @@ -428,6 +453,11 @@ pub fn resolver_installer_options( dependency_metadata: None, config_settings: config_setting .map(|config_settings| config_settings.into_iter().collect::()), + config_settings_package: config_settings_package.map(|config_settings| { + config_settings + .into_iter() + .collect::() + }), no_build_isolation: flag(no_build_isolation, build_isolation, "build-isolation"), no_build_isolation_package: if no_build_isolation_package.is_empty() { None diff --git a/crates/uv-configuration/src/config_settings.rs b/crates/uv-configuration/src/config_settings.rs index cd1d67196..c6238deb2 100644 --- a/crates/uv-configuration/src/config_settings.rs +++ b/crates/uv-configuration/src/config_settings.rs @@ -3,6 +3,7 @@ use std::{ str::FromStr, }; use uv_cache_key::CacheKeyHasher; +use uv_normalize::PackageName; #[derive(Debug, Clone)] pub struct ConfigSettingEntry { @@ -28,6 +29,32 @@ impl FromStr for ConfigSettingEntry { } } +#[derive(Debug, Clone)] +pub struct ConfigSettingPackageEntry { + /// The package name to apply the setting to. + package: PackageName, + /// The config setting entry. + setting: ConfigSettingEntry, +} + +impl FromStr for ConfigSettingPackageEntry { + type Err = String; + + fn from_str(s: &str) -> Result { + let Some((package_str, config_str)) = s.split_once(':') else { + return Err(format!( + "Invalid config setting: {s} (expected `PACKAGE:KEY=VALUE`)" + )); + }; + + let package = PackageName::from_str(package_str.trim()) + .map_err(|e| format!("Invalid package name: {e}"))?; + let setting = ConfigSettingEntry::from_str(config_str)?; + + Ok(Self { package, setting }) + } +} + #[derive(Debug, Clone, PartialEq, Eq)] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema), schemars(untagged))] enum ConfigSettingValue { @@ -212,6 +239,111 @@ impl<'de> serde::Deserialize<'de> for ConfigSettings { } } +/// Settings to pass to PEP 517 build backends on a per-package basis. +#[derive(Debug, Default, Clone, PartialEq, Eq)] +#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +pub struct PackageConfigSettings(BTreeMap); + +impl FromIterator for PackageConfigSettings { + fn from_iter>(iter: T) -> Self { + let mut package_configs: BTreeMap> = BTreeMap::new(); + + for entry in iter { + package_configs + .entry(entry.package) + .or_default() + .push(entry.setting); + } + + let configs = package_configs + .into_iter() + .map(|(package, entries)| (package, entries.into_iter().collect())) + .collect(); + + Self(configs) + } +} + +impl PackageConfigSettings { + /// Returns the config settings for a specific package, if any. + pub fn get(&self, package: &PackageName) -> Option<&ConfigSettings> { + self.0.get(package) + } + + /// Returns `true` if there are no package-specific settings. + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } + + /// Merge two sets of package config settings, with the values in `self` taking precedence. + #[must_use] + pub fn merge(mut self, other: PackageConfigSettings) -> PackageConfigSettings { + for (package, settings) in other.0 { + match self.0.entry(package) { + Entry::Vacant(vacant) => { + vacant.insert(settings); + } + Entry::Occupied(mut occupied) => { + let merged = occupied.get().clone().merge(settings); + occupied.insert(merged); + } + } + } + self + } +} + +impl uv_cache_key::CacheKey for PackageConfigSettings { + fn cache_key(&self, state: &mut CacheKeyHasher) { + for (package, settings) in &self.0 { + package.to_string().cache_key(state); + settings.cache_key(state); + } + } +} + +impl serde::Serialize for PackageConfigSettings { + fn serialize(&self, serializer: S) -> Result { + use serde::ser::SerializeMap; + + let mut map = serializer.serialize_map(Some(self.0.len()))?; + for (key, value) in &self.0 { + map.serialize_entry(&key.to_string(), value)?; + } + map.end() + } +} + +impl<'de> serde::Deserialize<'de> for PackageConfigSettings { + fn deserialize>(deserializer: D) -> Result { + struct Visitor; + + impl<'de> serde::de::Visitor<'de> for Visitor { + type Value = PackageConfigSettings; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("a map from package name to config settings") + } + + fn visit_map>( + self, + mut map: A, + ) -> Result { + let mut config = BTreeMap::default(); + while let Some((key, value)) = map.next_entry::()? { + let package = PackageName::from_str(&key).map_err(|e| { + serde::de::Error::custom(format!("Invalid package name: {e}")) + })?; + config.insert(package, value); + } + Ok(PackageConfigSettings(config)) + } + } + + deserializer.deserialize_map(Visitor) + } +} + #[cfg(test)] mod tests { use super::*; @@ -291,4 +423,56 @@ mod tests { ); assert_eq!(settings.escape_for_python(), r#"{"key":"val\\1 {}value"}"#); } + + #[test] + fn parse_config_setting_package_entry() { + // Test valid parsing + let entry = ConfigSettingPackageEntry::from_str("numpy:editable_mode=compat").unwrap(); + assert_eq!(entry.package.as_ref(), "numpy"); + assert_eq!(entry.setting.key, "editable_mode"); + assert_eq!(entry.setting.value, "compat"); + + // Test with package name containing hyphens + let entry = ConfigSettingPackageEntry::from_str("my-package:some_key=value").unwrap(); + assert_eq!(entry.package.as_ref(), "my-package"); + assert_eq!(entry.setting.key, "some_key"); + assert_eq!(entry.setting.value, "value"); + + // Test with spaces around values + let entry = ConfigSettingPackageEntry::from_str(" numpy : key = value ").unwrap(); + assert_eq!(entry.package.as_ref(), "numpy"); + assert_eq!(entry.setting.key, "key"); + assert_eq!(entry.setting.value, "value"); + } + + #[test] + fn collect_config_settings_package() { + let settings: PackageConfigSettings = vec![ + ConfigSettingPackageEntry::from_str("numpy:editable_mode=compat").unwrap(), + ConfigSettingPackageEntry::from_str("numpy:another_key=value").unwrap(), + ConfigSettingPackageEntry::from_str("scipy:build_option=fast").unwrap(), + ] + .into_iter() + .collect(); + + let numpy_settings = settings + .get(&PackageName::from_str("numpy").unwrap()) + .unwrap(); + assert_eq!( + numpy_settings.0.get("editable_mode"), + Some(&ConfigSettingValue::String("compat".to_string())) + ); + assert_eq!( + numpy_settings.0.get("another_key"), + Some(&ConfigSettingValue::String("value".to_string())) + ); + + let scipy_settings = settings + .get(&PackageName::from_str("scipy").unwrap()) + .unwrap(); + assert_eq!( + scipy_settings.0.get("build_option"), + Some(&ConfigSettingValue::String("fast".to_string())) + ); + } } diff --git a/crates/uv-dispatch/src/lib.rs b/crates/uv-dispatch/src/lib.rs index 874e412e5..2e34b583d 100644 --- a/crates/uv-dispatch/src/lib.rs +++ b/crates/uv-dispatch/src/lib.rs @@ -17,8 +17,8 @@ use uv_build_frontend::{SourceBuild, SourceBuildContext}; use uv_cache::Cache; use uv_client::RegistryClient; use uv_configuration::{ - BuildKind, BuildOptions, ConfigSettings, Constraints, IndexStrategy, PreviewMode, Reinstall, - SourceStrategy, + BuildKind, BuildOptions, ConfigSettings, Constraints, IndexStrategy, PackageConfigSettings, + PreviewMode, Reinstall, SourceStrategy, }; use uv_configuration::{BuildOutput, Concurrency}; use uv_distribution::DistributionDatabase; @@ -91,6 +91,7 @@ pub struct BuildDispatch<'a> { link_mode: uv_install_wheel::LinkMode, build_options: &'a BuildOptions, config_settings: &'a ConfigSettings, + config_settings_package: &'a PackageConfigSettings, hasher: &'a HashStrategy, exclude_newer: Option, source_build_context: SourceBuildContext, @@ -113,6 +114,7 @@ impl<'a> BuildDispatch<'a> { shared_state: SharedState, index_strategy: IndexStrategy, config_settings: &'a ConfigSettings, + config_settings_package: &'a PackageConfigSettings, build_isolation: BuildIsolation<'a>, link_mode: uv_install_wheel::LinkMode, build_options: &'a BuildOptions, @@ -134,6 +136,7 @@ impl<'a> BuildDispatch<'a> { dependency_metadata, index_strategy, config_settings, + config_settings_package, build_isolation, link_mode, build_options, @@ -200,6 +203,10 @@ impl BuildContext for BuildDispatch<'_> { self.config_settings } + fn config_settings_package(&self) -> &PackageConfigSettings { + self.config_settings_package + } + fn sources(&self) -> SourceStrategy { self.sources } @@ -295,6 +302,7 @@ impl BuildContext for BuildDispatch<'_> { self.hasher, self.index_locations, self.config_settings, + self.config_settings_package, self.cache(), venv, tags, @@ -418,6 +426,17 @@ impl BuildContext for BuildDispatch<'_> { build_stack.insert(dist.distribution_id()); } + // Get package-specific config settings if available; otherwise, use global settings. + let config_settings = if let Some(name) = dist_name { + if let Some(package_settings) = self.config_settings_package.get(name) { + package_settings.clone().merge(self.config_settings.clone()) + } else { + self.config_settings.clone() + } + } else { + self.config_settings.clone() + }; + let builder = SourceBuild::setup( source, subdirectory, @@ -431,7 +450,7 @@ impl BuildContext for BuildDispatch<'_> { self.index_locations, sources, self.workspace_cache(), - self.config_settings.clone(), + config_settings, self.build_isolation, &build_stack, build_kind, diff --git a/crates/uv-distribution/src/index/built_wheel_index.rs b/crates/uv-distribution/src/index/built_wheel_index.rs index 9752e7e4f..90ce5deed 100644 --- a/crates/uv-distribution/src/index/built_wheel_index.rs +++ b/crates/uv-distribution/src/index/built_wheel_index.rs @@ -1,10 +1,12 @@ +use std::borrow::Cow; use uv_cache::{Cache, CacheBucket, CacheShard, WheelCache}; use uv_cache_info::CacheInfo; use uv_cache_key::cache_digest; -use uv_configuration::ConfigSettings; +use uv_configuration::{ConfigSettings, PackageConfigSettings}; use uv_distribution_types::{ DirectUrlSourceDist, DirectorySourceDist, GitSourceDist, Hashed, PathSourceDist, }; +use uv_normalize::PackageName; use uv_platform_tags::Tags; use uv_types::HashStrategy; @@ -18,7 +20,8 @@ pub struct BuiltWheelIndex<'a> { cache: &'a Cache, tags: &'a Tags, hasher: &'a HashStrategy, - build_configuration: &'a ConfigSettings, + config_settings: &'a ConfigSettings, + config_settings_package: &'a PackageConfigSettings, } impl<'a> BuiltWheelIndex<'a> { @@ -27,13 +30,15 @@ impl<'a> BuiltWheelIndex<'a> { cache: &'a Cache, tags: &'a Tags, hasher: &'a HashStrategy, - build_configuration: &'a ConfigSettings, + config_settings: &'a ConfigSettings, + config_settings_package: &'a PackageConfigSettings, ) -> Self { Self { cache, tags, hasher, - build_configuration, + config_settings, + config_settings_package, } } @@ -63,10 +68,11 @@ impl<'a> BuiltWheelIndex<'a> { let cache_shard = cache_shard.shard(revision.id()); // If there are build settings, we need to scope to a cache shard. - let cache_shard = if self.build_configuration.is_empty() { + let config_settings = self.config_settings_for(&source_dist.name); + let cache_shard = if config_settings.is_empty() { cache_shard } else { - cache_shard.shard(cache_digest(self.build_configuration)) + cache_shard.shard(cache_digest(&config_settings)) }; Ok(self.find(&cache_shard)) @@ -100,10 +106,11 @@ impl<'a> BuiltWheelIndex<'a> { let cache_shard = cache_shard.shard(revision.id()); // If there are build settings, we need to scope to a cache shard. - let cache_shard = if self.build_configuration.is_empty() { + let config_settings = self.config_settings_for(&source_dist.name); + let cache_shard = if config_settings.is_empty() { cache_shard } else { - cache_shard.shard(cache_digest(self.build_configuration)) + cache_shard.shard(cache_digest(&config_settings)) }; Ok(self @@ -148,10 +155,11 @@ impl<'a> BuiltWheelIndex<'a> { let cache_shard = cache_shard.shard(revision.id()); // If there are build settings, we need to scope to a cache shard. - let cache_shard = if self.build_configuration.is_empty() { + let config_settings = self.config_settings_for(&source_dist.name); + let cache_shard = if config_settings.is_empty() { cache_shard } else { - cache_shard.shard(cache_digest(self.build_configuration)) + cache_shard.shard(cache_digest(&config_settings)) }; Ok(self @@ -174,10 +182,11 @@ impl<'a> BuiltWheelIndex<'a> { ); // If there are build settings, we need to scope to a cache shard. - let cache_shard = if self.build_configuration.is_empty() { + let config_settings = self.config_settings_for(&source_dist.name); + let cache_shard = if config_settings.is_empty() { cache_shard } else { - cache_shard.shard(cache_digest(self.build_configuration)) + cache_shard.shard(cache_digest(&config_settings)) }; self.find(&cache_shard) @@ -239,4 +248,13 @@ impl<'a> BuiltWheelIndex<'a> { candidate } + + /// Determine the [`ConfigSettings`] for the given package name. + fn config_settings_for(&self, name: &PackageName) -> Cow<'_, ConfigSettings> { + if let Some(package_settings) = self.config_settings_package.get(name) { + Cow::Owned(package_settings.clone().merge(self.config_settings.clone())) + } else { + Cow::Borrowed(self.config_settings) + } + } } diff --git a/crates/uv-distribution/src/source/mod.rs b/crates/uv-distribution/src/source/mod.rs index 1308e3d77..080a1e52d 100644 --- a/crates/uv-distribution/src/source/mod.rs +++ b/crates/uv-distribution/src/source/mod.rs @@ -29,7 +29,7 @@ use uv_cache_key::cache_digest; use uv_client::{ CacheControl, CachedClientError, Connectivity, DataWithCachePolicy, RegistryClient, }; -use uv_configuration::{BuildKind, BuildOutput, SourceStrategy}; +use uv_configuration::{BuildKind, BuildOutput, ConfigSettings, SourceStrategy}; use uv_distribution_filename::{SourceDistExtension, WheelFilename}; use uv_distribution_types::{ BuildableSource, DirectorySourceUrl, GitSourceUrl, HashPolicy, Hashed, PathSourceUrl, @@ -373,6 +373,23 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { Ok(metadata) } + /// Determine the [`ConfigSettings`] for the given package name. + fn config_settings_for(&self, name: Option<&PackageName>) -> Cow<'_, ConfigSettings> { + if let Some(name) = name { + if let Some(package_settings) = self.build_context.config_settings_package().get(name) { + Cow::Owned( + package_settings + .clone() + .merge(self.build_context.config_settings().clone()), + ) + } else { + Cow::Borrowed(self.build_context.config_settings()) + } + } else { + Cow::Borrowed(self.build_context.config_settings()) + } + } + /// Build a source distribution from a remote URL. async fn url<'data>( &self, @@ -407,11 +424,11 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { let source_dist_entry = cache_shard.entry(SOURCE); // If there are build settings, we need to scope to a cache shard. - let config_settings = self.build_context.config_settings(); + let config_settings = self.config_settings_for(source.name()); let cache_shard = if config_settings.is_empty() { cache_shard } else { - cache_shard.shard(cache_digest(config_settings)) + cache_shard.shard(cache_digest(&&config_settings)) }; // If the cache contains a compatible wheel, return it. @@ -580,11 +597,11 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { } // If there are build settings, we need to scope to a cache shard. - let config_settings = self.build_context.config_settings(); + let config_settings = self.config_settings_for(source.name()); let cache_shard = if config_settings.is_empty() { cache_shard } else { - cache_shard.shard(cache_digest(config_settings)) + cache_shard.shard(cache_digest(&config_settings)) }; // Otherwise, we either need to build the metadata. @@ -779,11 +796,11 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { let source_entry = cache_shard.entry(SOURCE); // If there are build settings, we need to scope to a cache shard. - let config_settings = self.build_context.config_settings(); + let config_settings = self.config_settings_for(source.name()); let cache_shard = if config_settings.is_empty() { cache_shard } else { - cache_shard.shard(cache_digest(config_settings)) + cache_shard.shard(cache_digest(&config_settings)) }; // If the cache contains a compatible wheel, return it. @@ -941,11 +958,11 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { } // If there are build settings, we need to scope to a cache shard. - let config_settings = self.build_context.config_settings(); + let config_settings = self.config_settings_for(source.name()); let cache_shard = if config_settings.is_empty() { cache_shard } else { - cache_shard.shard(cache_digest(config_settings)) + cache_shard.shard(cache_digest(&config_settings)) }; // Otherwise, we need to build a wheel. @@ -1083,11 +1100,11 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { let cache_shard = cache_shard.shard(revision.id()); // If there are build settings, we need to scope to a cache shard. - let config_settings = self.build_context.config_settings(); + let config_settings = self.config_settings_for(source.name()); let cache_shard = if config_settings.is_empty() { cache_shard } else { - cache_shard.shard(cache_digest(config_settings)) + cache_shard.shard(cache_digest(&config_settings)) }; // If the cache contains a compatible wheel, return it. @@ -1271,11 +1288,11 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { } // If there are build settings, we need to scope to a cache shard. - let config_settings = self.build_context.config_settings(); + let config_settings = self.config_settings_for(source.name()); let cache_shard = if config_settings.is_empty() { cache_shard } else { - cache_shard.shard(cache_digest(config_settings)) + cache_shard.shard(cache_digest(&config_settings)) }; // Otherwise, we need to build a wheel. @@ -1476,11 +1493,11 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { let _lock = cache_shard.lock().await.map_err(Error::CacheWrite)?; // If there are build settings, we need to scope to a cache shard. - let config_settings = self.build_context.config_settings(); + let config_settings = self.config_settings_for(source.name()); let cache_shard = if config_settings.is_empty() { cache_shard } else { - cache_shard.shard(cache_digest(config_settings)) + cache_shard.shard(cache_digest(&config_settings)) }; // If the cache contains a compatible wheel, return it. @@ -1779,11 +1796,11 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { } // If there are build settings, we need to scope to a cache shard. - let config_settings = self.build_context.config_settings(); + let config_settings = self.config_settings_for(source.name()); let cache_shard = if config_settings.is_empty() { cache_shard } else { - cache_shard.shard(cache_digest(config_settings)) + cache_shard.shard(cache_digest(&config_settings)) }; // Otherwise, we need to build a wheel. diff --git a/crates/uv-installer/src/plan.rs b/crates/uv-installer/src/plan.rs index e030e9b4d..69e10befc 100644 --- a/crates/uv-installer/src/plan.rs +++ b/crates/uv-installer/src/plan.rs @@ -4,7 +4,7 @@ use tracing::{debug, warn}; use uv_cache::{Cache, CacheBucket, WheelCache}; use uv_cache_info::Timestamp; -use uv_configuration::{BuildOptions, ConfigSettings, Reinstall}; +use uv_configuration::{BuildOptions, ConfigSettings, PackageConfigSettings, Reinstall}; use uv_distribution::{ BuiltWheelIndex, HttpArchivePointer, LocalArchivePointer, RegistryWheelIndex, }; @@ -52,6 +52,7 @@ impl<'a> Planner<'a> { hasher: &HashStrategy, index_locations: &IndexLocations, config_settings: &ConfigSettings, + config_settings_package: &PackageConfigSettings, cache: &Cache, venv: &PythonEnvironment, tags: &Tags, @@ -59,7 +60,13 @@ impl<'a> Planner<'a> { // Index all the already-downloaded wheels in the cache. let mut registry_index = RegistryWheelIndex::new(cache, tags, index_locations, hasher, config_settings); - let built_index = BuiltWheelIndex::new(cache, tags, hasher, config_settings); + let built_index = BuiltWheelIndex::new( + cache, + tags, + hasher, + config_settings, + config_settings_package, + ); let mut cached = vec![]; let mut remote = vec![]; diff --git a/crates/uv-settings/src/combine.rs b/crates/uv-settings/src/combine.rs index 8edbd2a05..738b00ffe 100644 --- a/crates/uv-settings/src/combine.rs +++ b/crates/uv-settings/src/combine.rs @@ -4,8 +4,8 @@ use std::path::PathBuf; use url::Url; use uv_configuration::{ - ConfigSettings, ExportFormat, IndexStrategy, KeyringProviderType, RequiredVersion, - TargetTriple, TrustedPublishing, + ConfigSettings, ExportFormat, IndexStrategy, KeyringProviderType, PackageConfigSettings, + RequiredVersion, TargetTriple, TrustedPublishing, }; use uv_distribution_types::{Index, IndexUrl, PipExtraIndex, PipFindLinks, PipIndex}; use uv_install_wheel::LinkMode; @@ -131,6 +131,17 @@ impl Combine for Option { } } +impl Combine for Option { + /// Combine two maps by merging the map in `self` with the map in `other`, if they're both + /// `Some`. + fn combine(self, other: Option) -> Option { + match (self, other) { + (Some(a), Some(b)) => Some(a.merge(b)), + (a, b) => a.or(b), + } + } +} + impl Combine for serde::de::IgnoredAny { fn combine(self, _other: Self) -> Self { self diff --git a/crates/uv-settings/src/settings.rs b/crates/uv-settings/src/settings.rs index e057cb40a..9eb765a1e 100644 --- a/crates/uv-settings/src/settings.rs +++ b/crates/uv-settings/src/settings.rs @@ -4,8 +4,8 @@ use serde::{Deserialize, Serialize}; use uv_cache_info::CacheKey; use uv_configuration::{ - ConfigSettings, IndexStrategy, KeyringProviderType, PackageNameSpecifier, RequiredVersion, - TargetTriple, TrustedHost, TrustedPublishing, + ConfigSettings, IndexStrategy, KeyringProviderType, PackageConfigSettings, + PackageNameSpecifier, RequiredVersion, TargetTriple, TrustedHost, TrustedPublishing, }; use uv_distribution_types::{ Index, IndexUrl, IndexUrlError, PipExtraIndex, PipFindLinks, PipIndex, StaticMetadata, @@ -361,6 +361,7 @@ pub struct ResolverOptions { pub fork_strategy: Option, pub dependency_metadata: Option>, pub config_settings: Option, + pub config_settings_package: Option, pub exclude_newer: Option, pub link_mode: Option, pub upgrade: Option, @@ -587,6 +588,18 @@ pub struct ResolverInstallerOptions { "# )] pub config_settings: Option, + /// Settings to pass to the [PEP 517](https://peps.python.org/pep-0517/) build backend for specific packages, + /// specified as `KEY=VALUE` pairs. + /// + /// Accepts a map from package names to string key-value pairs. + #[option( + default = "{}", + value_type = "dict", + example = r#" + config-settings-package = { numpy = { editable_mode = "compat" } } + "# + )] + pub config_settings_package: Option, /// Disable isolation when building source distributions. /// /// Assumes that build dependencies specified by [PEP 518](https://peps.python.org/pep-0518/) @@ -1333,6 +1346,16 @@ pub struct PipOptions { "# )] pub config_settings: Option, + /// Settings to pass to the [PEP 517](https://peps.python.org/pep-0517/) build backend for specific packages, + /// specified as `KEY=VALUE` pairs. + #[option( + default = "{}", + value_type = "dict", + example = r#" + config-settings-package = { numpy = { editable_mode = "compat" } } + "# + )] + pub config_settings_package: Option, /// The minimum Python version that should be supported by the resolved requirements (e.g., /// `3.8` or `3.8.17`). /// @@ -1651,6 +1674,7 @@ impl From for ResolverOptions { fork_strategy: value.fork_strategy, dependency_metadata: value.dependency_metadata, config_settings: value.config_settings, + config_settings_package: value.config_settings_package, exclude_newer: value.exclude_newer, link_mode: value.link_mode, upgrade: value.upgrade, @@ -1714,6 +1738,7 @@ pub struct ToolOptions { pub fork_strategy: Option, pub dependency_metadata: Option>, pub config_settings: Option, + pub config_settings_package: Option, pub no_build_isolation: Option, pub no_build_isolation_package: Option>, pub exclude_newer: Option, @@ -1741,6 +1766,7 @@ impl From for ToolOptions { fork_strategy: value.fork_strategy, dependency_metadata: value.dependency_metadata, config_settings: value.config_settings, + config_settings_package: value.config_settings_package, no_build_isolation: value.no_build_isolation, no_build_isolation_package: value.no_build_isolation_package, exclude_newer: value.exclude_newer, @@ -1770,6 +1796,7 @@ impl From for ResolverInstallerOptions { fork_strategy: value.fork_strategy, dependency_metadata: value.dependency_metadata, config_settings: value.config_settings, + config_settings_package: value.config_settings_package, no_build_isolation: value.no_build_isolation, no_build_isolation_package: value.no_build_isolation_package, exclude_newer: value.exclude_newer, @@ -1822,6 +1849,7 @@ pub struct OptionsWire { fork_strategy: Option, dependency_metadata: Option>, config_settings: Option, + config_settings_package: Option, no_build_isolation: Option, no_build_isolation_package: Option>, exclude_newer: Option, @@ -1911,6 +1939,7 @@ impl From for Options { fork_strategy, dependency_metadata, config_settings, + config_settings_package, no_build_isolation, no_build_isolation_package, exclude_newer, @@ -1977,6 +2006,7 @@ impl From for Options { fork_strategy, dependency_metadata, config_settings, + config_settings_package, no_build_isolation, no_build_isolation_package, exclude_newer, diff --git a/crates/uv-types/src/traits.rs b/crates/uv-types/src/traits.rs index a95367fef..e3f4ee012 100644 --- a/crates/uv-types/src/traits.rs +++ b/crates/uv-types/src/traits.rs @@ -7,7 +7,9 @@ use anyhow::Result; use rustc_hash::FxHashSet; use uv_cache::Cache; -use uv_configuration::{BuildKind, BuildOptions, BuildOutput, ConfigSettings, SourceStrategy}; +use uv_configuration::{ + BuildKind, BuildOptions, BuildOutput, ConfigSettings, PackageConfigSettings, SourceStrategy, +}; use uv_distribution_filename::DistFilename; use uv_distribution_types::{ CachedDist, DependencyMetadata, DistributionId, IndexCapabilities, IndexLocations, @@ -87,6 +89,9 @@ pub trait BuildContext { /// The [`ConfigSettings`] used to build distributions. fn config_settings(&self) -> &ConfigSettings; + /// The [`ConfigSettings`] used to build a specific package. + fn config_settings_package(&self) -> &PackageConfigSettings; + /// Whether to incorporate `tool.uv.sources` when resolving requirements. fn sources(&self) -> SourceStrategy; diff --git a/crates/uv/src/commands/build_frontend.rs b/crates/uv/src/commands/build_frontend.rs index a830f7aef..b3f9e5c89 100644 --- a/crates/uv/src/commands/build_frontend.rs +++ b/crates/uv/src/commands/build_frontend.rs @@ -16,7 +16,7 @@ use uv_client::{BaseClientBuilder, FlatIndexClient, RegistryClientBuilder}; use uv_configuration::{ BuildKind, BuildOptions, BuildOutput, Concurrency, ConfigSettings, Constraints, DependencyGroupsWithDefaults, HashCheckingMode, IndexStrategy, KeyringProviderType, - PreviewMode, SourceStrategy, + PackageConfigSettings, PreviewMode, SourceStrategy, }; use uv_dispatch::{BuildDispatch, SharedState}; use uv_distribution_filename::{ @@ -197,6 +197,7 @@ async fn build_impl( fork_strategy: _, dependency_metadata, config_setting, + config_settings_package, no_build_isolation, no_build_isolation_package, exclude_newer, @@ -357,6 +358,7 @@ async fn build_impl( dependency_metadata, *link_mode, config_setting, + config_settings_package, preview, ); async { @@ -434,6 +436,7 @@ async fn build_package( dependency_metadata: &DependencyMetadata, link_mode: LinkMode, config_setting: &ConfigSettings, + config_settings_package: &PackageConfigSettings, preview: PreviewMode, ) -> Result, Error> { let output_dir = if let Some(output_dir) = output_dir { @@ -568,6 +571,7 @@ async fn build_package( state.clone(), index_strategy, config_setting, + config_settings_package, build_isolation, link_mode, build_options, diff --git a/crates/uv/src/commands/pip/compile.rs b/crates/uv/src/commands/pip/compile.rs index c40716763..a5116327b 100644 --- a/crates/uv/src/commands/pip/compile.rs +++ b/crates/uv/src/commands/pip/compile.rs @@ -14,7 +14,8 @@ use uv_cache::Cache; use uv_client::{BaseClientBuilder, FlatIndexClient, RegistryClientBuilder}; use uv_configuration::{ BuildOptions, Concurrency, ConfigSettings, Constraints, ExportFormat, ExtrasSpecification, - IndexStrategy, NoBinary, NoBuild, PreviewMode, Reinstall, SourceStrategy, Upgrade, + IndexStrategy, NoBinary, NoBuild, PackageConfigSettings, PreviewMode, Reinstall, + SourceStrategy, Upgrade, }; use uv_configuration::{KeyringProviderType, TargetTriple}; use uv_dispatch::{BuildDispatch, SharedState}; @@ -90,6 +91,7 @@ pub(crate) async fn pip_compile( keyring_provider: KeyringProviderType, network_settings: &NetworkSettings, config_settings: ConfigSettings, + config_settings_package: PackageConfigSettings, no_build_isolation: bool, no_build_isolation_package: Vec, build_options: BuildOptions, @@ -477,6 +479,7 @@ pub(crate) async fn pip_compile( state, index_strategy, &config_settings, + &config_settings_package, build_isolation, link_mode, &build_options, diff --git a/crates/uv/src/commands/pip/install.rs b/crates/uv/src/commands/pip/install.rs index bbfe99c50..79e18bd98 100644 --- a/crates/uv/src/commands/pip/install.rs +++ b/crates/uv/src/commands/pip/install.rs @@ -11,7 +11,8 @@ use uv_cache::Cache; use uv_client::{BaseClientBuilder, FlatIndexClient, RegistryClientBuilder}; use uv_configuration::{ BuildOptions, Concurrency, ConfigSettings, Constraints, DryRun, ExtrasSpecification, - HashCheckingMode, IndexStrategy, PreviewMode, Reinstall, SourceStrategy, Upgrade, + HashCheckingMode, IndexStrategy, PackageConfigSettings, PreviewMode, Reinstall, SourceStrategy, + Upgrade, }; use uv_configuration::{KeyringProviderType, TargetTriple}; use uv_dispatch::{BuildDispatch, SharedState}; @@ -75,6 +76,7 @@ pub(crate) async fn pip_install( hash_checking: Option, installer_metadata: bool, config_settings: &ConfigSettings, + config_settings_package: &PackageConfigSettings, no_build_isolation: bool, no_build_isolation_package: Vec, build_options: BuildOptions, @@ -422,6 +424,7 @@ pub(crate) async fn pip_install( state.clone(), index_strategy, config_settings, + config_settings_package, build_isolation, link_mode, &build_options, @@ -513,6 +516,7 @@ pub(crate) async fn pip_install( compile, &index_locations, config_settings, + config_settings_package, &hasher, &tags, &client, diff --git a/crates/uv/src/commands/pip/operations.rs b/crates/uv/src/commands/pip/operations.rs index 55ab2aa1b..117321c14 100644 --- a/crates/uv/src/commands/pip/operations.rs +++ b/crates/uv/src/commands/pip/operations.rs @@ -13,7 +13,7 @@ use uv_cache::Cache; use uv_client::{BaseClientBuilder, RegistryClient}; use uv_configuration::{ BuildOptions, Concurrency, ConfigSettings, Constraints, DependencyGroups, DryRun, - ExtrasSpecification, Overrides, Reinstall, Upgrade, + ExtrasSpecification, Overrides, PackageConfigSettings, Reinstall, Upgrade, }; use uv_dispatch::BuildDispatch; use uv_distribution::{DistributionDatabase, SourcedDependencyGroups}; @@ -445,6 +445,7 @@ pub(crate) async fn install( compile: bool, index_urls: &IndexLocations, config_settings: &ConfigSettings, + config_settings_package: &PackageConfigSettings, hasher: &HashStrategy, tags: &Tags, client: &RegistryClient, @@ -470,6 +471,7 @@ pub(crate) async fn install( hasher, index_urls, config_settings, + config_settings_package, cache, venv, tags, diff --git a/crates/uv/src/commands/pip/sync.rs b/crates/uv/src/commands/pip/sync.rs index 6858ddad0..61999825e 100644 --- a/crates/uv/src/commands/pip/sync.rs +++ b/crates/uv/src/commands/pip/sync.rs @@ -9,7 +9,8 @@ use uv_cache::Cache; use uv_client::{BaseClientBuilder, FlatIndexClient, RegistryClientBuilder}; use uv_configuration::{ BuildOptions, Concurrency, ConfigSettings, Constraints, DryRun, ExtrasSpecification, - HashCheckingMode, IndexStrategy, PreviewMode, Reinstall, SourceStrategy, Upgrade, + HashCheckingMode, IndexStrategy, PackageConfigSettings, PreviewMode, Reinstall, SourceStrategy, + Upgrade, }; use uv_configuration::{KeyringProviderType, TargetTriple}; use uv_dispatch::{BuildDispatch, SharedState}; @@ -60,6 +61,7 @@ pub(crate) async fn pip_sync( allow_empty_requirements: bool, installer_metadata: bool, config_settings: &ConfigSettings, + config_settings_package: &PackageConfigSettings, no_build_isolation: bool, no_build_isolation_package: Vec, build_options: BuildOptions, @@ -355,6 +357,7 @@ pub(crate) async fn pip_sync( state.clone(), index_strategy, config_settings, + config_settings_package, build_isolation, link_mode, &build_options, @@ -448,6 +451,7 @@ pub(crate) async fn pip_sync( compile, &index_locations, config_settings, + config_settings_package, &hasher, &tags, &client, diff --git a/crates/uv/src/commands/project/add.rs b/crates/uv/src/commands/project/add.rs index 28cc2dcd5..12535f859 100644 --- a/crates/uv/src/commands/project/add.rs +++ b/crates/uv/src/commands/project/add.rs @@ -436,6 +436,7 @@ pub(crate) async fn add( state.clone().into_inner(), settings.resolver.index_strategy, &settings.resolver.config_setting, + &settings.resolver.config_settings_package, build_isolation, settings.resolver.link_mode, &settings.resolver.build_options, diff --git a/crates/uv/src/commands/project/lock.rs b/crates/uv/src/commands/project/lock.rs index e23bd97c2..706c86593 100644 --- a/crates/uv/src/commands/project/lock.rs +++ b/crates/uv/src/commands/project/lock.rs @@ -432,6 +432,7 @@ async fn do_lock( fork_strategy, dependency_metadata, config_setting, + config_settings_package, no_build_isolation, no_build_isolation_package, exclude_newer, @@ -674,6 +675,7 @@ async fn do_lock( state.fork().into_inner(), *index_strategy, config_setting, + config_settings_package, build_isolation, *link_mode, build_options, diff --git a/crates/uv/src/commands/project/mod.rs b/crates/uv/src/commands/project/mod.rs index cce02a70b..becd2a26e 100644 --- a/crates/uv/src/commands/project/mod.rs +++ b/crates/uv/src/commands/project/mod.rs @@ -1674,6 +1674,7 @@ pub(crate) async fn resolve_names( ResolverSettings { build_options, config_setting, + config_settings_package, dependency_metadata, exclude_newer, fork_strategy: _, @@ -1742,6 +1743,7 @@ pub(crate) async fn resolve_names( state.clone(), *index_strategy, config_setting, + config_settings_package, build_isolation, *link_mode, build_options, @@ -1832,6 +1834,7 @@ pub(crate) async fn resolve_environment( fork_strategy, dependency_metadata, config_setting, + config_settings_package, no_build_isolation, no_build_isolation_package, exclude_newer, @@ -1948,6 +1951,7 @@ pub(crate) async fn resolve_environment( state.clone().into_inner(), *index_strategy, config_setting, + config_settings_package, build_isolation, *link_mode, build_options, @@ -2013,6 +2017,7 @@ pub(crate) async fn sync_environment( keyring_provider, dependency_metadata, config_setting, + config_settings_package, no_build_isolation, no_build_isolation_package, exclude_newer, @@ -2084,6 +2089,7 @@ pub(crate) async fn sync_environment( state.clone().into_inner(), index_strategy, config_setting, + config_settings_package, build_isolation, link_mode, build_options, @@ -2106,6 +2112,7 @@ pub(crate) async fn sync_environment( compile_bytecode, index_locations, config_setting, + config_settings_package, &hasher, tags, &client, @@ -2169,6 +2176,7 @@ pub(crate) async fn update_environment( ResolverSettings { build_options, config_setting, + config_settings_package, dependency_metadata, exclude_newer, fork_strategy, @@ -2305,6 +2313,7 @@ pub(crate) async fn update_environment( state.clone(), *index_strategy, config_setting, + config_settings_package, build_isolation, *link_mode, build_options, @@ -2362,6 +2371,7 @@ pub(crate) async fn update_environment( *compile_bytecode, index_locations, config_setting, + config_settings_package, &hasher, tags, &client, diff --git a/crates/uv/src/commands/project/sync.rs b/crates/uv/src/commands/project/sync.rs index 8d2dd9629..adf3b61f2 100644 --- a/crates/uv/src/commands/project/sync.rs +++ b/crates/uv/src/commands/project/sync.rs @@ -573,6 +573,7 @@ pub(super) async fn do_sync( keyring_provider, dependency_metadata, config_setting, + config_settings_package, no_build_isolation, no_build_isolation_package, exclude_newer, @@ -709,6 +710,7 @@ pub(super) async fn do_sync( state.clone().into_inner(), index_strategy, config_setting, + config_settings_package, build_isolation, link_mode, build_options, @@ -733,6 +735,7 @@ pub(super) async fn do_sync( compile_bytecode, index_locations, config_setting, + config_settings_package, &hasher, &tags, &client, diff --git a/crates/uv/src/commands/project/tree.rs b/crates/uv/src/commands/project/tree.rs index cd1339d3e..756820dc7 100644 --- a/crates/uv/src/commands/project/tree.rs +++ b/crates/uv/src/commands/project/tree.rs @@ -200,6 +200,7 @@ pub(crate) async fn tree( fork_strategy: _, dependency_metadata: _, config_setting: _, + config_settings_package: _, no_build_isolation: _, no_build_isolation_package: _, exclude_newer: _, diff --git a/crates/uv/src/commands/venv.rs b/crates/uv/src/commands/venv.rs index 92eb1ead7..9d3b87fe1 100644 --- a/crates/uv/src/commands/venv.rs +++ b/crates/uv/src/commands/venv.rs @@ -12,7 +12,7 @@ use uv_cache::Cache; use uv_client::{BaseClientBuilder, FlatIndexClient, RegistryClientBuilder}; use uv_configuration::{ BuildOptions, Concurrency, ConfigSettings, Constraints, DependencyGroups, IndexStrategy, - KeyringProviderType, NoBinary, NoBuild, PreviewMode, SourceStrategy, + KeyringProviderType, NoBinary, NoBuild, PackageConfigSettings, PreviewMode, SourceStrategy, }; use uv_dispatch::{BuildDispatch, SharedState}; use uv_distribution_types::Requirement; @@ -269,6 +269,7 @@ pub(crate) async fn venv( let build_constraints = Constraints::default(); let build_hasher = HashStrategy::default(); let config_settings = ConfigSettings::default(); + let config_settings_package = PackageConfigSettings::default(); let sources = SourceStrategy::Disabled; // Do not allow builds @@ -286,6 +287,7 @@ pub(crate) async fn venv( state.clone(), index_strategy, &config_settings, + &config_settings_package, BuildIsolation::Isolated, link_mode, &build_options, diff --git a/crates/uv/src/lib.rs b/crates/uv/src/lib.rs index 9c9b41065..0f6c9465f 100644 --- a/crates/uv/src/lib.rs +++ b/crates/uv/src/lib.rs @@ -524,6 +524,7 @@ async fn run(mut cli: Cli) -> Result { args.settings.keyring_provider, &globals.network_settings, args.settings.config_setting, + args.settings.config_settings_package, args.settings.no_build_isolation, args.settings.no_build_isolation_package, args.settings.build_options, @@ -594,6 +595,7 @@ async fn run(mut cli: Cli) -> Result { args.settings.allow_empty_requirements, globals.installer_metadata, &args.settings.config_setting, + &args.settings.config_settings_package, args.settings.no_build_isolation, args.settings.no_build_isolation_package, args.settings.build_options, @@ -745,6 +747,7 @@ async fn run(mut cli: Cli) -> Result { args.settings.hash_checking, globals.installer_metadata, &args.settings.config_setting, + &args.settings.config_settings_package, args.settings.no_build_isolation, args.settings.no_build_isolation_package, args.settings.build_options, diff --git a/crates/uv/src/settings.rs b/crates/uv/src/settings.rs index 1ebeecba8..aa105cf97 100644 --- a/crates/uv/src/settings.rs +++ b/crates/uv/src/settings.rs @@ -23,9 +23,9 @@ use uv_client::Connectivity; use uv_configuration::{ BuildOptions, Concurrency, ConfigSettings, DependencyGroups, DryRun, EditableMode, ExportFormat, ExtrasSpecification, HashCheckingMode, IndexStrategy, InstallOptions, - KeyringProviderType, NoBinary, NoBuild, PreviewMode, ProjectBuildBackend, Reinstall, - RequiredVersion, SourceStrategy, TargetTriple, TrustedHost, TrustedPublishing, Upgrade, - VersionControlSystem, + KeyringProviderType, NoBinary, NoBuild, PackageConfigSettings, PreviewMode, + ProjectBuildBackend, Reinstall, RequiredVersion, SourceStrategy, TargetTriple, TrustedHost, + TrustedPublishing, Upgrade, VersionControlSystem, }; use uv_distribution_types::{DependencyMetadata, Index, IndexLocations, IndexUrl, Requirement}; use uv_install_wheel::LinkMode; @@ -712,6 +712,7 @@ impl ToolUpgradeSettings { pre, fork_strategy, config_setting, + config_setting_package: config_settings_package, no_build_isolation, no_build_isolation_package, build_isolation, @@ -746,6 +747,7 @@ impl ToolUpgradeSettings { pre, fork_strategy, config_setting, + config_settings_package, no_build_isolation, no_build_isolation_package, build_isolation, @@ -2694,6 +2696,7 @@ pub(crate) struct InstallerSettingsRef<'a> { pub(crate) keyring_provider: KeyringProviderType, pub(crate) dependency_metadata: &'a DependencyMetadata, pub(crate) config_setting: &'a ConfigSettings, + pub(crate) config_settings_package: &'a PackageConfigSettings, pub(crate) no_build_isolation: bool, pub(crate) no_build_isolation_package: &'a [PackageName], pub(crate) exclude_newer: Option, @@ -2712,6 +2715,7 @@ pub(crate) struct InstallerSettingsRef<'a> { pub(crate) struct ResolverSettings { pub(crate) build_options: BuildOptions, pub(crate) config_setting: ConfigSettings, + pub(crate) config_settings_package: PackageConfigSettings, pub(crate) dependency_metadata: DependencyMetadata, pub(crate) exclude_newer: Option, pub(crate) fork_strategy: ForkStrategy, @@ -2770,6 +2774,7 @@ impl From for ResolverSettings { index_strategy: value.index_strategy.unwrap_or_default(), keyring_provider: value.keyring_provider.unwrap_or_default(), config_setting: value.config_settings.unwrap_or_default(), + config_settings_package: value.config_settings_package.unwrap_or_default(), no_build_isolation: value.no_build_isolation.unwrap_or_default(), no_build_isolation_package: value.no_build_isolation_package.unwrap_or_default(), exclude_newer: value.exclude_newer, @@ -2849,6 +2854,7 @@ impl From for ResolverInstallerSettings { NoBuild::from_args(value.no_build, value.no_build_package.unwrap_or_default()), ), config_setting: value.config_settings.unwrap_or_default(), + config_settings_package: value.config_settings_package.unwrap_or_default(), dependency_metadata: DependencyMetadata::from_entries( value.dependency_metadata.into_iter().flatten(), ), @@ -2918,6 +2924,7 @@ pub(crate) struct PipSettings { pub(crate) custom_compile_command: Option, pub(crate) generate_hashes: bool, pub(crate) config_setting: ConfigSettings, + pub(crate) config_settings_package: PackageConfigSettings, pub(crate) python_version: Option, pub(crate) python_platform: Option, pub(crate) universal: bool, @@ -2987,6 +2994,7 @@ impl PipSettings { custom_compile_command, generate_hashes, config_settings, + config_settings_package, python_version, python_platform, universal, @@ -3022,6 +3030,7 @@ impl PipSettings { fork_strategy: top_level_fork_strategy, dependency_metadata: top_level_dependency_metadata, config_settings: top_level_config_settings, + config_settings_package: top_level_config_settings_package, no_build_isolation: top_level_no_build_isolation, no_build_isolation_package: top_level_no_build_isolation_package, exclude_newer: top_level_exclude_newer, @@ -3054,6 +3063,8 @@ impl PipSettings { let fork_strategy = fork_strategy.combine(top_level_fork_strategy); let dependency_metadata = dependency_metadata.combine(top_level_dependency_metadata); let config_settings = config_settings.combine(top_level_config_settings); + let config_settings_package = + config_settings_package.combine(top_level_config_settings_package); let no_build_isolation = no_build_isolation.combine(top_level_no_build_isolation); let no_build_isolation_package = no_build_isolation_package.combine(top_level_no_build_isolation_package); @@ -3156,6 +3167,10 @@ impl PipSettings { .config_settings .combine(config_settings) .unwrap_or_default(), + config_settings_package: args + .config_settings_package + .combine(config_settings_package) + .unwrap_or_default(), torch_backend: args.torch_backend.combine(torch_backend), python_version: args.python_version.combine(python_version), python_platform: args.python_platform.combine(python_platform), @@ -3249,6 +3264,7 @@ impl<'a> From<&'a ResolverInstallerSettings> for InstallerSettingsRef<'a> { keyring_provider: settings.resolver.keyring_provider, dependency_metadata: &settings.resolver.dependency_metadata, config_setting: &settings.resolver.config_setting, + config_settings_package: &settings.resolver.config_settings_package, no_build_isolation: settings.resolver.no_build_isolation, no_build_isolation_package: &settings.resolver.no_build_isolation_package, exclude_newer: settings.resolver.exclude_newer, diff --git a/crates/uv/tests/it/pip_install.rs b/crates/uv/tests/it/pip_install.rs index 2a7b0f404..a977ac813 100644 --- a/crates/uv/tests/it/pip_install.rs +++ b/crates/uv/tests/it/pip_install.rs @@ -4054,13 +4054,13 @@ fn config_settings_path() -> Result<()> { "### ); - // When installed without `--editable_mode=compat`, the `finder.py` file should be present. + // When installed without `editable_mode=compat`, the `finder.py` file should be present. let finder = context .site_packages() .join("__editable___setuptools_editable_0_1_0_finder.py"); assert!(finder.exists()); - // Reinstalling with `--editable_mode=compat` should be a no-op; changes in build configuration + // Reinstalling with `editable_mode=compat` should be a no-op; changes in build configuration // don't invalidate the environment. uv_snapshot!(context.filters(), context.pip_install() .arg("-r") @@ -4089,7 +4089,7 @@ fn config_settings_path() -> Result<()> { - setuptools-editable==0.1.0 (from file://[WORKSPACE]/scripts/packages/setuptools_editable) "###); - // Install the editable package with `--editable_mode=compat`. We should ignore the cached + // Install the editable package with `editable_mode=compat`. We should ignore the cached // build configuration and rebuild. uv_snapshot!(context.filters(), context.pip_install() .arg("-r") @@ -4109,7 +4109,7 @@ fn config_settings_path() -> Result<()> { "### ); - // When installed without `--editable_mode=compat`, the `finder.py` file should _not_ be present. + // When installed without `editable_mode=compat`, the `finder.py` file should _not_ be present. let finder = context .site_packages() .join("__editable___setuptools_editable_0_1_0_finder.py"); @@ -11739,3 +11739,114 @@ fn install_python_preference() { Audited 1 package in [TIME] "); } + +#[test] +fn config_settings_package() -> Result<()> { + let context = TestContext::new("3.12"); + + let requirements_txt = context.temp_dir.child("requirements.txt"); + requirements_txt.write_str(&format!( + "-e {}", + context + .workspace_root + .join("scripts/packages/setuptools_editable") + .display() + ))?; + + // Install the editable package. + uv_snapshot!(context.filters(), context.pip_install() + .arg("-r") + .arg("requirements.txt"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 2 packages in [TIME] + Prepared 2 packages in [TIME] + Installed 2 packages in [TIME] + + iniconfig==2.0.0 + + setuptools-editable==0.1.0 (from file://[WORKSPACE]/scripts/packages/setuptools_editable) + "### + ); + + // When installed without `editable_mode=compat`, the `finder.py` file should be present. + let finder = context + .site_packages() + .join("__editable___setuptools_editable_0_1_0_finder.py"); + assert!(finder.exists()); + + // Uninstall the package. + uv_snapshot!(context.filters(), context.pip_uninstall() + .arg("setuptools-editable"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Uninstalled 1 package in [TIME] + - setuptools-editable==0.1.0 (from file://[WORKSPACE]/scripts/packages/setuptools_editable) + "###); + + // Install the editable package with `editable_mode=compat`, scoped to the package. + uv_snapshot!(context.filters(), context.pip_install() + .arg("-r") + .arg("requirements.txt") + .arg("--config-settings-package") + .arg("setuptools-editable:editable_mode=compat"), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 2 packages in [TIME] + Prepared 1 package in [TIME] + Installed 1 package in [TIME] + + setuptools-editable==0.1.0 (from file://[WORKSPACE]/scripts/packages/setuptools_editable) + " + ); + + // When installed with `editable_mode=compat`, the `finder.py` file should _not_ be present. + let finder = context + .site_packages() + .join("__editable___setuptools_editable_0_1_0_finder.py"); + assert!(!finder.exists()); + + // Uninstall the package. + uv_snapshot!(context.filters(), context.pip_uninstall() + .arg("setuptools-editable"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Uninstalled 1 package in [TIME] + - setuptools-editable==0.1.0 (from file://[WORKSPACE]/scripts/packages/setuptools_editable) + "###); + + // Install the editable package with `editable_mode=compat`, by scoped to a different package. + uv_snapshot!(context.filters(), context.pip_install() + .arg("-r") + .arg("requirements.txt") + .arg("--config-settings-package") + .arg("setuptools:editable_mode=compat") + , @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 2 packages in [TIME] + Installed 1 package in [TIME] + + setuptools-editable==0.1.0 (from file://[WORKSPACE]/scripts/packages/setuptools_editable) + " + ); + + // When installed without `editable_mode=compat`, the `finder.py` file should be present. + let finder = context + .site_packages() + .join("__editable___setuptools_editable_0_1_0_finder.py"); + assert!(finder.exists()); + + Ok(()) +} diff --git a/crates/uv/tests/it/show_settings.rs b/crates/uv/tests/it/show_settings.rs index 2637af8ac..500e78965 100644 --- a/crates/uv/tests/it/show_settings.rs +++ b/crates/uv/tests/it/show_settings.rs @@ -203,6 +203,9 @@ fn resolve_uv_toml() -> anyhow::Result<()> { config_setting: ConfigSettings( {}, ), + config_settings_package: PackageConfigSettings( + {}, + ), python_version: None, python_platform: None, universal: false, @@ -385,6 +388,9 @@ fn resolve_uv_toml() -> anyhow::Result<()> { config_setting: ConfigSettings( {}, ), + config_settings_package: PackageConfigSettings( + {}, + ), python_version: None, python_platform: None, universal: false, @@ -568,6 +574,9 @@ fn resolve_uv_toml() -> anyhow::Result<()> { config_setting: ConfigSettings( {}, ), + config_settings_package: PackageConfigSettings( + {}, + ), python_version: None, python_platform: None, universal: false, @@ -783,6 +792,9 @@ fn resolve_pyproject_toml() -> anyhow::Result<()> { config_setting: ConfigSettings( {}, ), + config_settings_package: PackageConfigSettings( + {}, + ), python_version: None, python_platform: None, universal: false, @@ -933,6 +945,9 @@ fn resolve_pyproject_toml() -> anyhow::Result<()> { config_setting: ConfigSettings( {}, ), + config_settings_package: PackageConfigSettings( + {}, + ), python_version: None, python_platform: None, universal: false, @@ -1127,6 +1142,9 @@ fn resolve_pyproject_toml() -> anyhow::Result<()> { config_setting: ConfigSettings( {}, ), + config_settings_package: PackageConfigSettings( + {}, + ), python_version: None, python_platform: Some( X8664UnknownLinuxGnu, @@ -1369,6 +1387,9 @@ fn resolve_index_url() -> anyhow::Result<()> { config_setting: ConfigSettings( {}, ), + config_settings_package: PackageConfigSettings( + {}, + ), python_version: None, python_platform: None, universal: false, @@ -1621,6 +1642,9 @@ fn resolve_index_url() -> anyhow::Result<()> { config_setting: ConfigSettings( {}, ), + config_settings_package: PackageConfigSettings( + {}, + ), python_version: None, python_platform: None, universal: false, @@ -1828,6 +1852,9 @@ fn resolve_find_links() -> anyhow::Result<()> { config_setting: ConfigSettings( {}, ), + config_settings_package: PackageConfigSettings( + {}, + ), python_version: None, python_platform: None, universal: false, @@ -2000,6 +2027,9 @@ fn resolve_top_level() -> anyhow::Result<()> { config_setting: ConfigSettings( {}, ), + config_settings_package: PackageConfigSettings( + {}, + ), python_version: None, python_platform: None, universal: false, @@ -2232,6 +2262,9 @@ fn resolve_top_level() -> anyhow::Result<()> { config_setting: ConfigSettings( {}, ), + config_settings_package: PackageConfigSettings( + {}, + ), python_version: None, python_platform: None, universal: false, @@ -2447,6 +2480,9 @@ fn resolve_top_level() -> anyhow::Result<()> { config_setting: ConfigSettings( {}, ), + config_settings_package: PackageConfigSettings( + {}, + ), python_version: None, python_platform: None, universal: false, @@ -2618,6 +2654,9 @@ fn resolve_user_configuration() -> anyhow::Result<()> { config_setting: ConfigSettings( {}, ), + config_settings_package: PackageConfigSettings( + {}, + ), python_version: None, python_platform: None, universal: false, @@ -2773,6 +2812,9 @@ fn resolve_user_configuration() -> anyhow::Result<()> { config_setting: ConfigSettings( {}, ), + config_settings_package: PackageConfigSettings( + {}, + ), python_version: None, python_platform: None, universal: false, @@ -2928,6 +2970,9 @@ fn resolve_user_configuration() -> anyhow::Result<()> { config_setting: ConfigSettings( {}, ), + config_settings_package: PackageConfigSettings( + {}, + ), python_version: None, python_platform: None, universal: false, @@ -3085,6 +3130,9 @@ fn resolve_user_configuration() -> anyhow::Result<()> { config_setting: ConfigSettings( {}, ), + config_settings_package: PackageConfigSettings( + {}, + ), python_version: None, python_platform: None, universal: false, @@ -3208,6 +3256,7 @@ fn resolve_tool() -> anyhow::Result<()> { fork_strategy: None, dependency_metadata: None, config_settings: None, + config_settings_package: None, no_build_isolation: None, no_build_isolation_package: None, exclude_newer: None, @@ -3234,6 +3283,9 @@ fn resolve_tool() -> anyhow::Result<()> { config_setting: ConfigSettings( {}, ), + config_settings_package: PackageConfigSettings( + {}, + ), dependency_metadata: DependencyMetadata( {}, ), @@ -3426,6 +3478,9 @@ fn resolve_poetry_toml() -> anyhow::Result<()> { config_setting: ConfigSettings( {}, ), + config_settings_package: PackageConfigSettings( + {}, + ), python_version: None, python_platform: None, universal: false, @@ -3643,6 +3698,9 @@ fn resolve_both() -> anyhow::Result<()> { config_setting: ConfigSettings( {}, ), + config_settings_package: PackageConfigSettings( + {}, + ), python_version: None, python_platform: None, universal: false, @@ -3950,6 +4008,9 @@ fn resolve_config_file() -> anyhow::Result<()> { config_setting: ConfigSettings( {}, ), + config_settings_package: PackageConfigSettings( + {}, + ), python_version: None, python_platform: None, universal: false, @@ -4004,7 +4065,7 @@ fn resolve_config_file() -> anyhow::Result<()> { | 1 | [project] | ^^^^^^^ - unknown field `project`, expected one of `required-version`, `native-tls`, `offline`, `no-cache`, `cache-dir`, `preview`, `python-preference`, `python-downloads`, `concurrent-downloads`, `concurrent-builds`, `concurrent-installs`, `index`, `index-url`, `extra-index-url`, `no-index`, `find-links`, `index-strategy`, `keyring-provider`, `allow-insecure-host`, `resolution`, `prerelease`, `fork-strategy`, `dependency-metadata`, `config-settings`, `no-build-isolation`, `no-build-isolation-package`, `exclude-newer`, `link-mode`, `compile-bytecode`, `no-sources`, `upgrade`, `upgrade-package`, `reinstall`, `reinstall-package`, `no-build`, `no-build-package`, `no-binary`, `no-binary-package`, `python-install-mirror`, `pypy-install-mirror`, `python-downloads-json-url`, `publish-url`, `trusted-publishing`, `check-url`, `add-bounds`, `pip`, `cache-keys`, `override-dependencies`, `constraint-dependencies`, `build-constraint-dependencies`, `environments`, `required-environments`, `conflicts`, `workspace`, `sources`, `managed`, `package`, `default-groups`, `dependency-groups`, `dev-dependencies`, `build-backend` + unknown field `project`, expected one of `required-version`, `native-tls`, `offline`, `no-cache`, `cache-dir`, `preview`, `python-preference`, `python-downloads`, `concurrent-downloads`, `concurrent-builds`, `concurrent-installs`, `index`, `index-url`, `extra-index-url`, `no-index`, `find-links`, `index-strategy`, `keyring-provider`, `allow-insecure-host`, `resolution`, `prerelease`, `fork-strategy`, `dependency-metadata`, `config-settings`, `config-settings-package`, `no-build-isolation`, `no-build-isolation-package`, `exclude-newer`, `link-mode`, `compile-bytecode`, `no-sources`, `upgrade`, `upgrade-package`, `reinstall`, `reinstall-package`, `no-build`, `no-build-package`, `no-binary`, `no-binary-package`, `python-install-mirror`, `pypy-install-mirror`, `python-downloads-json-url`, `publish-url`, `trusted-publishing`, `check-url`, `add-bounds`, `pip`, `cache-keys`, `override-dependencies`, `constraint-dependencies`, `build-constraint-dependencies`, `environments`, `required-environments`, `conflicts`, `workspace`, `sources`, `managed`, `package`, `default-groups`, `dependency-groups`, `dev-dependencies`, `build-backend` " ); @@ -4199,6 +4260,9 @@ fn resolve_skip_empty() -> anyhow::Result<()> { config_setting: ConfigSettings( {}, ), + config_settings_package: PackageConfigSettings( + {}, + ), python_version: None, python_platform: None, universal: false, @@ -4357,6 +4421,9 @@ fn resolve_skip_empty() -> anyhow::Result<()> { config_setting: ConfigSettings( {}, ), + config_settings_package: PackageConfigSettings( + {}, + ), python_version: None, python_platform: None, universal: false, @@ -4534,6 +4601,9 @@ fn allow_insecure_host() -> anyhow::Result<()> { config_setting: ConfigSettings( {}, ), + config_settings_package: PackageConfigSettings( + {}, + ), python_version: None, python_platform: None, universal: false, @@ -4772,6 +4842,9 @@ fn index_priority() -> anyhow::Result<()> { config_setting: ConfigSettings( {}, ), + config_settings_package: PackageConfigSettings( + {}, + ), python_version: None, python_platform: None, universal: false, @@ -4989,6 +5062,9 @@ fn index_priority() -> anyhow::Result<()> { config_setting: ConfigSettings( {}, ), + config_settings_package: PackageConfigSettings( + {}, + ), python_version: None, python_platform: None, universal: false, @@ -5212,6 +5288,9 @@ fn index_priority() -> anyhow::Result<()> { config_setting: ConfigSettings( {}, ), + config_settings_package: PackageConfigSettings( + {}, + ), python_version: None, python_platform: None, universal: false, @@ -5430,6 +5509,9 @@ fn index_priority() -> anyhow::Result<()> { config_setting: ConfigSettings( {}, ), + config_settings_package: PackageConfigSettings( + {}, + ), python_version: None, python_platform: None, universal: false, @@ -5655,6 +5737,9 @@ fn index_priority() -> anyhow::Result<()> { config_setting: ConfigSettings( {}, ), + config_settings_package: PackageConfigSettings( + {}, + ), python_version: None, python_platform: None, universal: false, @@ -5873,6 +5958,9 @@ fn index_priority() -> anyhow::Result<()> { config_setting: ConfigSettings( {}, ), + config_settings_package: PackageConfigSettings( + {}, + ), python_version: None, python_platform: None, universal: false, @@ -6035,6 +6123,9 @@ fn verify_hashes() -> anyhow::Result<()> { config_setting: ConfigSettings( {}, ), + config_settings_package: PackageConfigSettings( + {}, + ), python_version: None, python_platform: None, universal: false, @@ -6183,6 +6274,9 @@ fn verify_hashes() -> anyhow::Result<()> { config_setting: ConfigSettings( {}, ), + config_settings_package: PackageConfigSettings( + {}, + ), python_version: None, python_platform: None, universal: false, @@ -6329,6 +6423,9 @@ fn verify_hashes() -> anyhow::Result<()> { config_setting: ConfigSettings( {}, ), + config_settings_package: PackageConfigSettings( + {}, + ), python_version: None, python_platform: None, universal: false, @@ -6477,6 +6574,9 @@ fn verify_hashes() -> anyhow::Result<()> { config_setting: ConfigSettings( {}, ), + config_settings_package: PackageConfigSettings( + {}, + ), python_version: None, python_platform: None, universal: false, @@ -6623,6 +6723,9 @@ fn verify_hashes() -> anyhow::Result<()> { config_setting: ConfigSettings( {}, ), + config_settings_package: PackageConfigSettings( + {}, + ), python_version: None, python_platform: None, universal: false, @@ -6770,6 +6873,9 @@ fn verify_hashes() -> anyhow::Result<()> { config_setting: ConfigSettings( {}, ), + config_settings_package: PackageConfigSettings( + {}, + ), python_version: None, python_platform: None, universal: false, diff --git a/crates/uv/tests/it/sync.rs b/crates/uv/tests/it/sync.rs index 5a8d79447..16c4d673a 100644 --- a/crates/uv/tests/it/sync.rs +++ b/crates/uv/tests/it/sync.rs @@ -11126,3 +11126,146 @@ fn sync_python_preference() -> Result<()> { Ok(()) } + +#[test] +fn sync_config_settings_package() -> Result<()> { + let context = TestContext::new("3.12").with_exclude_newer("2025-07-25T00:00:00Z"); + + // Create a child project that uses `setuptools`. + let dependency = context.temp_dir.child("dependency"); + dependency.child("pyproject.toml").write_str( + r#" + [project] + name = "dependency" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = [] + [build-system] + requires = ["setuptools>=42"] + build-backend = "setuptools.build_meta" + "#, + )?; + dependency + .child("dependency") + .child("__init__.py") + .touch()?; + + // Install the `dependency` without `editable_mode=compat`. + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str( + r#" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = ["dependency"] + + [tool.uv.sources] + dependency = { path = "dependency", editable = true } + "#, + )?; + + // Lock the project + context.lock().assert().success(); + + uv_snapshot!(context.filters(), context.sync(), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 2 packages in [TIME] + Prepared 1 package in [TIME] + Installed 1 package in [TIME] + + dependency==0.1.0 (from file://[TEMP_DIR]/dependency) + "); + + // When installed without `editable_mode=compat`, the `finder.py` file should be present. + let finder = context + .site_packages() + .join("__editable___dependency_0_1_0_finder.py"); + assert!(finder.exists()); + + // Remove the virtual environment. + fs_err::remove_dir_all(&context.venv)?; + + // Install the `dependency` with `editable_mode=compat` scoped to the package. + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str( + r#" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = ["dependency"] + + [tool.uv.sources] + dependency = { path = "dependency", editable = true } + + [tool.uv.config-settings-package] + dependency = { editable_mode = "compat" } + "#, + )?; + + uv_snapshot!(context.filters(), context.sync(), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Using CPython 3.12.[X] interpreter at: [PYTHON-3.12] + Creating virtual environment at: .venv + Resolved 2 packages in [TIME] + Prepared 1 package in [TIME] + Installed 1 package in [TIME] + + dependency==0.1.0 (from file://[TEMP_DIR]/dependency) + "); + + // When installed with `editable_mode=compat`, the `finder.py` file should _not_ be present. + let finder = context + .site_packages() + .join("__editable___dependency_0_1_0_finder.py"); + assert!(!finder.exists()); + + // Remove the virtual environment. + fs_err::remove_dir_all(&context.venv)?; + + // Install the `dependency` with `editable_mode=compat` scoped to another package. + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str( + r#" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = ["dependency"] + + [tool.uv.sources] + dependency = { path = "dependency", editable = true } + + [tool.uv.config-settings-package] + setuptools = { editable_mode = "compat" } + "#, + )?; + + uv_snapshot!(context.filters(), context.sync(), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Using CPython 3.12.[X] interpreter at: [PYTHON-3.12] + Creating virtual environment at: .venv + Resolved 2 packages in [TIME] + Installed 1 package in [TIME] + + dependency==0.1.0 (from file://[TEMP_DIR]/dependency) + "); + + // When installed without `editable_mode=compat`, the `finder.py` file should be present. + let finder = context + .site_packages() + .join("__editable___dependency_0_1_0_finder.py"); + assert!(finder.exists()); + + Ok(()) +} diff --git a/docs/reference/cli.md b/docs/reference/cli.md index 4fc832cdb..2ca95dce0 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -84,6 +84,7 @@ uv run [OPTIONS] [COMMAND]

May also be set with the UV_COMPILE_BYTECODE environment variable.

--config-file config-file

The path to a uv.toml file to use for configuration.

While uv configuration can be included in a pyproject.toml file, it is not allowed in this context.

May also be set with the UV_CONFIG_FILE environment variable.

--config-setting, --config-settings, -C config-setting

Settings to pass to the PEP 517 build backend, specified as KEY=VALUE pairs

+
--config-settings-package, --config-settings-package config-settings-package

Settings to pass to the PEP 517 build backend for a specific package, specified as PACKAGE:KEY=VALUE pairs

--default-index default-index

The URL of the default package index (by default: https://pypi.org/simple).

Accepts either a repository compliant with PEP 503 (the simple repository API), or a local directory laid out in the same format.

The index given by this flag is given lower priority than all other indexes specified via the --index flag.

@@ -442,6 +443,7 @@ uv add [OPTIONS] >

May also be set with the UV_COMPILE_BYTECODE environment variable.

--config-file config-file

The path to a uv.toml file to use for configuration.

While uv configuration can be included in a pyproject.toml file, it is not allowed in this context.

May also be set with the UV_CONFIG_FILE environment variable.

--config-setting, --config-settings, -C config-setting

Settings to pass to the PEP 517 build backend, specified as KEY=VALUE pairs

+
--config-settings-package, --config-settings-package config-settings-package

Settings to pass to the PEP 517 build backend for a specific package, specified as PACKAGE:KEY=VALUE pairs

--constraints, --constraint, -c constraints

Constrain versions using the given requirements files.

Constraints files are requirements.txt-like files that only control the version of a requirement that's installed. The constraints will not be added to the project's pyproject.toml file, but will be respected during dependency resolution.

This is equivalent to pip's --constraint option.

@@ -639,6 +641,7 @@ uv remove [OPTIONS] ...

May also be set with the UV_COMPILE_BYTECODE environment variable.

--config-file config-file

The path to a uv.toml file to use for configuration.

While uv configuration can be included in a pyproject.toml file, it is not allowed in this context.

May also be set with the UV_CONFIG_FILE environment variable.

--config-setting, --config-settings, -C config-setting

Settings to pass to the PEP 517 build backend, specified as KEY=VALUE pairs

+
--config-settings-package, --config-settings-package config-settings-package

Settings to pass to the PEP 517 build backend for a specific package, specified as PACKAGE:KEY=VALUE pairs

--default-index default-index

The URL of the default package index (by default: https://pypi.org/simple).

Accepts either a repository compliant with PEP 503 (the simple repository API), or a local directory laid out in the same format.

The index given by this flag is given lower priority than all other indexes specified via the --index flag.

@@ -817,6 +820,7 @@ uv version [OPTIONS] [VALUE]

May also be set with the UV_COMPILE_BYTECODE environment variable.

--config-file config-file

The path to a uv.toml file to use for configuration.

While uv configuration can be included in a pyproject.toml file, it is not allowed in this context.

May also be set with the UV_CONFIG_FILE environment variable.

--config-setting, --config-settings, -C config-setting

Settings to pass to the PEP 517 build backend, specified as KEY=VALUE pairs

+
--config-settings-package, --config-settings-package config-settings-package

Settings to pass to the PEP 517 build backend for a specific package, specified as PACKAGE:KEY=VALUE pairs

--default-index default-index

The URL of the default package index (by default: https://pypi.org/simple).

Accepts either a repository compliant with PEP 503 (the simple repository API), or a local directory laid out in the same format.

The index given by this flag is given lower priority than all other indexes specified via the --index flag.

@@ -1001,6 +1005,7 @@ uv sync [OPTIONS]

May also be set with the UV_COMPILE_BYTECODE environment variable.

--config-file config-file

The path to a uv.toml file to use for configuration.

While uv configuration can be included in a pyproject.toml file, it is not allowed in this context.

May also be set with the UV_CONFIG_FILE environment variable.

--config-setting, --config-settings, -C config-setting

Settings to pass to the PEP 517 build backend, specified as KEY=VALUE pairs

+
--config-settings-package, --config-settings-package config-settings-package

Settings to pass to the PEP 517 build backend for a specific package, specified as PACKAGE:KEY=VALUE pairs

--default-index default-index

The URL of the default package index (by default: https://pypi.org/simple).

Accepts either a repository compliant with PEP 503 (the simple repository API), or a local directory laid out in the same format.

The index given by this flag is given lower priority than all other indexes specified via the --index flag.

@@ -1248,6 +1253,7 @@ uv lock [OPTIONS]
--config-file config-file

The path to a uv.toml file to use for configuration.

While uv configuration can be included in a pyproject.toml file, it is not allowed in this context.

May also be set with the UV_CONFIG_FILE environment variable.

--config-setting, --config-settings, -C config-setting

Settings to pass to the PEP 517 build backend, specified as KEY=VALUE pairs

+
--config-settings-package, --config-settings-package config-settings-package

Settings to pass to the PEP 517 build backend for a specific package, specified as PACKAGE:KEY=VALUE pairs

--default-index default-index

The URL of the default package index (by default: https://pypi.org/simple).

Accepts either a repository compliant with PEP 503 (the simple repository API), or a local directory laid out in the same format.

The index given by this flag is given lower priority than all other indexes specified via the --index flag.

@@ -1411,6 +1417,7 @@ uv export [OPTIONS]
--config-file config-file

The path to a uv.toml file to use for configuration.

While uv configuration can be included in a pyproject.toml file, it is not allowed in this context.

May also be set with the UV_CONFIG_FILE environment variable.

--config-setting, --config-settings, -C config-setting

Settings to pass to the PEP 517 build backend, specified as KEY=VALUE pairs

+
--config-settings-package, --config-settings-package config-settings-package

Settings to pass to the PEP 517 build backend for a specific package, specified as PACKAGE:KEY=VALUE pairs

--default-index default-index

The URL of the default package index (by default: https://pypi.org/simple).

Accepts either a repository compliant with PEP 503 (the simple repository API), or a local directory laid out in the same format.

The index given by this flag is given lower priority than all other indexes specified via the --index flag.

@@ -1605,6 +1612,7 @@ uv tree [OPTIONS]
--config-file config-file

The path to a uv.toml file to use for configuration.

While uv configuration can be included in a pyproject.toml file, it is not allowed in this context.

May also be set with the UV_CONFIG_FILE environment variable.

--config-setting, --config-settings, -C config-setting

Settings to pass to the PEP 517 build backend, specified as KEY=VALUE pairs

+
--config-settings-package, --config-settings-package config-settings-package

Settings to pass to the PEP 517 build backend for a specific package, specified as PACKAGE:KEY=VALUE pairs

--default-index default-index

The URL of the default package index (by default: https://pypi.org/simple).

Accepts either a repository compliant with PEP 503 (the simple repository API), or a local directory laid out in the same format.

The index given by this flag is given lower priority than all other indexes specified via the --index flag.

@@ -1863,6 +1871,7 @@ uv tool run [OPTIONS] [COMMAND]

May also be set with the UV_COMPILE_BYTECODE environment variable.

--config-file config-file

The path to a uv.toml file to use for configuration.

While uv configuration can be included in a pyproject.toml file, it is not allowed in this context.

May also be set with the UV_CONFIG_FILE environment variable.

--config-setting, --config-settings, -C config-setting

Settings to pass to the PEP 517 build backend, specified as KEY=VALUE pairs

+
--config-settings-package, --config-settings-package config-settings-package

Settings to pass to the PEP 517 build backend for a specific package, specified as PACKAGE:KEY=VALUE pairs

--constraints, --constraint, -c constraints

Constrain versions using the given requirements files.

Constraints files are requirements.txt-like files that only control the version of a requirement that's installed. However, including a package in a constraints file will not trigger the installation of that package.

This is equivalent to pip's --constraint option.

@@ -2035,6 +2044,7 @@ uv tool install [OPTIONS]

May also be set with the UV_COMPILE_BYTECODE environment variable.

--config-file config-file

The path to a uv.toml file to use for configuration.

While uv configuration can be included in a pyproject.toml file, it is not allowed in this context.

May also be set with the UV_CONFIG_FILE environment variable.

--config-setting, --config-settings, -C config-setting

Settings to pass to the PEP 517 build backend, specified as KEY=VALUE pairs

+
--config-settings-package, --config-settings-package config-settings-package

Settings to pass to the PEP 517 build backend for a specific package, specified as PACKAGE:KEY=VALUE pairs

--constraints, --constraint, -c constraints

Constrain versions using the given requirements files.

Constraints files are requirements.txt-like files that only control the version of a requirement that's installed. However, including a package in a constraints file will not trigger the installation of that package.

This is equivalent to pip's --constraint option.

@@ -2202,6 +2212,7 @@ uv tool upgrade [OPTIONS] ...

May also be set with the UV_COMPILE_BYTECODE environment variable.

--config-file config-file

The path to a uv.toml file to use for configuration.

While uv configuration can be included in a pyproject.toml file, it is not allowed in this context.

May also be set with the UV_CONFIG_FILE environment variable.

--config-setting, --config-settings, -C config-setting

Settings to pass to the PEP 517 build backend, specified as KEY=VALUE pairs

+
--config-setting-package, --config-settings-package config-setting-package

Settings to pass to the PEP 517 build backend for a specific package, specified as PACKAGE:KEY=VALUE pairs

--default-index default-index

The URL of the default package index (by default: https://pypi.org/simple).

Accepts either a repository compliant with PEP 503 (the simple repository API), or a local directory laid out in the same format.

The index given by this flag is given lower priority than all other indexes specified via the --index flag.

@@ -3345,6 +3356,7 @@ uv pip compile [OPTIONS] >
--config-file config-file

The path to a uv.toml file to use for configuration.

While uv configuration can be included in a pyproject.toml file, it is not allowed in this context.

May also be set with the UV_CONFIG_FILE environment variable.

--config-setting, --config-settings, -C config-setting

Settings to pass to the PEP 517 build backend, specified as KEY=VALUE pairs

+
--config-settings-package, --config-settings-package config-settings-package

Settings to pass to the PEP 517 build backend for a specific package, specified as PACKAGE:KEY=VALUE pairs

--constraints, --constraint, -c constraints

Constrain versions using the given requirements files.

Constraints files are requirements.txt-like files that only control the version of a requirement that's installed. However, including a package in a constraints file will not trigger the installation of that package.

This is equivalent to pip's --constraint option.

@@ -3650,6 +3662,7 @@ uv pip sync [OPTIONS] ...

May also be set with the UV_COMPILE_BYTECODE environment variable.

--config-file config-file

The path to a uv.toml file to use for configuration.

While uv configuration can be included in a pyproject.toml file, it is not allowed in this context.

May also be set with the UV_CONFIG_FILE environment variable.

--config-setting, --config-settings, -C config-setting

Settings to pass to the PEP 517 build backend, specified as KEY=VALUE pairs

+
--config-settings-package, --config-settings-package config-settings-package

Settings to pass to the PEP 517 build backend for a specific package, specified as PACKAGE:KEY=VALUE pairs

--constraints, --constraint, -c constraints

Constrain versions using the given requirements files.

Constraints files are requirements.txt-like files that only control the version of a requirement that's installed. However, including a package in a constraints file will not trigger the installation of that package.

This is equivalent to pip's --constraint option.

@@ -3900,6 +3913,7 @@ uv pip install [OPTIONS] |--editable May also be set with the UV_COMPILE_BYTECODE environment variable.

--config-file config-file

The path to a uv.toml file to use for configuration.

While uv configuration can be included in a pyproject.toml file, it is not allowed in this context.

May also be set with the UV_CONFIG_FILE environment variable.

--config-setting, --config-settings, -C config-setting

Settings to pass to the PEP 517 build backend, specified as KEY=VALUE pairs

+
--config-settings-package, --config-settings-package config-settings-package

Settings to pass to the PEP 517 build backend for a specific package, specified as PACKAGE:KEY=VALUE pairs

--constraints, --constraint, -c constraints

Constrain versions using the given requirements files.

Constraints files are requirements.txt-like files that only control the version of a requirement that's installed. However, including a package in a constraints file will not trigger the installation of that package.

This is equivalent to pip's --constraint option.

@@ -4845,6 +4859,7 @@ uv build [OPTIONS] [SRC]
--config-file config-file

The path to a uv.toml file to use for configuration.

While uv configuration can be included in a pyproject.toml file, it is not allowed in this context.

May also be set with the UV_CONFIG_FILE environment variable.

--config-setting, --config-settings, -C config-setting

Settings to pass to the PEP 517 build backend, specified as KEY=VALUE pairs

+
--config-settings-package, --config-settings-package config-settings-package

Settings to pass to the PEP 517 build backend for a specific package, specified as PACKAGE:KEY=VALUE pairs

--default-index default-index

The URL of the default package index (by default: https://pypi.org/simple).

Accepts either a repository compliant with PEP 503 (the simple repository API), or a local directory laid out in the same format.

The index given by this flag is given lower priority than all other indexes specified via the --index flag.

diff --git a/docs/reference/settings.md b/docs/reference/settings.md index bdee1e4a1..55d3f8ae4 100644 --- a/docs/reference/settings.md +++ b/docs/reference/settings.md @@ -1006,6 +1006,33 @@ specified as `KEY=VALUE` pairs. --- +### [`config-settings-package`](#config-settings-package) {: #config-settings-package } + +Settings to pass to the [PEP 517](https://peps.python.org/pep-0517/) build backend for specific packages, +specified as `KEY=VALUE` pairs. + +Accepts a map from package names to string key-value pairs. + +**Default value**: `{}` + +**Type**: `dict` + +**Example usage**: + +=== "pyproject.toml" + + ```toml + [tool.uv] + config-settings-package = { numpy = { editable_mode = "compat" } } + ``` +=== "uv.toml" + + ```toml + config-settings-package = { numpy = { editable_mode = "compat" } } + ``` + +--- + ### [`dependency-metadata`](#dependency-metadata) {: #dependency-metadata } Pre-defined static metadata for dependencies of the project (direct or transitive). When @@ -2244,6 +2271,33 @@ specified as `KEY=VALUE` pairs. --- +#### [`config-settings-package`](#pip_config-settings-package) {: #pip_config-settings-package } + + +Settings to pass to the [PEP 517](https://peps.python.org/pep-0517/) build backend for specific packages, +specified as `KEY=VALUE` pairs. + +**Default value**: `{}` + +**Type**: `dict` + +**Example usage**: + +=== "pyproject.toml" + + ```toml + [tool.uv.pip] + config-settings-package = { numpy = { editable_mode = "compat" } } + ``` +=== "uv.toml" + + ```toml + [pip] + config-settings-package = { numpy = { editable_mode = "compat" } } + ``` + +--- + #### [`custom-compile-command`](#pip_custom-compile-command) {: #pip_custom-compile-command } diff --git a/uv.schema.json b/uv.schema.json index ba89f65f4..22b30cd06 100644 --- a/uv.schema.json +++ b/uv.schema.json @@ -119,6 +119,17 @@ } ] }, + "config-settings-package": { + "description": "Settings to pass to the [PEP 517](https://peps.python.org/pep-0517/) build backend for specific packages,\nspecified as `KEY=VALUE` pairs.\n\nAccepts a map from package names to string key-value pairs.", + "anyOf": [ + { + "$ref": "#/definitions/PackageConfigSettings" + }, + { + "type": "null" + } + ] + }, "conflicts": { "description": "A list of sets of conflicting groups or extras.", "anyOf": [ @@ -1104,6 +1115,13 @@ } ] }, + "PackageConfigSettings": { + "description": "Settings to pass to PEP 517 build backends on a per-package basis.", + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/ConfigSettings" + } + }, "PackageName": { "description": "The normalized name of a package.\n\nConverts the name to lowercase and collapses runs of `-`, `_`, and `.` down to a single `-`.\nFor example, `---`, `.`, and `__` are all converted to a single `-`.\n\nSee: ", "type": "string" @@ -1185,6 +1203,17 @@ } ] }, + "config-settings-package": { + "description": "Settings to pass to the [PEP 517](https://peps.python.org/pep-0517/) build backend for specific packages,\nspecified as `KEY=VALUE` pairs.", + "anyOf": [ + { + "$ref": "#/definitions/PackageConfigSettings" + }, + { + "type": "null" + } + ] + }, "custom-compile-command": { "description": "The header comment to include at the top of the output file generated by `uv pip compile`.\n\nUsed to reflect custom build scripts and commands that wrap `uv pip compile`.", "type": [