From 74c50649834f09d408543170bff61dd1d7c3af31 Mon Sep 17 00:00:00 2001 From: Aria Desires Date: Mon, 16 Jun 2025 17:26:34 -0400 Subject: [PATCH 01/14] WIP extra-build-dependencies --- crates/uv-bench/benches/uv.rs | 4 +- crates/uv-build-frontend/src/lib.rs | 47 ++++++++--- crates/uv-cli/src/options.rs | 2 + crates/uv-dispatch/src/lib.rs | 5 ++ crates/uv-settings/src/combine.rs | 17 +++- crates/uv-settings/src/settings.rs | 38 ++++++++- crates/uv-workspace/src/pyproject.rs | 79 ++++++++++++++++++ crates/uv/src/commands/build_frontend.rs | 5 ++ crates/uv/src/commands/pip/compile.rs | 3 + crates/uv/src/commands/pip/install.rs | 3 + crates/uv/src/commands/pip/sync.rs | 3 + crates/uv/src/commands/project/add.rs | 1 + crates/uv/src/commands/project/lock.rs | 2 + crates/uv/src/commands/project/mod.rs | 8 ++ crates/uv/src/commands/project/sync.rs | 2 + 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 | 16 +++- crates/uv/tests/it/sync.rs | 100 +++++++++++++++++++++++ docs/reference/settings.md | 84 +++++++++++++++++++ uv.schema.json | 33 ++++++++ 22 files changed, 444 insertions(+), 16 deletions(-) diff --git a/crates/uv-bench/benches/uv.rs b/crates/uv-bench/benches/uv.rs index 8380ccd60..9c83c61d3 100644 --- a/crates/uv-bench/benches/uv.rs +++ b/crates/uv-bench/benches/uv.rs @@ -103,7 +103,7 @@ mod resolver { ResolverEnvironment, ResolverOutput, }; use uv_types::{BuildIsolation, EmptyInstalledPackages, HashStrategy}; - use uv_workspace::WorkspaceCache; + use uv_workspace::{WorkspaceCache, pyproject::ExtraBuildDependencies}; static MARKERS: LazyLock = LazyLock::new(|| { MarkerEnvironment::try_from(MarkerEnvironmentBuilder { @@ -141,6 +141,7 @@ mod resolver { universal: bool, ) -> Result { let build_isolation = BuildIsolation::default(); + let extra_build_dependencies = ExtraBuildDependencies::default(); let build_options = BuildOptions::default(); let concurrency = Concurrency::default(); let config_settings = ConfigSettings::default(); @@ -187,6 +188,7 @@ mod resolver { &config_settings, &config_settings_package, build_isolation, + &extra_build_dependencies, LinkMode::default(), &build_options, &hashes, diff --git a/crates/uv-build-frontend/src/lib.rs b/crates/uv-build-frontend/src/lib.rs index 67bee9619..eab331908 100644 --- a/crates/uv-build-frontend/src/lib.rs +++ b/crates/uv-build-frontend/src/lib.rs @@ -42,6 +42,7 @@ use uv_static::EnvVars; use uv_types::{AnyErrorBuild, BuildContext, BuildIsolation, BuildStack, SourceBuildTrait}; use uv_warnings::warn_user_once; use uv_workspace::WorkspaceCache; +use uv_workspace::pyproject::ExtraBuildDependencies; pub use crate::error::{Error, MissingHeaderCause}; @@ -281,6 +282,7 @@ impl SourceBuild { workspace_cache: &WorkspaceCache, config_settings: ConfigSettings, build_isolation: BuildIsolation<'_>, + extra_build_dependencies: &ExtraBuildDependencies, build_stack: &BuildStack, build_kind: BuildKind, mut environment_variables: FxHashMap, @@ -297,7 +299,6 @@ impl SourceBuild { }; let default_backend: Pep517Backend = DEFAULT_BACKEND.clone(); - // Check if we have a PEP 517 build backend. let (pep517_backend, project) = Self::extract_pep517_backend( &source_tree, @@ -305,6 +306,7 @@ impl SourceBuild { fallback_package_name, locations, source_strategy, + extra_build_dependencies, workspace_cache, &default_backend, ) @@ -506,6 +508,7 @@ impl SourceBuild { package_name: Option<&PackageName>, locations: &IndexLocations, source_strategy: SourceStrategy, + extra_build_dependencies: &ExtraBuildDependencies, workspace_cache: &WorkspaceCache, default_backend: &Pep517Backend, ) -> Result<(Pep517Backend, Option), Box> { @@ -517,17 +520,24 @@ impl SourceBuild { let pyproject_toml: PyProjectToml = PyProjectToml::deserialize(pyproject_toml.into_deserializer()) .map_err(Error::InvalidPyprojectTomlSchema)?; + let name = pyproject_toml + .project + .as_ref() + .map(|project| &project.name) + .or(package_name); + let extra_build_dependencies = name + .as_ref() + .and_then(|name| extra_build_dependencies.get(name).cloned()) + .unwrap_or_default(); + + let backend = if let Some(mut build_system) = pyproject_toml.build_system { + // Apply extra-build-dependencies if there are any + build_system.requires.extend(extra_build_dependencies); - let backend = if let Some(build_system) = pyproject_toml.build_system { // If necessary, lower the requirements. let requirements = match source_strategy { SourceStrategy::Enabled => { - if let Some(name) = pyproject_toml - .project - .as_ref() - .map(|project| &project.name) - .or(package_name) - { + if let Some(name) = name { let build_requires = uv_pypi_types::BuildRequires { name: Some(name.clone()), requires_dist: build_system.requires, @@ -606,7 +616,13 @@ impl SourceBuild { ); } } - default_backend.clone() + let mut backend = default_backend.clone(); + // Apply extra_build_dependencies + // TODO(Gankra): should Sources/Indexes be applied on this path? + backend + .requirements + .extend(extra_build_dependencies.into_iter().map(Requirement::from)); + backend }; Ok((backend, pyproject_toml.project)) } @@ -617,12 +633,21 @@ impl SourceBuild { source_tree.to_path_buf(), ))); } - // If no `pyproject.toml` is present, by default, proceed with a PEP 517 build using // the default backend, to match `build`. `pip` uses `setup.py` directly in this // case, but plans to make PEP 517 builds the default in the future. // See: https://github.com/pypa/pip/issues/9175. - Ok((default_backend.clone(), None)) + let mut backend = default_backend.clone(); + // Apply extra_build_dependencies + // TODO(Gankra): should Sources/Indexes be applied on this path? + let extra_build_dependencies = package_name + .as_ref() + .and_then(|name| extra_build_dependencies.get(name).cloned()) + .unwrap_or_default(); + backend + .requirements + .extend(extra_build_dependencies.into_iter().map(Requirement::from)); + Ok((backend, None)) } Err(err) => Err(Box::new(err.into())), } diff --git a/crates/uv-cli/src/options.rs b/crates/uv-cli/src/options.rs index d2e651a19..e41591b79 100644 --- a/crates/uv-cli/src/options.rs +++ b/crates/uv-cli/src/options.rs @@ -347,6 +347,7 @@ pub fn resolver_options( }), no_build_isolation: flag(no_build_isolation, build_isolation, "build-isolation"), no_build_isolation_package: Some(no_build_isolation_package), + extra_build_dependencies: None, exclude_newer, link_mode, no_build: flag(no_build, build, "build"), @@ -464,6 +465,7 @@ pub fn resolver_installer_options( } else { Some(no_build_isolation_package) }, + extra_build_dependencies: None, exclude_newer, link_mode, compile_bytecode: flag(compile_bytecode, no_compile_bytecode, "compile-bytecode"), diff --git a/crates/uv-dispatch/src/lib.rs b/crates/uv-dispatch/src/lib.rs index 2e34b583d..51b727406 100644 --- a/crates/uv-dispatch/src/lib.rs +++ b/crates/uv-dispatch/src/lib.rs @@ -40,6 +40,7 @@ use uv_types::{ HashStrategy, InFlight, }; use uv_workspace::WorkspaceCache; +use uv_workspace::pyproject::ExtraBuildDependencies; #[derive(Debug, Error)] pub enum BuildDispatchError { @@ -88,6 +89,7 @@ pub struct BuildDispatch<'a> { shared_state: SharedState, dependency_metadata: &'a DependencyMetadata, build_isolation: BuildIsolation<'a>, + extra_build_dependencies: &'a ExtraBuildDependencies, link_mode: uv_install_wheel::LinkMode, build_options: &'a BuildOptions, config_settings: &'a ConfigSettings, @@ -116,6 +118,7 @@ impl<'a> BuildDispatch<'a> { config_settings: &'a ConfigSettings, config_settings_package: &'a PackageConfigSettings, build_isolation: BuildIsolation<'a>, + extra_build_dependencies: &'a ExtraBuildDependencies, link_mode: uv_install_wheel::LinkMode, build_options: &'a BuildOptions, hasher: &'a HashStrategy, @@ -138,6 +141,7 @@ impl<'a> BuildDispatch<'a> { config_settings, config_settings_package, build_isolation, + extra_build_dependencies, link_mode, build_options, hasher, @@ -452,6 +456,7 @@ impl BuildContext for BuildDispatch<'_> { self.workspace_cache(), config_settings, self.build_isolation, + self.extra_build_dependencies, &build_stack, build_kind, self.build_extra_env_vars.clone(), diff --git a/crates/uv-settings/src/combine.rs b/crates/uv-settings/src/combine.rs index 738b00ffe..771d12988 100644 --- a/crates/uv-settings/src/combine.rs +++ b/crates/uv-settings/src/combine.rs @@ -1,5 +1,5 @@ -use std::num::NonZeroUsize; use std::path::PathBuf; +use std::{collections::BTreeMap, num::NonZeroUsize}; use url::Url; @@ -120,6 +120,21 @@ impl Combine for Option> { } } +impl Combine for Option>> { + /// Combine two maps of vecs by combining their vecs + fn combine(self, other: Option>>) -> Option>> { + match (self, other) { + (Some(mut a), Some(b)) => { + for (key, value) in b { + a.entry(key).or_default().extend(value); + } + Some(a) + } + (a, b) => a.or(b), + } + } +} + impl Combine for Option { /// Combine two maps by merging the map in `self` with the map in `other`, if they're both /// `Some`. diff --git a/crates/uv-settings/src/settings.rs b/crates/uv-settings/src/settings.rs index 9eb765a1e..23b11e87b 100644 --- a/crates/uv-settings/src/settings.rs +++ b/crates/uv-settings/src/settings.rs @@ -20,7 +20,7 @@ use uv_redacted::DisplaySafeUrl; use uv_resolver::{AnnotationStyle, ExcludeNewer, ForkStrategy, PrereleaseMode, ResolutionMode}; use uv_static::EnvVars; use uv_torch::TorchMode; -use uv_workspace::pyproject_mut::AddBoundsKind; +use uv_workspace::{pyproject::ExtraBuildDependencies, pyproject_mut::AddBoundsKind}; /// A `pyproject.toml` with an (optional) `[tool.uv]` section. #[allow(dead_code)] @@ -372,6 +372,7 @@ pub struct ResolverOptions { pub no_binary_package: Option>, pub no_build_isolation: Option, pub no_build_isolation_package: Option>, + pub extra_build_dependencies: Option, pub no_sources: Option, } @@ -624,6 +625,20 @@ pub struct ResolverInstallerOptions { "# )] pub no_build_isolation_package: Option>, + /// Additional build dependencies for dependencies. + /// + /// This is intended for enabling more packages to be built with + /// build-isolation, by adding dependencies that they ambiently + /// assume to exist (`setuptools` and `pip` being common). + #[option( + default = "[]", + value_type = "dict", + example = r#" + [extra-build-dependencies] + pytest = ["setuptools"] + "# + )] + pub extra_build_dependencies: Option, /// Limit candidate packages to those that were uploaded prior to a given point in time. /// /// Accepts a superset of [RFC 3339](https://www.rfc-editor.org/rfc/rfc3339.html) (e.g., @@ -1120,6 +1135,20 @@ pub struct PipOptions { "# )] pub no_build_isolation_package: Option>, + /// Additional build dependencies for dependencies. + /// + /// This is intended for enabling more packages to be built with + /// build-isolation, by adding dependencies that they ambiently + /// assume to exist (`setuptools` and `pip` being common). + #[option( + default = "[]", + value_type = "dict", + example = r#" + [extra-build-dependencies] + pytest = ["setuptools"] + "# + )] + pub extra_build_dependencies: Option, /// Validate the Python environment, to detect packages with missing dependencies and other /// issues. #[option( @@ -1685,6 +1714,7 @@ impl From for ResolverOptions { no_binary_package: value.no_binary_package, no_build_isolation: value.no_build_isolation, no_build_isolation_package: value.no_build_isolation_package, + extra_build_dependencies: value.extra_build_dependencies, no_sources: value.no_sources, } } @@ -1741,6 +1771,7 @@ pub struct ToolOptions { pub config_settings_package: Option, pub no_build_isolation: Option, pub no_build_isolation_package: Option>, + pub extra_build_dependencies: Option, pub exclude_newer: Option, pub link_mode: Option, pub compile_bytecode: Option, @@ -1769,6 +1800,7 @@ impl From for ToolOptions { config_settings_package: value.config_settings_package, no_build_isolation: value.no_build_isolation, no_build_isolation_package: value.no_build_isolation_package, + extra_build_dependencies: value.extra_build_dependencies, exclude_newer: value.exclude_newer, link_mode: value.link_mode, compile_bytecode: value.compile_bytecode, @@ -1799,6 +1831,7 @@ impl From for ResolverInstallerOptions { config_settings_package: value.config_settings_package, no_build_isolation: value.no_build_isolation, no_build_isolation_package: value.no_build_isolation_package, + extra_build_dependencies: value.extra_build_dependencies, exclude_newer: value.exclude_newer, link_mode: value.link_mode, compile_bytecode: value.compile_bytecode, @@ -1852,6 +1885,7 @@ pub struct OptionsWire { config_settings_package: Option, no_build_isolation: Option, no_build_isolation_package: Option>, + extra_build_dependencies: Option, exclude_newer: Option, link_mode: Option, compile_bytecode: Option, @@ -1969,6 +2003,7 @@ impl From for Options { sources, default_groups, dependency_groups, + extra_build_dependencies, dev_dependencies, managed, package, @@ -2009,6 +2044,7 @@ impl From for Options { config_settings_package, no_build_isolation, no_build_isolation_package, + extra_build_dependencies, exclude_newer, link_mode, compile_bytecode, diff --git a/crates/uv-workspace/src/pyproject.rs b/crates/uv-workspace/src/pyproject.rs index 4a994b801..90b2fb9cc 100644 --- a/crates/uv-workspace/src/pyproject.rs +++ b/crates/uv-workspace/src/pyproject.rs @@ -377,6 +377,21 @@ pub struct ToolUv { )] pub dependency_groups: Option, + /// Additional build dependencies for dependencies. + /// + /// This is intended for enabling more packages to be built with + /// build-isolation, by adding dependencies that they ambiently + /// assume to exist (`setuptools` and `pip` being common). + #[option( + default = "[]", + value_type = "dict", + example = r#" + [tool.uv.extra-build-dependencies] + pytest = ["pip"] + "# + )] + pub extra_build_dependencies: Option, + /// The project's development dependencies. /// /// Development dependencies will be installed by default in `uv run` and `uv sync`, but will @@ -748,6 +763,70 @@ pub struct DependencyGroupSettings { pub requires_python: Option, } +pub type ExtraBuildDependencies = + BTreeMap>>; + +#[derive(Default, Debug, Clone, PartialEq, Eq)] +#[cfg_attr(test, derive(Serialize))] +#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +pub struct ToolUvExtraBuildDependencies(ExtraBuildDependencies); + +impl ToolUvExtraBuildDependencies { + /// Returns the underlying `BTreeMap` of group names to settings. + pub fn inner(&self) -> &ExtraBuildDependencies { + &self.0 + } + + /// Convert the [`ToolUvExtraBuildDependencies`] into its inner `BTreeMap`. + #[must_use] + pub fn into_inner(self) -> ExtraBuildDependencies { + self.0 + } +} + +/// Ensure that all keys in the TOML table are unique. +impl<'de> serde::de::Deserialize<'de> for ToolUvExtraBuildDependencies { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + struct DependenciesVisitor; + + impl<'de> serde::de::Visitor<'de> for DependenciesVisitor { + type Value = ToolUvExtraBuildDependencies; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("a map with unique keys") + } + + fn visit_map(self, mut access: M) -> Result + where + M: serde::de::MapAccess<'de>, + { + let mut groups = BTreeMap::new(); + while let Some((key, value)) = access + .next_entry::>>()? + { + match groups.entry(key) { + std::collections::btree_map::Entry::Occupied(entry) => { + return Err(serde::de::Error::custom(format!( + "duplicate extra-build-dependencies for `{}`", + entry.key() + ))); + } + std::collections::btree_map::Entry::Vacant(entry) => { + entry.insert(value); + } + } + } + Ok(ToolUvExtraBuildDependencies(groups)) + } + } + + deserializer.deserialize_map(DependenciesVisitor) + } +} + #[derive(Deserialize, OptionsMetadata, Default, Debug, Clone, PartialEq, Eq)] #[cfg_attr(test, derive(Serialize))] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] diff --git a/crates/uv/src/commands/build_frontend.rs b/crates/uv/src/commands/build_frontend.rs index b3f9e5c89..a367b7a2e 100644 --- a/crates/uv/src/commands/build_frontend.rs +++ b/crates/uv/src/commands/build_frontend.rs @@ -38,6 +38,7 @@ use uv_requirements::RequirementsSource; use uv_resolver::{ExcludeNewer, FlatIndex}; use uv_settings::PythonInstallMirrors; use uv_types::{AnyErrorBuild, BuildContext, BuildIsolation, BuildStack, HashStrategy}; +use uv_workspace::pyproject::ExtraBuildDependencies; use uv_workspace::{DiscoveryOptions, Workspace, WorkspaceCache, WorkspaceError}; use crate::commands::ExitStatus; @@ -200,6 +201,7 @@ async fn build_impl( config_settings_package, no_build_isolation, no_build_isolation_package, + extra_build_dependencies, exclude_newer, link_mode, upgrade: _, @@ -346,6 +348,7 @@ async fn build_impl( build_constraints, *no_build_isolation, no_build_isolation_package, + extra_build_dependencies, *index_strategy, *keyring_provider, *exclude_newer, @@ -424,6 +427,7 @@ async fn build_package( build_constraints: &[RequirementsSource], no_build_isolation: bool, no_build_isolation_package: &[PackageName], + extra_build_dependencies: &ExtraBuildDependencies, index_strategy: IndexStrategy, keyring_provider: KeyringProviderType, exclude_newer: Option, @@ -573,6 +577,7 @@ async fn build_package( config_setting, config_settings_package, build_isolation, + extra_build_dependencies, link_mode, build_options, &hasher, diff --git a/crates/uv/src/commands/pip/compile.rs b/crates/uv/src/commands/pip/compile.rs index a5116327b..15e42c2ab 100644 --- a/crates/uv/src/commands/pip/compile.rs +++ b/crates/uv/src/commands/pip/compile.rs @@ -45,6 +45,7 @@ use uv_torch::{TorchMode, TorchStrategy}; use uv_types::{BuildIsolation, EmptyInstalledPackages, HashStrategy}; use uv_warnings::warn_user; use uv_workspace::WorkspaceCache; +use uv_workspace::pyproject::ExtraBuildDependencies; use crate::commands::pip::loggers::DefaultResolveLogger; use crate::commands::pip::{operations, resolution_environment}; @@ -94,6 +95,7 @@ pub(crate) async fn pip_compile( config_settings_package: PackageConfigSettings, no_build_isolation: bool, no_build_isolation_package: Vec, + extra_build_dependencies: &ExtraBuildDependencies, build_options: BuildOptions, mut python_version: Option, python_platform: Option, @@ -481,6 +483,7 @@ pub(crate) async fn pip_compile( &config_settings, &config_settings_package, build_isolation, + extra_build_dependencies, link_mode, &build_options, &build_hashes, diff --git a/crates/uv/src/commands/pip/install.rs b/crates/uv/src/commands/pip/install.rs index 79e18bd98..86328e7ad 100644 --- a/crates/uv/src/commands/pip/install.rs +++ b/crates/uv/src/commands/pip/install.rs @@ -39,6 +39,7 @@ use uv_torch::{TorchMode, TorchStrategy}; use uv_types::{BuildIsolation, HashStrategy}; use uv_warnings::warn_user; use uv_workspace::WorkspaceCache; +use uv_workspace::pyproject::ExtraBuildDependencies; use crate::commands::pip::loggers::{DefaultInstallLogger, DefaultResolveLogger, InstallLogger}; use crate::commands::pip::operations::Modifications; @@ -79,6 +80,7 @@ pub(crate) async fn pip_install( config_settings_package: &PackageConfigSettings, no_build_isolation: bool, no_build_isolation_package: Vec, + extra_build_dependencies: &ExtraBuildDependencies, build_options: BuildOptions, modifications: Modifications, python_version: Option, @@ -426,6 +428,7 @@ pub(crate) async fn pip_install( config_settings, config_settings_package, build_isolation, + extra_build_dependencies, link_mode, &build_options, &build_hasher, diff --git a/crates/uv/src/commands/pip/sync.rs b/crates/uv/src/commands/pip/sync.rs index 61999825e..c4f5c0aab 100644 --- a/crates/uv/src/commands/pip/sync.rs +++ b/crates/uv/src/commands/pip/sync.rs @@ -33,6 +33,7 @@ use uv_torch::{TorchMode, TorchStrategy}; use uv_types::{BuildIsolation, HashStrategy}; use uv_warnings::warn_user; use uv_workspace::WorkspaceCache; +use uv_workspace::pyproject::ExtraBuildDependencies; use crate::commands::pip::loggers::{DefaultInstallLogger, DefaultResolveLogger}; use crate::commands::pip::operations::Modifications; @@ -64,6 +65,7 @@ pub(crate) async fn pip_sync( config_settings_package: &PackageConfigSettings, no_build_isolation: bool, no_build_isolation_package: Vec, + extra_build_dependencies: &ExtraBuildDependencies, build_options: BuildOptions, python_version: Option, python_platform: Option, @@ -359,6 +361,7 @@ pub(crate) async fn pip_sync( config_settings, config_settings_package, build_isolation, + extra_build_dependencies, link_mode, &build_options, &build_hasher, diff --git a/crates/uv/src/commands/project/add.rs b/crates/uv/src/commands/project/add.rs index 12535f859..2f024418e 100644 --- a/crates/uv/src/commands/project/add.rs +++ b/crates/uv/src/commands/project/add.rs @@ -438,6 +438,7 @@ pub(crate) async fn add( &settings.resolver.config_setting, &settings.resolver.config_settings_package, build_isolation, + &settings.resolver.extra_build_dependencies, settings.resolver.link_mode, &settings.resolver.build_options, &build_hasher, diff --git a/crates/uv/src/commands/project/lock.rs b/crates/uv/src/commands/project/lock.rs index 706c86593..77565ba84 100644 --- a/crates/uv/src/commands/project/lock.rs +++ b/crates/uv/src/commands/project/lock.rs @@ -435,6 +435,7 @@ async fn do_lock( config_settings_package, no_build_isolation, no_build_isolation_package, + extra_build_dependencies, exclude_newer, link_mode, upgrade, @@ -677,6 +678,7 @@ async fn do_lock( config_setting, config_settings_package, build_isolation, + extra_build_dependencies, *link_mode, build_options, &build_hasher, diff --git a/crates/uv/src/commands/project/mod.rs b/crates/uv/src/commands/project/mod.rs index becd2a26e..6c5ea1e6f 100644 --- a/crates/uv/src/commands/project/mod.rs +++ b/crates/uv/src/commands/project/mod.rs @@ -1684,6 +1684,7 @@ pub(crate) async fn resolve_names( link_mode, no_build_isolation, no_build_isolation_package, + extra_build_dependencies, prerelease: _, resolution: _, sources, @@ -1745,6 +1746,7 @@ pub(crate) async fn resolve_names( config_setting, config_settings_package, build_isolation, + extra_build_dependencies, *link_mode, build_options, &build_hasher, @@ -1837,6 +1839,7 @@ pub(crate) async fn resolve_environment( config_settings_package, no_build_isolation, no_build_isolation_package, + extra_build_dependencies, exclude_newer, link_mode, upgrade: _, @@ -1953,6 +1956,7 @@ pub(crate) async fn resolve_environment( config_setting, config_settings_package, build_isolation, + extra_build_dependencies, *link_mode, build_options, &build_hasher, @@ -2020,6 +2024,7 @@ pub(crate) async fn sync_environment( config_settings_package, no_build_isolation, no_build_isolation_package, + extra_build_dependencies, exclude_newer, link_mode, compile_bytecode, @@ -2091,6 +2096,7 @@ pub(crate) async fn sync_environment( config_setting, config_settings_package, build_isolation, + extra_build_dependencies, link_mode, build_options, &build_hasher, @@ -2186,6 +2192,7 @@ pub(crate) async fn update_environment( link_mode, no_build_isolation, no_build_isolation_package, + extra_build_dependencies, prerelease, resolution, sources, @@ -2315,6 +2322,7 @@ pub(crate) async fn update_environment( config_setting, config_settings_package, build_isolation, + extra_build_dependencies, *link_mode, build_options, &build_hasher, diff --git a/crates/uv/src/commands/project/sync.rs b/crates/uv/src/commands/project/sync.rs index adf3b61f2..a5ccf85e6 100644 --- a/crates/uv/src/commands/project/sync.rs +++ b/crates/uv/src/commands/project/sync.rs @@ -576,6 +576,7 @@ pub(super) async fn do_sync( config_settings_package, no_build_isolation, no_build_isolation_package, + extra_build_dependencies, exclude_newer, link_mode, compile_bytecode, @@ -712,6 +713,7 @@ pub(super) async fn do_sync( config_setting, config_settings_package, build_isolation, + extra_build_dependencies, link_mode, build_options, &build_hasher, diff --git a/crates/uv/src/commands/project/tree.rs b/crates/uv/src/commands/project/tree.rs index 756820dc7..15d0c990b 100644 --- a/crates/uv/src/commands/project/tree.rs +++ b/crates/uv/src/commands/project/tree.rs @@ -203,6 +203,7 @@ pub(crate) async fn tree( config_settings_package: _, no_build_isolation: _, no_build_isolation_package: _, + extra_build_dependencies: _, exclude_newer: _, link_mode: _, upgrade: _, diff --git a/crates/uv/src/commands/venv.rs b/crates/uv/src/commands/venv.rs index 9d3b87fe1..116dc0553 100644 --- a/crates/uv/src/commands/venv.rs +++ b/crates/uv/src/commands/venv.rs @@ -29,6 +29,7 @@ use uv_shell::{Shell, shlex_posix, shlex_windows}; use uv_types::{AnyErrorBuild, BuildContext, BuildIsolation, BuildStack, HashStrategy}; use uv_virtualenv::OnExisting; use uv_warnings::warn_user; +use uv_workspace::pyproject::ExtraBuildDependencies; use uv_workspace::{DiscoveryOptions, VirtualProject, WorkspaceCache, WorkspaceError}; use crate::commands::ExitStatus; @@ -274,7 +275,7 @@ pub(crate) async fn venv( // Do not allow builds let build_options = BuildOptions::new(NoBinary::None, NoBuild::All); - + let extra_build_dependencies = ExtraBuildDependencies::default(); // Prep the build context. let build_dispatch = BuildDispatch::new( &client, @@ -289,6 +290,7 @@ pub(crate) async fn venv( &config_settings, &config_settings_package, BuildIsolation::Isolated, + &extra_build_dependencies, link_mode, &build_options, &build_hasher, diff --git a/crates/uv/src/lib.rs b/crates/uv/src/lib.rs index 0f6c9465f..c2af77086 100644 --- a/crates/uv/src/lib.rs +++ b/crates/uv/src/lib.rs @@ -527,6 +527,7 @@ async fn run(mut cli: Cli) -> Result { args.settings.config_settings_package, args.settings.no_build_isolation, args.settings.no_build_isolation_package, + &args.settings.extra_build_dependencies, args.settings.build_options, args.settings.python_version, args.settings.python_platform, @@ -598,6 +599,7 @@ async fn run(mut cli: Cli) -> Result { &args.settings.config_settings_package, args.settings.no_build_isolation, args.settings.no_build_isolation_package, + &args.settings.extra_build_dependencies, args.settings.build_options, args.settings.python_version, args.settings.python_platform, @@ -750,6 +752,7 @@ async fn run(mut cli: Cli) -> Result { &args.settings.config_settings_package, args.settings.no_build_isolation, args.settings.no_build_isolation_package, + &args.settings.extra_build_dependencies, args.settings.build_options, args.modifications, args.settings.python_version, diff --git a/crates/uv/src/settings.rs b/crates/uv/src/settings.rs index aa105cf97..d14d3acdc 100644 --- a/crates/uv/src/settings.rs +++ b/crates/uv/src/settings.rs @@ -44,7 +44,7 @@ use uv_settings::{ use uv_static::EnvVars; use uv_torch::TorchMode; use uv_warnings::warn_user_once; -use uv_workspace::pyproject::DependencyType; +use uv_workspace::pyproject::{DependencyType, ExtraBuildDependencies}; use uv_workspace::pyproject_mut::AddBoundsKind; use crate::commands::ToolRunCommand; @@ -2699,6 +2699,7 @@ pub(crate) struct InstallerSettingsRef<'a> { pub(crate) config_settings_package: &'a PackageConfigSettings, pub(crate) no_build_isolation: bool, pub(crate) no_build_isolation_package: &'a [PackageName], + pub(crate) extra_build_dependencies: &'a ExtraBuildDependencies, pub(crate) exclude_newer: Option, pub(crate) link_mode: LinkMode, pub(crate) compile_bytecode: bool, @@ -2725,6 +2726,7 @@ pub(crate) struct ResolverSettings { pub(crate) link_mode: LinkMode, pub(crate) no_build_isolation: bool, pub(crate) no_build_isolation_package: Vec, + pub(crate) extra_build_dependencies: ExtraBuildDependencies, pub(crate) prerelease: PrereleaseMode, pub(crate) resolution: ResolutionMode, pub(crate) sources: SourceStrategy, @@ -2777,6 +2779,7 @@ impl From for ResolverSettings { 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(), + extra_build_dependencies: value.extra_build_dependencies.unwrap_or_default(), exclude_newer: value.exclude_newer, link_mode: value.link_mode.unwrap_or_default(), sources: SourceStrategy::from_args(value.no_sources.unwrap_or_default()), @@ -2866,6 +2869,7 @@ impl From for ResolverInstallerSettings { link_mode: value.link_mode.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(), + extra_build_dependencies: value.extra_build_dependencies.unwrap_or_default(), prerelease: value.prerelease.unwrap_or_default(), resolution: value.resolution.unwrap_or_default(), sources: SourceStrategy::from_args(value.no_sources.unwrap_or_default()), @@ -2908,6 +2912,7 @@ pub(crate) struct PipSettings { pub(crate) torch_backend: Option, pub(crate) no_build_isolation: bool, pub(crate) no_build_isolation_package: Vec, + pub(crate) extra_build_dependencies: ExtraBuildDependencies, pub(crate) build_options: BuildOptions, pub(crate) allow_empty_requirements: bool, pub(crate) strict: bool, @@ -2975,6 +2980,7 @@ impl PipSettings { only_binary, no_build_isolation, no_build_isolation_package, + extra_build_dependencies, strict, extra, all_extras, @@ -3033,6 +3039,7 @@ impl PipSettings { 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, + extra_build_dependencies: top_level_extra_build_dependencies, exclude_newer: top_level_exclude_newer, link_mode: top_level_link_mode, compile_bytecode: top_level_compile_bytecode, @@ -3068,6 +3075,8 @@ impl PipSettings { 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); + let extra_build_dependencies = + extra_build_dependencies.combine(top_level_extra_build_dependencies); let exclude_newer = exclude_newer.combine(top_level_exclude_newer); let link_mode = link_mode.combine(top_level_link_mode); let compile_bytecode = compile_bytecode.combine(top_level_compile_bytecode); @@ -3163,6 +3172,10 @@ impl PipSettings { .no_build_isolation_package .combine(no_build_isolation_package) .unwrap_or_default(), + extra_build_dependencies: args + .extra_build_dependencies + .combine(extra_build_dependencies) + .unwrap_or_default(), config_setting: args .config_settings .combine(config_settings) @@ -3267,6 +3280,7 @@ impl<'a> From<&'a ResolverInstallerSettings> for InstallerSettingsRef<'a> { 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, + extra_build_dependencies: &settings.resolver.extra_build_dependencies, exclude_newer: settings.resolver.exclude_newer, link_mode: settings.resolver.link_mode, compile_bytecode: settings.compile_bytecode, diff --git a/crates/uv/tests/it/sync.rs b/crates/uv/tests/it/sync.rs index 16c4d673a..37dea2842 100644 --- a/crates/uv/tests/it/sync.rs +++ b/crates/uv/tests/it/sync.rs @@ -1527,6 +1527,106 @@ fn sync_build_isolation_extra() -> Result<()> { Ok(()) } +/// Use dedicated extra groups to install dependencies for `--no-build-isolation-package`. +#[test] +fn sync_build_isolation_fail() -> Result<()> { + let context = TestContext::new("3.12").with_filtered_counts(); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str( + r#" + [project] + name = "myproject" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = ["fasttext==0.9.2"] + + [build-system] + requires = ["setuptools >= 40.9.0"] + build-backend = "setuptools.build_meta" + "#, + )?; + let filters = std::iter::once((r"exit code: 1", "exit status: 1")) + .chain(context.filters()) + .collect::>(); + + // Running `uv sync` should fail due to missing build-dependencies + uv_snapshot!(&filters, context.sync(), @r#" + success: false + exit_code: 1 + ----- stdout ----- + + ----- stderr ----- + × Failed to build `fasttext==0.9.2` + ├─▶ The build backend returned an error + ╰─▶ Call to `setuptools.build_meta:__legacy__.build_wheel` failed (exit status: 1) + + [stderr] + [CACHE_DIR]/builds-v0/[TMP]/python: No module named pip + Traceback (most recent call last): + File "", line 38, in __init__ + ModuleNotFoundError: No module named 'pybind11' + + During handling of the above exception, another exception occurred: + + Traceback (most recent call last): + File "", line 14, in + File "[CACHE_DIR]/builds-v0/[TMP]/build_meta.py", line 325, in get_requires_for_build_wheel + return self._get_build_requires(config_settings, requirements=['wheel']) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "[CACHE_DIR]/builds-v0/[TMP]/build_meta.py", line 295, in _get_build_requires + self.run_setup() + File "[CACHE_DIR]/builds-v0/[TMP]/build_meta.py", line 487, in run_setup + super().run_setup(setup_script=setup_script) + File "[CACHE_DIR]/builds-v0/[TMP]/build_meta.py", line 311, in run_setup + exec(code, locals()) + File "", line 72, in + File "", line 41, in __init__ + RuntimeError: pybind11 install failed. + + hint: This usually indicates a problem with the package or the build environment. + help: `fasttext` (v0.9.2) was included because `myproject` (v0.1.0) depends on `fasttext==0.9.2` + "#); + + // Adding extra-build-dependencies should solve the issue + pyproject_toml.write_str( + r#" + [project] + name = "myproject" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = ["fasttext==0.9.2"] + + [build-system] + requires = ["setuptools >= 40.9.0"] + build-backend = "setuptools.build_meta" + + [tool.uv.extra-build-dependencies] + fasttext = ["setuptools", "wheel", "pybind11"] + "#, + )?; + + uv_snapshot!(&filters, context.sync(), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved [N] packages in [TIME] + Prepared [N] packages in [TIME] + Installed [N] packages in [TIME] + + fasttext==0.9.2 + + myproject==0.1.0 (from file://[TEMP_DIR]/) + + numpy==1.26.4 + + pybind11==2.11.1 + + setuptools==69.2.0 + "); + + // assert!(context.temp_dir.child("uv.lock").exists()); + + Ok(()) +} + /// Avoid using incompatible versions for build dependencies that are also part of the resolved /// environment. This is a very subtle issue, but: when locking, we don't enforce platform /// compatibility. So, if we reuse the resolver state to install, and the install itself has to diff --git a/docs/reference/settings.md b/docs/reference/settings.md index 55d3f8ae4..7159eef1b 100644 --- a/docs/reference/settings.md +++ b/docs/reference/settings.md @@ -202,6 +202,28 @@ environments = ["sys_platform == 'darwin'"] --- +### [`extra-build-dependencies`](#extra-build-dependencies) {: #extra-build-dependencies } + +Additional build dependencies for dependencies. + +This is intended for enabling more packages to be built with +build-isolation, by adding dependencies that they ambiently +assume to exist (`setuptools` and `pip` being common). + +**Default value**: `[]` + +**Type**: `dict` + +**Example usage**: + +```toml title="pyproject.toml" + +[tool.uv.extra-build-dependencies] +pytest = ["pip"] +``` + +--- + ### [`index`](#index) {: #index } The indexes to use when resolving dependencies. @@ -1101,6 +1123,36 @@ behave consistently across timezones. --- +### [`extra-build-dependencies`](#extra-build-dependencies) {: #extra-build-dependencies } + +Additional build dependencies for dependencies. + +This is intended for enabling more packages to be built with +build-isolation, by adding dependencies that they ambiently +assume to exist (`setuptools` and `pip` being common). + +**Default value**: `[]` + +**Type**: `dict` + +**Example usage**: + +=== "pyproject.toml" + + ```toml + [tool.uv] + [extra-build-dependencies] + pytest = ["setuptools"] + ``` +=== "uv.toml" + + ```toml + [extra-build-dependencies] + pytest = ["setuptools"] + ``` + +--- + ### [`extra-index-url`](#extra-index-url) {: #extra-index-url } Extra URLs of package indexes to use, in addition to `--index-url`. @@ -2562,6 +2614,38 @@ Only applies to `pyproject.toml`, `setup.py`, and `setup.cfg` sources. --- +#### [`extra-build-dependencies`](#pip_extra-build-dependencies) {: #pip_extra-build-dependencies } + + +Additional build dependencies for dependencies. + +This is intended for enabling more packages to be built with +build-isolation, by adding dependencies that they ambiently +assume to exist (`setuptools` and `pip` being common). + +**Default value**: `[]` + +**Type**: `dict` + +**Example usage**: + +=== "pyproject.toml" + + ```toml + [tool.uv.pip] + [extra-build-dependencies] + pytest = ["setuptools"] + ``` +=== "uv.toml" + + ```toml + [pip] + [extra-build-dependencies] + pytest = ["setuptools"] + ``` + +--- + #### [`extra-index-url`](#pip_extra-index-url) {: #pip_extra-index-url } diff --git a/uv.schema.json b/uv.schema.json index 22b30cd06..5b01aaed7 100644 --- a/uv.schema.json +++ b/uv.schema.json @@ -214,6 +214,17 @@ } ] }, + "extra-build-dependencies": { + "description": "Additional build dependencies for dependencies.\n\nThis is intended for enabling more packages to be built with build-isolation, by adding dependencies that they ambiently assume to exist (`setuptools` and `pip` being common).", + "anyOf": [ + { + "$ref": "#/definitions/ToolUvExtraBuildDependencies" + }, + { + "type": "null" + } + ] + }, "extra-index-url": { "description": "Extra URLs of package indexes to use, in addition to `--index-url`.\n\nAccepts either a repository compliant with [PEP 503](https://peps.python.org/pep-0503/)\n(the simple repository API), or a local directory laid out in the same format.\n\nAll indexes provided via this flag take priority over the index specified by\n[`index_url`](#index-url) or [`index`](#index) with `default = true`. When multiple indexes\nare provided, earlier values take priority.\n\nTo control uv's resolution strategy when multiple indexes are present, see\n[`index_strategy`](#index-strategy).\n\n(Deprecated: use `index` instead.)", "type": [ @@ -1287,6 +1298,19 @@ "$ref": "#/definitions/ExtraName" } }, + "extra-build-dependencies": { + "description": "Additional build dependencies for dependencies.\n\nThis is intended for enabling more packages to be built with build-isolation, by adding dependencies that they ambiently assume to exist (`setuptools` and `pip` being common).", + "type": [ + "object", + "null" + ], + "additionalProperties": { + "type": "array", + "items": { + "$ref": "#/definitions/Requirement" + } + } + }, "extra-index-url": { "description": "Extra URLs of package indexes to use, in addition to `--index-url`.\n\nAccepts either a repository compliant with [PEP 503](https://peps.python.org/pep-0503/)\n(the simple repository API), or a local directory laid out in the same format.\n\nAll indexes provided via this flag take priority over the index specified by\n[`index_url`](#index-url). When multiple indexes are provided, earlier values take priority.\n\nTo control uv's resolution strategy when multiple indexes are present, see\n[`index_strategy`](#index-strategy).", "type": [ @@ -2315,6 +2339,15 @@ "$ref": "#/definitions/DependencyGroupSettings" } }, + "ToolUvExtraBuildDependencies": { + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "$ref": "#/definitions/Requirement" + } + } + }, "ToolUvSources": { "type": "object", "additionalProperties": { From 5108c1ecd0dbe03f5f089f7d8eec032d692f9307 Mon Sep 17 00:00:00 2001 From: Zanie Blue Date: Fri, 18 Jul 2025 08:15:55 -0500 Subject: [PATCH 02/14] snapshot regens --- crates/uv-workspace/src/workspace.rs | 6 +++++ crates/uv/tests/it/show_settings.rs | 38 +++++++++++++++++++++++++++- crates/uv/tests/it/sync.rs | 2 +- 3 files changed, 44 insertions(+), 2 deletions(-) diff --git a/crates/uv-workspace/src/workspace.rs b/crates/uv-workspace/src/workspace.rs index 09f2b692a..53342865e 100644 --- a/crates/uv-workspace/src/workspace.rs +++ b/crates/uv-workspace/src/workspace.rs @@ -1970,6 +1970,7 @@ mod tests { "package": null, "default-groups": null, "dependency-groups": null, + "extra-build-dependencies": null, "dev-dependencies": null, "override-dependencies": null, "constraint-dependencies": null, @@ -2070,6 +2071,7 @@ mod tests { "package": null, "default-groups": null, "dependency-groups": null, + "extra-build-dependencies": null, "dev-dependencies": null, "override-dependencies": null, "constraint-dependencies": null, @@ -2283,6 +2285,7 @@ mod tests { "package": null, "default-groups": null, "dependency-groups": null, + "extra-build-dependencies": null, "dev-dependencies": null, "override-dependencies": null, "constraint-dependencies": null, @@ -2392,6 +2395,7 @@ mod tests { "package": null, "default-groups": null, "dependency-groups": null, + "extra-build-dependencies": null, "dev-dependencies": null, "override-dependencies": null, "constraint-dependencies": null, @@ -2514,6 +2518,7 @@ mod tests { "package": null, "default-groups": null, "dependency-groups": null, + "extra-build-dependencies": null, "dev-dependencies": null, "override-dependencies": null, "constraint-dependencies": null, @@ -2610,6 +2615,7 @@ mod tests { "package": null, "default-groups": null, "dependency-groups": null, + "extra-build-dependencies": null, "dev-dependencies": null, "override-dependencies": null, "constraint-dependencies": null, diff --git a/crates/uv/tests/it/show_settings.rs b/crates/uv/tests/it/show_settings.rs index 500e78965..e984b2365 100644 --- a/crates/uv/tests/it/show_settings.rs +++ b/crates/uv/tests/it/show_settings.rs @@ -180,6 +180,7 @@ fn resolve_uv_toml() -> anyhow::Result<()> { torch_backend: None, no_build_isolation: false, no_build_isolation_package: [], + extra_build_dependencies: {}, build_options: BuildOptions { no_binary: None, no_build: None, @@ -365,6 +366,7 @@ fn resolve_uv_toml() -> anyhow::Result<()> { torch_backend: None, no_build_isolation: false, no_build_isolation_package: [], + extra_build_dependencies: {}, build_options: BuildOptions { no_binary: None, no_build: None, @@ -551,6 +553,7 @@ fn resolve_uv_toml() -> anyhow::Result<()> { torch_backend: None, no_build_isolation: false, no_build_isolation_package: [], + extra_build_dependencies: {}, build_options: BuildOptions { no_binary: None, no_build: None, @@ -769,6 +772,7 @@ fn resolve_pyproject_toml() -> anyhow::Result<()> { torch_backend: None, no_build_isolation: false, no_build_isolation_package: [], + extra_build_dependencies: {}, build_options: BuildOptions { no_binary: None, no_build: None, @@ -922,6 +926,7 @@ fn resolve_pyproject_toml() -> anyhow::Result<()> { torch_backend: None, no_build_isolation: false, no_build_isolation_package: [], + extra_build_dependencies: {}, build_options: BuildOptions { no_binary: None, no_build: None, @@ -1119,6 +1124,7 @@ fn resolve_pyproject_toml() -> anyhow::Result<()> { torch_backend: None, no_build_isolation: false, no_build_isolation_package: [], + extra_build_dependencies: {}, build_options: BuildOptions { no_binary: None, no_build: None, @@ -1364,6 +1370,7 @@ fn resolve_index_url() -> anyhow::Result<()> { torch_backend: None, no_build_isolation: false, no_build_isolation_package: [], + extra_build_dependencies: {}, build_options: BuildOptions { no_binary: None, no_build: None, @@ -1619,6 +1626,7 @@ fn resolve_index_url() -> anyhow::Result<()> { torch_backend: None, no_build_isolation: false, no_build_isolation_package: [], + extra_build_dependencies: {}, build_options: BuildOptions { no_binary: None, no_build: None, @@ -1829,6 +1837,7 @@ fn resolve_find_links() -> anyhow::Result<()> { torch_backend: None, no_build_isolation: false, no_build_isolation_package: [], + extra_build_dependencies: {}, build_options: BuildOptions { no_binary: None, no_build: None, @@ -2004,6 +2013,7 @@ fn resolve_top_level() -> anyhow::Result<()> { torch_backend: None, no_build_isolation: false, no_build_isolation_package: [], + extra_build_dependencies: {}, build_options: BuildOptions { no_binary: None, no_build: None, @@ -2239,6 +2249,7 @@ fn resolve_top_level() -> anyhow::Result<()> { torch_backend: None, no_build_isolation: false, no_build_isolation_package: [], + extra_build_dependencies: {}, build_options: BuildOptions { no_binary: None, no_build: None, @@ -2457,6 +2468,7 @@ fn resolve_top_level() -> anyhow::Result<()> { torch_backend: None, no_build_isolation: false, no_build_isolation_package: [], + extra_build_dependencies: {}, build_options: BuildOptions { no_binary: None, no_build: None, @@ -2631,6 +2643,7 @@ fn resolve_user_configuration() -> anyhow::Result<()> { torch_backend: None, no_build_isolation: false, no_build_isolation_package: [], + extra_build_dependencies: {}, build_options: BuildOptions { no_binary: None, no_build: None, @@ -2789,6 +2802,7 @@ fn resolve_user_configuration() -> anyhow::Result<()> { torch_backend: None, no_build_isolation: false, no_build_isolation_package: [], + extra_build_dependencies: {}, build_options: BuildOptions { no_binary: None, no_build: None, @@ -2947,6 +2961,7 @@ fn resolve_user_configuration() -> anyhow::Result<()> { torch_backend: None, no_build_isolation: false, no_build_isolation_package: [], + extra_build_dependencies: {}, build_options: BuildOptions { no_binary: None, no_build: None, @@ -3107,6 +3122,7 @@ fn resolve_user_configuration() -> anyhow::Result<()> { torch_backend: None, no_build_isolation: false, no_build_isolation_package: [], + extra_build_dependencies: {}, build_options: BuildOptions { no_binary: None, no_build: None, @@ -3259,6 +3275,7 @@ fn resolve_tool() -> anyhow::Result<()> { config_settings_package: None, no_build_isolation: None, no_build_isolation_package: None, + extra_build_dependencies: None, exclude_newer: None, link_mode: Some( Clone, @@ -3301,6 +3318,7 @@ fn resolve_tool() -> anyhow::Result<()> { link_mode: Clone, no_build_isolation: false, no_build_isolation_package: [], + extra_build_dependencies: {}, prerelease: IfNecessaryOrExplicit, resolution: LowestDirect, sources: Enabled, @@ -3455,6 +3473,7 @@ fn resolve_poetry_toml() -> anyhow::Result<()> { torch_backend: None, no_build_isolation: false, no_build_isolation_package: [], + extra_build_dependencies: {}, build_options: BuildOptions { no_binary: None, no_build: None, @@ -3675,6 +3694,7 @@ fn resolve_both() -> anyhow::Result<()> { torch_backend: None, no_build_isolation: false, no_build_isolation_package: [], + extra_build_dependencies: {}, build_options: BuildOptions { no_binary: None, no_build: None, @@ -3985,6 +4005,7 @@ fn resolve_config_file() -> anyhow::Result<()> { torch_backend: None, no_build_isolation: false, no_build_isolation_package: [], + extra_build_dependencies: {}, build_options: BuildOptions { no_binary: None, no_build: None, @@ -4065,7 +4086,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`, `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` + 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`, `extra-build-dependencies`, `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` " ); @@ -4237,6 +4258,7 @@ fn resolve_skip_empty() -> anyhow::Result<()> { torch_backend: None, no_build_isolation: false, no_build_isolation_package: [], + extra_build_dependencies: {}, build_options: BuildOptions { no_binary: None, no_build: None, @@ -4398,6 +4420,7 @@ fn resolve_skip_empty() -> anyhow::Result<()> { torch_backend: None, no_build_isolation: false, no_build_isolation_package: [], + extra_build_dependencies: {}, build_options: BuildOptions { no_binary: None, no_build: None, @@ -4578,6 +4601,7 @@ fn allow_insecure_host() -> anyhow::Result<()> { torch_backend: None, no_build_isolation: false, no_build_isolation_package: [], + extra_build_dependencies: {}, build_options: BuildOptions { no_binary: None, no_build: None, @@ -4819,6 +4843,7 @@ fn index_priority() -> anyhow::Result<()> { torch_backend: None, no_build_isolation: false, no_build_isolation_package: [], + extra_build_dependencies: {}, build_options: BuildOptions { no_binary: None, no_build: None, @@ -5039,6 +5064,7 @@ fn index_priority() -> anyhow::Result<()> { torch_backend: None, no_build_isolation: false, no_build_isolation_package: [], + extra_build_dependencies: {}, build_options: BuildOptions { no_binary: None, no_build: None, @@ -5265,6 +5291,7 @@ fn index_priority() -> anyhow::Result<()> { torch_backend: None, no_build_isolation: false, no_build_isolation_package: [], + extra_build_dependencies: {}, build_options: BuildOptions { no_binary: None, no_build: None, @@ -5486,6 +5513,7 @@ fn index_priority() -> anyhow::Result<()> { torch_backend: None, no_build_isolation: false, no_build_isolation_package: [], + extra_build_dependencies: {}, build_options: BuildOptions { no_binary: None, no_build: None, @@ -5714,6 +5742,7 @@ fn index_priority() -> anyhow::Result<()> { torch_backend: None, no_build_isolation: false, no_build_isolation_package: [], + extra_build_dependencies: {}, build_options: BuildOptions { no_binary: None, no_build: None, @@ -5935,6 +5964,7 @@ fn index_priority() -> anyhow::Result<()> { torch_backend: None, no_build_isolation: false, no_build_isolation_package: [], + extra_build_dependencies: {}, build_options: BuildOptions { no_binary: None, no_build: None, @@ -6100,6 +6130,7 @@ fn verify_hashes() -> anyhow::Result<()> { torch_backend: None, no_build_isolation: false, no_build_isolation_package: [], + extra_build_dependencies: {}, build_options: BuildOptions { no_binary: None, no_build: None, @@ -6251,6 +6282,7 @@ fn verify_hashes() -> anyhow::Result<()> { torch_backend: None, no_build_isolation: false, no_build_isolation_package: [], + extra_build_dependencies: {}, build_options: BuildOptions { no_binary: None, no_build: None, @@ -6400,6 +6432,7 @@ fn verify_hashes() -> anyhow::Result<()> { torch_backend: None, no_build_isolation: false, no_build_isolation_package: [], + extra_build_dependencies: {}, build_options: BuildOptions { no_binary: None, no_build: None, @@ -6551,6 +6584,7 @@ fn verify_hashes() -> anyhow::Result<()> { torch_backend: None, no_build_isolation: false, no_build_isolation_package: [], + extra_build_dependencies: {}, build_options: BuildOptions { no_binary: None, no_build: None, @@ -6700,6 +6734,7 @@ fn verify_hashes() -> anyhow::Result<()> { torch_backend: None, no_build_isolation: false, no_build_isolation_package: [], + extra_build_dependencies: {}, build_options: BuildOptions { no_binary: None, no_build: None, @@ -6850,6 +6885,7 @@ fn verify_hashes() -> anyhow::Result<()> { torch_backend: None, no_build_isolation: false, no_build_isolation_package: [], + extra_build_dependencies: {}, build_options: BuildOptions { no_binary: None, no_build: None, diff --git a/crates/uv/tests/it/sync.rs b/crates/uv/tests/it/sync.rs index 37dea2842..ccefdb0d4 100644 --- a/crates/uv/tests/it/sync.rs +++ b/crates/uv/tests/it/sync.rs @@ -1622,7 +1622,7 @@ fn sync_build_isolation_fail() -> Result<()> { + setuptools==69.2.0 "); - // assert!(context.temp_dir.child("uv.lock").exists()); + assert!(context.temp_dir.child("uv.lock").exists()); Ok(()) } From 190a40cd36fbadf7750400efb23ef169292c86fe Mon Sep 17 00:00:00 2001 From: konstin Date: Wed, 16 Jul 2025 09:22:33 +0200 Subject: [PATCH 03/14] Use faster test case --- crates/uv/tests/it/sync.rs | 94 +++++++++++++++----------------------- 1 file changed, 38 insertions(+), 56 deletions(-) diff --git a/crates/uv/tests/it/sync.rs b/crates/uv/tests/it/sync.rs index ccefdb0d4..44acaf2ab 100644 --- a/crates/uv/tests/it/sync.rs +++ b/crates/uv/tests/it/sync.rs @@ -1533,80 +1533,66 @@ fn sync_build_isolation_fail() -> Result<()> { let context = TestContext::new("3.12").with_filtered_counts(); let pyproject_toml = context.temp_dir.child("pyproject.toml"); - pyproject_toml.write_str( - r#" + pyproject_toml.write_str(indoc! {r#" [project] - name = "myproject" + name = "project" version = "0.1.0" - requires-python = ">=3.12" - dependencies = ["fasttext==0.9.2"] + requires-python = ">=3.9" [build-system] - requires = ["setuptools >= 40.9.0"] - build-backend = "setuptools.build_meta" - "#, - )?; - let filters = std::iter::once((r"exit code: 1", "exit status: 1")) - .chain(context.filters()) - .collect::>(); + requires = ["hatchling"] + backend-path = ["."] + build-backend = "build_backend" + "#})?; + let build_backend = context.temp_dir.child("build_backend.py"); + build_backend.write_str(indoc! {r#" + import sys + + from hatchling.build import * + + try: + import anyio + except ModuleNotFoundError: + print("Missing `anyio` module to build package", file=sys.stderr) + sys.exit(1) + "#})?; + context.temp_dir.child("src/project/__init__.py").touch()?; // Running `uv sync` should fail due to missing build-dependencies - uv_snapshot!(&filters, context.sync(), @r#" + uv_snapshot!(context.filters(), context.sync(), @r" success: false exit_code: 1 ----- stdout ----- ----- stderr ----- - × Failed to build `fasttext==0.9.2` + Resolved [N] packages in [TIME] + × Failed to build `project @ file://[TEMP_DIR]/` ├─▶ The build backend returned an error - ╰─▶ Call to `setuptools.build_meta:__legacy__.build_wheel` failed (exit status: 1) + ╰─▶ Call to `build_backend.build_editable` failed (exit status: 1) [stderr] - [CACHE_DIR]/builds-v0/[TMP]/python: No module named pip - Traceback (most recent call last): - File "", line 38, in __init__ - ModuleNotFoundError: No module named 'pybind11' - - During handling of the above exception, another exception occurred: - - Traceback (most recent call last): - File "", line 14, in - File "[CACHE_DIR]/builds-v0/[TMP]/build_meta.py", line 325, in get_requires_for_build_wheel - return self._get_build_requires(config_settings, requirements=['wheel']) - ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - File "[CACHE_DIR]/builds-v0/[TMP]/build_meta.py", line 295, in _get_build_requires - self.run_setup() - File "[CACHE_DIR]/builds-v0/[TMP]/build_meta.py", line 487, in run_setup - super().run_setup(setup_script=setup_script) - File "[CACHE_DIR]/builds-v0/[TMP]/build_meta.py", line 311, in run_setup - exec(code, locals()) - File "", line 72, in - File "", line 41, in __init__ - RuntimeError: pybind11 install failed. + Missing `anyio` module to build package hint: This usually indicates a problem with the package or the build environment. - help: `fasttext` (v0.9.2) was included because `myproject` (v0.1.0) depends on `fasttext==0.9.2` - "#); + "); // Adding extra-build-dependencies should solve the issue - pyproject_toml.write_str( - r#" + pyproject_toml.write_str(indoc! {r#" [project] - name = "myproject" + name = "project" version = "0.1.0" - requires-python = ">=3.12" - dependencies = ["fasttext==0.9.2"] + requires-python = ">=3.9" [build-system] - requires = ["setuptools >= 40.9.0"] - build-backend = "setuptools.build_meta" - - [tool.uv.extra-build-dependencies] - fasttext = ["setuptools", "wheel", "pybind11"] - "#, - )?; + requires = ["hatchling"] + backend-path = ["."] + build-backend = "build_backend" - uv_snapshot!(&filters, context.sync(), @r" + [tool.uv.extra-build-dependencies] + project = ["anyio"] + "#})?; + + uv_snapshot!(context.filters(), context.sync(), @r" success: true exit_code: 0 ----- stdout ----- @@ -1615,11 +1601,7 @@ fn sync_build_isolation_fail() -> Result<()> { Resolved [N] packages in [TIME] Prepared [N] packages in [TIME] Installed [N] packages in [TIME] - + fasttext==0.9.2 - + myproject==0.1.0 (from file://[TEMP_DIR]/) - + numpy==1.26.4 - + pybind11==2.11.1 - + setuptools==69.2.0 + + project==0.1.0 (from file://[TEMP_DIR]/) "); assert!(context.temp_dir.child("uv.lock").exists()); From 4ca5770ba13677eadb18e51afdf49828c0caaabb Mon Sep 17 00:00:00 2001 From: konstin Date: Wed, 16 Jul 2025 12:06:53 +0200 Subject: [PATCH 04/14] Update JSON schema --- uv.schema.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/uv.schema.json b/uv.schema.json index 5b01aaed7..3dd626452 100644 --- a/uv.schema.json +++ b/uv.schema.json @@ -215,7 +215,7 @@ ] }, "extra-build-dependencies": { - "description": "Additional build dependencies for dependencies.\n\nThis is intended for enabling more packages to be built with build-isolation, by adding dependencies that they ambiently assume to exist (`setuptools` and `pip` being common).", + "description": "Additional build dependencies for dependencies.\n\nThis is intended for enabling more packages to be built with\nbuild-isolation, by adding dependencies that they ambiently\nassume to exist (`setuptools` and `pip` being common).", "anyOf": [ { "$ref": "#/definitions/ToolUvExtraBuildDependencies" @@ -1299,7 +1299,7 @@ } }, "extra-build-dependencies": { - "description": "Additional build dependencies for dependencies.\n\nThis is intended for enabling more packages to be built with build-isolation, by adding dependencies that they ambiently assume to exist (`setuptools` and `pip` being common).", + "description": "Additional build dependencies for dependencies.\n\nThis is intended for enabling more packages to be built with\nbuild-isolation, by adding dependencies that they ambiently\nassume to exist (`setuptools` and `pip` being common).", "type": [ "object", "null" From a0876c43c3a3132c2195fe966933f905597fda1b Mon Sep 17 00:00:00 2001 From: Zanie Blue Date: Fri, 18 Jul 2025 11:30:01 -0500 Subject: [PATCH 05/14] Never apply sources --- crates/uv-build-frontend/src/lib.rs | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/crates/uv-build-frontend/src/lib.rs b/crates/uv-build-frontend/src/lib.rs index eab331908..a2f23a904 100644 --- a/crates/uv-build-frontend/src/lib.rs +++ b/crates/uv-build-frontend/src/lib.rs @@ -530,10 +530,7 @@ impl SourceBuild { .and_then(|name| extra_build_dependencies.get(name).cloned()) .unwrap_or_default(); - let backend = if let Some(mut build_system) = pyproject_toml.build_system { - // Apply extra-build-dependencies if there are any - build_system.requires.extend(extra_build_dependencies); - + let backend = if let Some(build_system) = pyproject_toml.build_system { // If necessary, lower the requirements. let requirements = match source_strategy { SourceStrategy::Enabled => { @@ -557,6 +554,9 @@ impl SourceBuild { .requires .into_iter() .map(Requirement::from) + .chain( + extra_build_dependencies.into_iter().map(Requirement::from), + ) .collect() } } @@ -564,6 +564,7 @@ impl SourceBuild { .requires .into_iter() .map(Requirement::from) + .chain(extra_build_dependencies.into_iter().map(Requirement::from)) .collect(), }; @@ -617,8 +618,7 @@ impl SourceBuild { } } let mut backend = default_backend.clone(); - // Apply extra_build_dependencies - // TODO(Gankra): should Sources/Indexes be applied on this path? + // Apply extra build dependencies backend .requirements .extend(extra_build_dependencies.into_iter().map(Requirement::from)); @@ -638,8 +638,7 @@ impl SourceBuild { // case, but plans to make PEP 517 builds the default in the future. // See: https://github.com/pypa/pip/issues/9175. let mut backend = default_backend.clone(); - // Apply extra_build_dependencies - // TODO(Gankra): should Sources/Indexes be applied on this path? + // Apply extra build dependencies let extra_build_dependencies = package_name .as_ref() .and_then(|name| extra_build_dependencies.get(name).cloned()) From c337ceabcece58e40f062fe58c26f3e628d429ca Mon Sep 17 00:00:00 2001 From: Zanie Blue Date: Fri, 18 Jul 2025 12:37:01 -0500 Subject: [PATCH 06/14] Never apply sources --- crates/uv-build-frontend/src/lib.rs | 60 +++--- crates/uv/tests/it/sync.rs | 309 ++++++++++++++++++++++++++-- 2 files changed, 320 insertions(+), 49 deletions(-) diff --git a/crates/uv-build-frontend/src/lib.rs b/crates/uv-build-frontend/src/lib.rs index a2f23a904..7f1685bc1 100644 --- a/crates/uv-build-frontend/src/lib.rs +++ b/crates/uv-build-frontend/src/lib.rs @@ -4,6 +4,7 @@ mod error; +use std::borrow::Cow; use std::ffi::OsString; use std::fmt::Formatter; use std::fmt::Write; @@ -306,7 +307,6 @@ impl SourceBuild { fallback_package_name, locations, source_strategy, - extra_build_dependencies, workspace_cache, &default_backend, ) @@ -324,6 +324,14 @@ impl SourceBuild { .or(fallback_package_version) .cloned(); + let extra_build_dependencies = package_name + .as_ref() + .and_then(|name| extra_build_dependencies.get(name).cloned()) + .unwrap_or_default() + .into_iter() + .map(Requirement::from) + .collect(); + // Create a virtual environment, or install into the shared environment if requested. let venv = if let Some(venv) = build_isolation.shared_environment(package_name.as_ref()) { venv.clone() @@ -351,10 +359,13 @@ impl SourceBuild { source_build_context, &default_backend, &pep517_backend, + extra_build_dependencies, build_stack, ) .await?; + // TODO(zanieb): We'll report `build-system.requires` here but it may include + // `extra-build-dependencies` build_context .install(&resolved_requirements, &venv, build_stack) .await @@ -473,10 +484,13 @@ impl SourceBuild { source_build_context: SourceBuildContext, default_backend: &Pep517Backend, pep517_backend: &Pep517Backend, + extra_build_dependencies: Vec, build_stack: &BuildStack, ) -> Result { Ok( - if pep517_backend.requirements == default_backend.requirements { + if pep517_backend.requirements == default_backend.requirements + && extra_build_dependencies.is_empty() + { let mut resolution = source_build_context.default_resolution.lock().await; if let Some(resolved_requirements) = &*resolution { resolved_requirements.clone() @@ -491,8 +505,21 @@ impl SourceBuild { resolved_requirements } } else { + // TODO(zanieb): It's unclear if we actually want to solve these together. We might + // want to perform a separate solve to allow conflicts? + let requirements = if extra_build_dependencies.is_empty() { + Cow::Borrowed(&pep517_backend.requirements) + } else { + // If there are extra build dependencies, we need to resolve them together with + // the backend requirements. + let mut requirements = pep517_backend.requirements.clone(); + requirements.extend(extra_build_dependencies); + Cow::Owned(requirements) + }; + // TODO(zanieb): We'll report `build-system.requires` here but it may include + // `extra-build-dependencies` build_context - .resolve(&pep517_backend.requirements, build_stack) + .resolve(&requirements, build_stack) .await .map_err(|err| { Error::RequirementsResolve("`build-system.requires`", err.into()) @@ -508,7 +535,6 @@ impl SourceBuild { package_name: Option<&PackageName>, locations: &IndexLocations, source_strategy: SourceStrategy, - extra_build_dependencies: &ExtraBuildDependencies, workspace_cache: &WorkspaceCache, default_backend: &Pep517Backend, ) -> Result<(Pep517Backend, Option), Box> { @@ -525,10 +551,6 @@ impl SourceBuild { .as_ref() .map(|project| &project.name) .or(package_name); - let extra_build_dependencies = name - .as_ref() - .and_then(|name| extra_build_dependencies.get(name).cloned()) - .unwrap_or_default(); let backend = if let Some(build_system) = pyproject_toml.build_system { // If necessary, lower the requirements. @@ -554,9 +576,6 @@ impl SourceBuild { .requires .into_iter() .map(Requirement::from) - .chain( - extra_build_dependencies.into_iter().map(Requirement::from), - ) .collect() } } @@ -564,7 +583,6 @@ impl SourceBuild { .requires .into_iter() .map(Requirement::from) - .chain(extra_build_dependencies.into_iter().map(Requirement::from)) .collect(), }; @@ -617,12 +635,8 @@ impl SourceBuild { ); } } - let mut backend = default_backend.clone(); - // Apply extra build dependencies - backend - .requirements - .extend(extra_build_dependencies.into_iter().map(Requirement::from)); - backend + + default_backend.clone() }; Ok((backend, pyproject_toml.project)) } @@ -637,15 +651,7 @@ impl SourceBuild { // the default backend, to match `build`. `pip` uses `setup.py` directly in this // case, but plans to make PEP 517 builds the default in the future. // See: https://github.com/pypa/pip/issues/9175. - let mut backend = default_backend.clone(); - // Apply extra build dependencies - let extra_build_dependencies = package_name - .as_ref() - .and_then(|name| extra_build_dependencies.get(name).cloned()) - .unwrap_or_default(); - backend - .requirements - .extend(extra_build_dependencies.into_iter().map(Requirement::from)); + let backend = default_backend.clone(); Ok((backend, None)) } Err(err) => Err(Box::new(err.into())), diff --git a/crates/uv/tests/it/sync.rs b/crates/uv/tests/it/sync.rs index 44acaf2ab..73c628657 100644 --- a/crates/uv/tests/it/sync.rs +++ b/crates/uv/tests/it/sync.rs @@ -1527,15 +1527,17 @@ fn sync_build_isolation_extra() -> Result<()> { Ok(()) } -/// Use dedicated extra groups to install dependencies for `--no-build-isolation-package`. #[test] -fn sync_build_isolation_fail() -> Result<()> { +fn sync_extra_build_dependencies() -> Result<()> { let context = TestContext::new("3.12").with_filtered_counts(); - let pyproject_toml = context.temp_dir.child("pyproject.toml"); - pyproject_toml.write_str(indoc! {r#" + // Write a test package that arbitrarily requires `anyio` at build time + let child = context.temp_dir.child("child"); + child.create_dir_all()?; + let child_pyproject_toml = child.child("pyproject.toml"); + child_pyproject_toml.write_str(indoc! {r#" [project] - name = "project" + name = "child" version = "0.1.0" requires-python = ">=3.9" @@ -1544,7 +1546,7 @@ fn sync_build_isolation_fail() -> Result<()> { backend-path = ["."] build-backend = "build_backend" "#})?; - let build_backend = context.temp_dir.child("build_backend.py"); + let build_backend = child.child("build_backend.py"); build_backend.write_str(indoc! {r#" import sys @@ -1553,46 +1555,59 @@ fn sync_build_isolation_fail() -> Result<()> { try: import anyio except ModuleNotFoundError: - print("Missing `anyio` module to build package", file=sys.stderr) + print("Missing `anyio` module", file=sys.stderr) sys.exit(1) "#})?; - context.temp_dir.child("src/project/__init__.py").touch()?; + child.child("src/child/__init__.py").touch()?; + + let parent = &context.temp_dir; + let pyproject_toml = parent.child("pyproject.toml"); + pyproject_toml.write_str(indoc! {r#" + [project] + name = "parent" + version = "0.1.0" + requires-python = ">=3.9" + dependencies = ["child"] + + [tool.uv.sources] + child = { path = "child" } + "#})?; // Running `uv sync` should fail due to missing build-dependencies - uv_snapshot!(context.filters(), context.sync(), @r" + uv_snapshot!(context.filters(), context.sync().arg("--reinstall").arg("--refresh"), @r" success: false exit_code: 1 ----- stdout ----- ----- stderr ----- Resolved [N] packages in [TIME] - × Failed to build `project @ file://[TEMP_DIR]/` + × Failed to build `child @ file://[TEMP_DIR]/child` ├─▶ The build backend returned an error - ╰─▶ Call to `build_backend.build_editable` failed (exit status: 1) + ╰─▶ Call to `build_backend.build_wheel` failed (exit status: 1) [stderr] - Missing `anyio` module to build package + Missing `anyio` module hint: This usually indicates a problem with the package or the build environment. + help: `child` was included because `parent` (v0.1.0) depends on `child` "); - // Adding extra-build-dependencies should solve the issue + // Adding `extra-build-dependencies` should solve the issue pyproject_toml.write_str(indoc! {r#" [project] - name = "project" + name = "parent" version = "0.1.0" requires-python = ">=3.9" + dependencies = ["child"] - [build-system] - requires = ["hatchling"] - backend-path = ["."] - build-backend = "build_backend" + [tool.uv.sources] + child = { path = "child" } [tool.uv.extra-build-dependencies] - project = ["anyio"] + child = ["anyio"] "#})?; - uv_snapshot!(context.filters(), context.sync(), @r" + uv_snapshot!(context.filters(), context.sync().arg("--reinstall").arg("--refresh"), @r" success: true exit_code: 0 ----- stdout ----- @@ -1601,10 +1616,260 @@ fn sync_build_isolation_fail() -> Result<()> { Resolved [N] packages in [TIME] Prepared [N] packages in [TIME] Installed [N] packages in [TIME] - + project==0.1.0 (from file://[TEMP_DIR]/) + + child==0.1.0 (from file://[TEMP_DIR]/child) "); - assert!(context.temp_dir.child("uv.lock").exists()); + // Adding `extra-build-dependencies` with the wrong name should not + pyproject_toml.write_str(indoc! {r#" + [project] + name = "parent" + version = "0.1.0" + requires-python = ">=3.9" + dependencies = ["child"] + + [tool.uv.sources] + child = { path = "child" } + + [tool.uv.extra-build-dependencies] + wrong_name = ["anyio"] + "#})?; + + uv_snapshot!(context.filters(), context.sync().arg("--reinstall").arg("--refresh"), @r" + success: false + exit_code: 1 + ----- stdout ----- + + ----- stderr ----- + Resolved [N] packages in [TIME] + × Failed to build `child @ file://[TEMP_DIR]/child` + ├─▶ The build backend returned an error + ╰─▶ Call to `build_backend.build_wheel` failed (exit status: 1) + + [stderr] + Missing `anyio` module + + hint: This usually indicates a problem with the package or the build environment. + help: `child` was included because `parent` (v0.1.0) depends on `child` + "); + + // Write a test package that arbitrarily bans `anyio` at build time + let bad_child = context.temp_dir.child("bad_child"); + bad_child.create_dir_all()?; + let bad_child_pyproject_toml = bad_child.child("pyproject.toml"); + bad_child_pyproject_toml.write_str(indoc! {r#" + [project] + name = "bad_child" + version = "0.1.0" + requires-python = ">=3.9" + + [build-system] + requires = ["hatchling"] + backend-path = ["."] + build-backend = "build_backend" + "#})?; + let build_backend = bad_child.child("build_backend.py"); + build_backend.write_str(indoc! {r#" + import sys + + from hatchling.build import * + + try: + import anyio + except ModuleNotFoundError: + pass + else: + print("Found `anyio` module", file=sys.stderr) + sys.exit(1) + "#})?; + bad_child.child("src/bad_child/__init__.py").touch()?; + + // Depend on `bad_child` too + pyproject_toml.write_str(indoc! {r#" + [project] + name = "parent" + version = "0.1.0" + requires-python = ">=3.9" + dependencies = ["child", "bad_child"] + + [tool.uv.sources] + child = { path = "child" } + bad_child = { path = "bad_child" } + + [tool.uv.extra-build-dependencies] + child = ["anyio"] + bad_child = ["anyio"] + "#})?; + + // Confirm that `bad_child` fails if anyio is provided + uv_snapshot!(context.filters(), context.sync().arg("--reinstall").arg("--refresh"), @r" + success: false + exit_code: 1 + ----- stdout ----- + + ----- stderr ----- + Resolved [N] packages in [TIME] + × Failed to build `bad-child @ file://[TEMP_DIR]/bad_child` + ├─▶ The build backend returned an error + ╰─▶ Call to `build_backend.build_wheel` failed (exit status: 1) + + [stderr] + Found `anyio` module + + hint: This usually indicates a problem with the package or the build environment. + help: `bad-child` was included because `parent` (v0.1.0) depends on `bad-child` + "); + + // But `anyio` is not provided to `bad_child` if scoped to `child` + pyproject_toml.write_str(indoc! {r#" + [project] + name = "parent" + version = "0.1.0" + requires-python = ">=3.9" + dependencies = ["child", "bad_child"] + + [tool.uv.sources] + child = { path = "child" } + bad_child = { path = "bad_child" } + + [tool.uv.extra-build-dependencies] + child = ["anyio"] + "#})?; + + uv_snapshot!(context.filters(), context.sync().arg("--reinstall").arg("--refresh"), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved [N] packages in [TIME] + Prepared [N] packages in [TIME] + Uninstalled [N] packages in [TIME] + Installed [N] packages in [TIME] + + bad-child==0.1.0 (from file://[TEMP_DIR]/bad_child) + ~ child==0.1.0 (from file://[TEMP_DIR]/child) + "); + + Ok(()) +} + +#[test] +fn sync_extra_build_dependencies_sources() -> Result<()> { + let context = TestContext::new("3.12").with_filtered_counts(); + + let anyio_local = context.workspace_root.join("scripts/packages/anyio_local"); + + // Write a test package that arbitrarily requires `anyio` at a specific _path_ at build time + let child = context.temp_dir.child("child"); + child.create_dir_all()?; + let child_pyproject_toml = child.child("pyproject.toml"); + child_pyproject_toml.write_str(indoc! {r#" + [project] + name = "child" + version = "0.1.0" + requires-python = ">=3.9" + + [build-system] + requires = ["hatchling"] + backend-path = ["."] + build-backend = "build_backend" + "#})?; + let build_backend = child.child("build_backend.py"); + build_backend.write_str(&formatdoc! {r#" + import sys + + from hatchling.build import * + + try: + import anyio + except ModuleNotFoundError: + print("Missing `anyio` module", file=sys.stderr) + sys.exit(1) + + if not anyio.__file__.startswith("{0}"): + print("Found anyio at", anyio.__file__, file=sys.stderr) + print("Expected {0}", file=sys.stderr) + sys.exit(1) + "#, anyio_local.display() + })?; + child.child("src/child/__init__.py").touch()?; + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str(&formatdoc! {r#" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = ["child"] + + [tool.uv.sources] + anyio = {{ path = "{anyio_local}" }} + child = {{ path = "child" }} + + [tool.uv.extra-build-dependencies] + child = ["anyio"] + "#, + anyio_local = anyio_local.portable_display(), + })?; + + // Running `uv sync` should fail due to the unapplied source + uv_snapshot!(context.filters(), context.sync().arg("--reinstall").arg("--refresh"), @r" + success: false + exit_code: 1 + ----- stdout ----- + + ----- stderr ----- + Resolved [N] packages in [TIME] + × Failed to build `child @ file://[TEMP_DIR]/child` + ├─▶ The build backend returned an error + ╰─▶ Call to `build_backend.build_wheel` failed (exit status: 1) + + [stderr] + Found anyio at [CACHE_DIR]/builds-v0/[TMP]/__init__.py + Expected [WORKSPACE]/scripts/packages/anyio_local + + hint: This usually indicates a problem with the package or the build environment. + help: `child` was included because `project` (v0.1.0) depends on `child` + "); + + // We also don't apply sources from the child + let child = context.temp_dir.child("child"); + child.create_dir_all()?; + let child_pyproject_toml = child.child("pyproject.toml"); + child_pyproject_toml.write_str(&formatdoc! {r#" + [project] + name = "child" + version = "0.1.0" + requires-python = ">=3.9" + + [build-system] + requires = ["hatchling"] + backend-path = ["."] + build-backend = "build_backend" + + [tool.uv.sources] + anyio = {{ path = "{}" }} + "#, anyio_local.display() + })?; + + // Running `uv sync` should fail due to the unapplied source + uv_snapshot!(context.filters(), context.sync().arg("--reinstall").arg("--refresh"), @r" + success: false + exit_code: 1 + ----- stdout ----- + + ----- stderr ----- + Resolved [N] packages in [TIME] + × Failed to build `child @ file://[TEMP_DIR]/child` + ├─▶ The build backend returned an error + ╰─▶ Call to `build_backend.build_wheel` failed (exit status: 1) + + [stderr] + Found anyio at [CACHE_DIR]/builds-v0/[TMP]/__init__.py + Expected [WORKSPACE]/scripts/packages/anyio_local + + hint: This usually indicates a problem with the package or the build environment. + help: `child` was included because `project` (v0.1.0) depends on `child` + "); Ok(()) } From fa21bf0b7a7b8c27428d4ffe611720bfbe1316f0 Mon Sep 17 00:00:00 2001 From: Zanie Blue Date: Tue, 22 Jul 2025 08:24:43 -0500 Subject: [PATCH 07/14] Add preview warnings --- crates/uv/src/commands/pip/compile.rs | 8 +++++++- crates/uv/src/commands/pip/install.rs | 8 +++++++- crates/uv/src/commands/pip/sync.rs | 8 +++++++- crates/uv/src/commands/project/add.rs | 6 ++++++ crates/uv/src/commands/project/lock.rs | 6 ++++++ crates/uv/src/commands/project/sync.rs | 8 +++++++- crates/uv/src/lib.rs | 15 +++++++++++++++ 7 files changed, 55 insertions(+), 4 deletions(-) diff --git a/crates/uv/src/commands/pip/compile.rs b/crates/uv/src/commands/pip/compile.rs index 15e42c2ab..036c43435 100644 --- a/crates/uv/src/commands/pip/compile.rs +++ b/crates/uv/src/commands/pip/compile.rs @@ -43,7 +43,7 @@ use uv_resolver::{ }; use uv_torch::{TorchMode, TorchStrategy}; use uv_types::{BuildIsolation, EmptyInstalledPackages, HashStrategy}; -use uv_warnings::warn_user; +use uv_warnings::{warn_user, warn_user_once}; use uv_workspace::WorkspaceCache; use uv_workspace::pyproject::ExtraBuildDependencies; @@ -113,6 +113,12 @@ pub(crate) async fn pip_compile( printer: Printer, preview: PreviewMode, ) -> Result { + if preview.is_disabled() && !extra_build_dependencies.is_empty() { + warn_user_once!( + "The `extra-build-dependencies` option is experimental and may change without warning. Pass `--preview` to disable this warning." + ); + } + // If the user provides a `pyproject.toml` or other TOML file as the output file, raise an // error. if output_file diff --git a/crates/uv/src/commands/pip/install.rs b/crates/uv/src/commands/pip/install.rs index 86328e7ad..548a2b14e 100644 --- a/crates/uv/src/commands/pip/install.rs +++ b/crates/uv/src/commands/pip/install.rs @@ -37,7 +37,7 @@ use uv_resolver::{ }; use uv_torch::{TorchMode, TorchStrategy}; use uv_types::{BuildIsolation, HashStrategy}; -use uv_warnings::warn_user; +use uv_warnings::{warn_user, warn_user_once}; use uv_workspace::WorkspaceCache; use uv_workspace::pyproject::ExtraBuildDependencies; @@ -102,6 +102,12 @@ pub(crate) async fn pip_install( ) -> anyhow::Result { let start = std::time::Instant::now(); + if preview.is_disabled() && !extra_build_dependencies.is_empty() { + warn_user_once!( + "The `extra-build-dependencies` option is experimental and may change without warning. Pass `--preview` to disable this warning." + ); + } + let client_builder = BaseClientBuilder::new() .retries_from_env()? .connectivity(network_settings.connectivity) diff --git a/crates/uv/src/commands/pip/sync.rs b/crates/uv/src/commands/pip/sync.rs index c4f5c0aab..963fd0659 100644 --- a/crates/uv/src/commands/pip/sync.rs +++ b/crates/uv/src/commands/pip/sync.rs @@ -31,7 +31,7 @@ use uv_resolver::{ }; use uv_torch::{TorchMode, TorchStrategy}; use uv_types::{BuildIsolation, HashStrategy}; -use uv_warnings::warn_user; +use uv_warnings::{warn_user, warn_user_once}; use uv_workspace::WorkspaceCache; use uv_workspace::pyproject::ExtraBuildDependencies; @@ -84,6 +84,12 @@ pub(crate) async fn pip_sync( printer: Printer, preview: PreviewMode, ) -> Result { + if preview.is_disabled() && !extra_build_dependencies.is_empty() { + warn_user_once!( + "The `extra-build-dependencies` option is experimental and may change without warning. Pass `--preview` to disable this warning." + ); + } + let client_builder = BaseClientBuilder::new() .retries_from_env()? .connectivity(network_settings.connectivity) diff --git a/crates/uv/src/commands/project/add.rs b/crates/uv/src/commands/project/add.rs index 2f024418e..42cba36f6 100644 --- a/crates/uv/src/commands/project/add.rs +++ b/crates/uv/src/commands/project/add.rs @@ -101,6 +101,12 @@ pub(crate) async fn add( warn_user_once!("The bounds option is in preview and may change in any future release."); } + if preview.is_disabled() && !settings.resolver.extra_build_dependencies.is_empty() { + warn_user_once!( + "The `extra-build-dependencies` option is experimental and may change without warning. Pass `--preview` to disable this warning." + ); + } + for source in &requirements { match source { RequirementsSource::PyprojectToml(_) => { diff --git a/crates/uv/src/commands/project/lock.rs b/crates/uv/src/commands/project/lock.rs index 77565ba84..9d2b33ba5 100644 --- a/crates/uv/src/commands/project/lock.rs +++ b/crates/uv/src/commands/project/lock.rs @@ -443,6 +443,12 @@ async fn do_lock( sources, } = settings; + if preview.is_disabled() && !extra_build_dependencies.is_empty() { + warn_user_once!( + "The `extra-build-dependencies` option is experimental and may change without warning. Pass `--preview` to disable this warning." + ); + } + // Collect the requirements, etc. let members = target.members(); let packages = target.packages(); diff --git a/crates/uv/src/commands/project/sync.rs b/crates/uv/src/commands/project/sync.rs index a5ccf85e6..141a06d44 100644 --- a/crates/uv/src/commands/project/sync.rs +++ b/crates/uv/src/commands/project/sync.rs @@ -30,7 +30,7 @@ use uv_resolver::{FlatIndex, Installable, Lock}; use uv_scripts::{Pep723ItemRef, Pep723Script}; use uv_settings::PythonInstallMirrors; use uv_types::{BuildIsolation, HashStrategy}; -use uv_warnings::warn_user; +use uv_warnings::{warn_user, warn_user_once}; use uv_workspace::pyproject::Source; use uv_workspace::{DiscoveryOptions, MemberDiscovery, VirtualProject, Workspace, WorkspaceCache}; @@ -585,6 +585,12 @@ pub(super) async fn do_sync( sources, } = settings; + if preview.is_disabled() && !extra_build_dependencies.is_empty() { + warn_user_once!( + "The `extra-build-dependencies` option is experimental and may change without warning. Pass `--preview` to disable this warning." + ); + } + let client_builder = BaseClientBuilder::new() .retries_from_env()? .connectivity(network_settings.connectivity) diff --git a/crates/uv/src/lib.rs b/crates/uv/src/lib.rs index c2af77086..3f061740f 100644 --- a/crates/uv/src/lib.rs +++ b/crates/uv/src/lib.rs @@ -444,6 +444,11 @@ async fn run(mut cli: Cli) -> Result { // Resolve the settings from the command-line arguments and workspace configuration. let args = PipCompileSettings::resolve(args, filesystem); show_settings!(args); + if !args.settings.extra_build_dependencies.is_empty() && globals.preview.is_disabled() { + warn_user_once!( + "The `extra-build-dependencies` setting is experimental and may change without warning. Pass `--preview` to disable this warning." + ); + } // Initialize the cache. let cache = cache.init()?.with_refresh( @@ -555,6 +560,11 @@ async fn run(mut cli: Cli) -> Result { // Resolve the settings from the command-line arguments and workspace configuration. let args = PipSyncSettings::resolve(args, filesystem); show_settings!(args); + if !args.settings.extra_build_dependencies.is_empty() && globals.preview.is_disabled() { + warn_user_once!( + "The `extra-build-dependencies` setting is experimental and may change without warning. Pass `--preview` to disable this warning." + ); + } // Initialize the cache. let cache = cache.init()?.with_refresh( @@ -628,6 +638,11 @@ async fn run(mut cli: Cli) -> Result { // Resolve the settings from the command-line arguments and workspace configuration. let mut args = PipInstallSettings::resolve(args, filesystem); show_settings!(args); + if !args.settings.extra_build_dependencies.is_empty() && globals.preview.is_disabled() { + warn_user_once!( + "The `extra-build-dependencies` setting is experimental and may change without warning. Pass `--preview` to disable this warning." + ); + } let mut requirements = Vec::with_capacity( args.package.len() + args.editables.len() + args.requirements.len(), From 6fb86184108ff113b9e63de7f1d6833a42813b99 Mon Sep 17 00:00:00 2001 From: Zanie Blue Date: Tue, 22 Jul 2025 16:06:00 -0500 Subject: [PATCH 08/14] Lower sources --- crates/uv/src/commands/project/sync.rs | 81 +++++++++++++++++- crates/uv/tests/it/sync.rs | 82 ++++++++++++++----- .../packages/anyio_local/anyio/__init__.py | 2 + 3 files changed, 140 insertions(+), 25 deletions(-) diff --git a/crates/uv/src/commands/project/sync.rs b/crates/uv/src/commands/project/sync.rs index 141a06d44..89721fb93 100644 --- a/crates/uv/src/commands/project/sync.rs +++ b/crates/uv/src/commands/project/sync.rs @@ -18,13 +18,14 @@ use uv_configuration::{ }; use uv_dispatch::BuildDispatch; use uv_distribution_types::{ - DirectorySourceDist, Dist, Index, Requirement, Resolution, ResolvedDist, SourceDist, + DirectorySourceDist, Dist, Index, IndexLocations, Requirement, Resolution, ResolvedDist, + SourceDist, }; use uv_fs::{PortablePathBuf, Simplified}; use uv_installer::SitePackages; use uv_normalize::{DefaultExtras, DefaultGroups, PackageName}; use uv_pep508::{MarkerTree, VersionOrUrl}; -use uv_pypi_types::{ParsedArchiveUrl, ParsedGitUrl, ParsedUrl}; +use uv_pypi_types::{ParsedArchiveUrl, ParsedGitUrl, ParsedUrl, VerbatimParsedUrl}; use uv_python::{PythonDownloads, PythonEnvironment, PythonPreference, PythonRequest}; use uv_resolver::{FlatIndex, Installable, Lock}; use uv_scripts::{Pep723ItemRef, Pep723Script}; @@ -32,7 +33,10 @@ use uv_settings::PythonInstallMirrors; use uv_types::{BuildIsolation, HashStrategy}; use uv_warnings::{warn_user, warn_user_once}; use uv_workspace::pyproject::Source; -use uv_workspace::{DiscoveryOptions, MemberDiscovery, VirtualProject, Workspace, WorkspaceCache}; +use uv_workspace::{ + DiscoveryOptions, MemberDiscovery, VirtualProject, Workspace, WorkspaceCache, + pyproject::ExtraBuildDependencies, +}; use crate::commands::pip::loggers::{DefaultInstallLogger, DefaultResolveLogger, InstallLogger}; use crate::commands::pip::operations::Modifications; @@ -543,6 +547,61 @@ impl Deref for SyncEnvironment { } } +/// Lower extra build dependencies using workspace sources. +/// +/// This ensures that extra build dependencies respect source configurations +/// from the project's `tool.uv.sources` table. +#[allow(clippy::result_large_err)] +fn lower_extra_build_dependencies( + extra_build_dependencies: &ExtraBuildDependencies, + workspace: &Workspace, + index_locations: &IndexLocations, +) -> Result { + use std::collections::BTreeMap; + use uv_configuration::SourceStrategy; + + let mut lowered_dependencies = BTreeMap::new(); + + for (package_name, requirements) in extra_build_dependencies { + // Use BuildRequires to lower the requirements + let metadata = uv_distribution::BuildRequires::from_workspace( + uv_pypi_types::BuildRequires { + name: Some(package_name.clone()), + requires_dist: requirements.clone(), + }, + workspace, + index_locations, + SourceStrategy::Enabled, + )?; + + // Extract the lowered requirements and convert them + let lowered_requirements: Vec> = + metadata.requires_dist.into_iter().map(Into::into).collect(); + lowered_dependencies.insert(package_name.clone(), lowered_requirements); + } + + Ok(ExtraBuildDependencies::from(lowered_dependencies)) +} + +/// Lower extra build dependencies using script sources. +/// +/// This ensures that extra build dependencies respect source configurations +/// from the script's metadata. +fn lower_extra_build_dependencies_for_script( + extra_build_dependencies: &ExtraBuildDependencies, + _script: &Pep723Script, + _index_locations: &IndexLocations, +) -> ExtraBuildDependencies { + // Scripts don't have extra build dependencies per se, but we still need to handle + // the case for consistency. Since scripts don't define extra build dependencies + // for other packages, we just return the dependencies as-is. + // + // If in the future scripts support defining extra build dependencies for packages + // they depend on, we would need to implement proper lowering here using the + // script's sources. + extra_build_dependencies.clone() +} + /// Sync a lockfile with an environment. #[allow(clippy::fn_params_excessive_bools)] pub(super) async fn do_sync( @@ -591,6 +650,20 @@ pub(super) async fn do_sync( ); } + // Lower extra build dependencies to apply source configurations + let extra_build_dependencies = match &target { + InstallTarget::Workspace { workspace, .. } + | InstallTarget::Project { workspace, .. } + | InstallTarget::NonProjectWorkspace { workspace, .. } => { + lower_extra_build_dependencies(extra_build_dependencies, workspace, index_locations)? + } + InstallTarget::Script { script, .. } => lower_extra_build_dependencies_for_script( + extra_build_dependencies, + script, + index_locations, + ), + }; + let client_builder = BaseClientBuilder::new() .retries_from_env()? .connectivity(network_settings.connectivity) @@ -719,7 +792,7 @@ pub(super) async fn do_sync( config_setting, config_settings_package, build_isolation, - extra_build_dependencies, + &extra_build_dependencies, link_mode, build_options, &build_hasher, diff --git a/crates/uv/tests/it/sync.rs b/crates/uv/tests/it/sync.rs index 73c628657..5f87bbbb5 100644 --- a/crates/uv/tests/it/sync.rs +++ b/crates/uv/tests/it/sync.rs @@ -1613,6 +1613,7 @@ fn sync_extra_build_dependencies() -> Result<()> { ----- stdout ----- ----- stderr ----- + warning: The `extra-build-dependencies` option is experimental and may change without warning. Pass `--preview` to disable this warning. Resolved [N] packages in [TIME] Prepared [N] packages in [TIME] Installed [N] packages in [TIME] @@ -1640,6 +1641,7 @@ fn sync_extra_build_dependencies() -> Result<()> { ----- stdout ----- ----- stderr ----- + warning: The `extra-build-dependencies` option is experimental and may change without warning. Pass `--preview` to disable this warning. Resolved [N] packages in [TIME] × Failed to build `child @ file://[TEMP_DIR]/child` ├─▶ The build backend returned an error @@ -1707,6 +1709,7 @@ fn sync_extra_build_dependencies() -> Result<()> { ----- stdout ----- ----- stderr ----- + warning: The `extra-build-dependencies` option is experimental and may change without warning. Pass `--preview` to disable this warning. Resolved [N] packages in [TIME] × Failed to build `bad-child @ file://[TEMP_DIR]/bad_child` ├─▶ The build backend returned an error @@ -1741,6 +1744,7 @@ fn sync_extra_build_dependencies() -> Result<()> { ----- stdout ----- ----- stderr ----- + warning: The `extra-build-dependencies` option is experimental and may change without warning. Pass `--preview` to disable this warning. Resolved [N] packages in [TIME] Prepared [N] packages in [TIME] Uninstalled [N] packages in [TIME] @@ -1785,12 +1789,11 @@ fn sync_extra_build_dependencies_sources() -> Result<()> { print("Missing `anyio` module", file=sys.stderr) sys.exit(1) - if not anyio.__file__.startswith("{0}"): - print("Found anyio at", anyio.__file__, file=sys.stderr) - print("Expected {0}", file=sys.stderr) + # Check that we got the local version of anyio by checking for the marker + if not hasattr(anyio, 'LOCAL_ANYIO_MARKER'): + print("Found system anyio instead of local anyio", file=sys.stderr) sys.exit(1) - "#, anyio_local.display() - })?; + "#})?; child.child("src/child/__init__.py").touch()?; let pyproject_toml = context.temp_dir.child("pyproject.toml"); @@ -1811,27 +1814,30 @@ fn sync_extra_build_dependencies_sources() -> Result<()> { anyio_local = anyio_local.portable_display(), })?; - // Running `uv sync` should fail due to the unapplied source + // Running `uv sync` should succeed, as `anyio` is provided as a source uv_snapshot!(context.filters(), context.sync().arg("--reinstall").arg("--refresh"), @r" - success: false - exit_code: 1 + success: true + exit_code: 0 ----- stdout ----- ----- stderr ----- + warning: The `extra-build-dependencies` option is experimental and may change without warning. Pass `--preview` to disable this warning. Resolved [N] packages in [TIME] - × Failed to build `child @ file://[TEMP_DIR]/child` - ├─▶ The build backend returned an error - ╰─▶ Call to `build_backend.build_wheel` failed (exit status: 1) - - [stderr] - Found anyio at [CACHE_DIR]/builds-v0/[TMP]/__init__.py - Expected [WORKSPACE]/scripts/packages/anyio_local - - hint: This usually indicates a problem with the package or the build environment. - help: `child` was included because `project` (v0.1.0) depends on `child` + Prepared [N] packages in [TIME] + Installed [N] packages in [TIME] + + child==0.1.0 (from file://[TEMP_DIR]/child) "); - // We also don't apply sources from the child + Ok(()) +} + +#[test] +fn sync_extra_build_dependencies_sources_from_child() -> Result<()> { + let context = TestContext::new("3.12").with_filtered_counts(); + + let anyio_local = context.workspace_root.join("scripts/packages/anyio_local"); + + // Write a test package that arbitrarily requires `anyio` at a specific _path_ at build time let child = context.temp_dir.child("child"); child.create_dir_all()?; let child_pyproject_toml = child.child("pyproject.toml"); @@ -1850,6 +1856,40 @@ fn sync_extra_build_dependencies_sources() -> Result<()> { anyio = {{ path = "{}" }} "#, anyio_local.display() })?; + let build_backend = child.child("build_backend.py"); + build_backend.write_str(&formatdoc! {r#" + import sys + + from hatchling.build import * + + try: + import anyio + except ModuleNotFoundError: + print("Missing `anyio` module", file=sys.stderr) + sys.exit(1) + + # Check that we got the local version of anyio by checking for the marker + if not hasattr(anyio, 'LOCAL_ANYIO_MARKER'): + print("Found system anyio instead of local anyio", file=sys.stderr) + sys.exit(1) + "#})?; + child.child("src/child/__init__.py").touch()?; + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str(indoc! {r#" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = ["child"] + + [tool.uv.sources] + child = { path = "child" } + + [tool.uv.extra-build-dependencies] + child = ["anyio"] + "#, + })?; // Running `uv sync` should fail due to the unapplied source uv_snapshot!(context.filters(), context.sync().arg("--reinstall").arg("--refresh"), @r" @@ -1858,14 +1898,14 @@ fn sync_extra_build_dependencies_sources() -> Result<()> { ----- stdout ----- ----- stderr ----- + warning: The `extra-build-dependencies` option is experimental and may change without warning. Pass `--preview` to disable this warning. Resolved [N] packages in [TIME] × Failed to build `child @ file://[TEMP_DIR]/child` ├─▶ The build backend returned an error ╰─▶ Call to `build_backend.build_wheel` failed (exit status: 1) [stderr] - Found anyio at [CACHE_DIR]/builds-v0/[TMP]/__init__.py - Expected [WORKSPACE]/scripts/packages/anyio_local + Found system anyio instead of local anyio hint: This usually indicates a problem with the package or the build environment. help: `child` was included because `project` (v0.1.0) depends on `child` diff --git a/scripts/packages/anyio_local/anyio/__init__.py b/scripts/packages/anyio_local/anyio/__init__.py index e69de29bb..a3e374663 100644 --- a/scripts/packages/anyio_local/anyio/__init__.py +++ b/scripts/packages/anyio_local/anyio/__init__.py @@ -0,0 +1,2 @@ +# This is a local dummy anyio package +LOCAL_ANYIO_MARKER = True \ No newline at end of file From 8b5d9314ef4417a4c753cc62ef010a20ffeeebc4 Mon Sep 17 00:00:00 2001 From: Zanie Blue Date: Wed, 23 Jul 2025 10:37:15 -0500 Subject: [PATCH 09/14] Use in cache shards --- Cargo.lock | 1 + crates/uv-bench/benches/uv.rs | 8 +- crates/uv-dispatch/src/lib.rs | 14 ++- crates/uv-distribution/src/lib.rs | 4 +- .../src/metadata/build_requires.rs | 91 ++++++++++++++++++- crates/uv-distribution/src/metadata/mod.rs | 2 +- crates/uv-distribution/src/source/mod.rs | 70 +++++++++----- crates/uv-pep440/src/version.rs | 34 ++++--- crates/uv-pep440/src/version_specifier.rs | 5 + crates/uv-pep508/Cargo.toml | 1 + crates/uv-pep508/src/lib.rs | 47 ++++++++++ crates/uv-types/src/traits.rs | 3 + crates/uv/src/commands/build_frontend.rs | 5 +- crates/uv/src/commands/pip/compile.rs | 5 +- crates/uv/src/commands/pip/install.rs | 5 +- crates/uv/src/commands/pip/sync.rs | 5 +- crates/uv/src/commands/project/add.rs | 14 ++- crates/uv/src/commands/project/lock.rs | 13 ++- crates/uv/src/commands/project/mod.rs | 20 +++- crates/uv/src/commands/project/sync.rs | 83 ++++------------- crates/uv/src/commands/venv.rs | 6 +- crates/uv/tests/it/sync.rs | 26 ++++-- 22 files changed, 323 insertions(+), 139 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 77dfad413..f9316abc1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5514,6 +5514,7 @@ dependencies = [ "tracing-test", "unicode-width 0.2.1", "url", + "uv-cache-key", "uv-fs", "uv-normalize", "uv-pep440", diff --git a/crates/uv-bench/benches/uv.rs b/crates/uv-bench/benches/uv.rs index 9c83c61d3..1c2ecfeaf 100644 --- a/crates/uv-bench/benches/uv.rs +++ b/crates/uv-bench/benches/uv.rs @@ -103,7 +103,7 @@ mod resolver { ResolverEnvironment, ResolverOutput, }; use uv_types::{BuildIsolation, EmptyInstalledPackages, HashStrategy}; - use uv_workspace::{WorkspaceCache, pyproject::ExtraBuildDependencies}; + use uv_workspace::WorkspaceCache; static MARKERS: LazyLock = LazyLock::new(|| { MarkerEnvironment::try_from(MarkerEnvironmentBuilder { @@ -141,7 +141,9 @@ mod resolver { universal: bool, ) -> Result { let build_isolation = BuildIsolation::default(); - let extra_build_dependencies = ExtraBuildDependencies::default(); + let extra_build_requires = uv_distribution::ExtraBuildRequires { + extra_build_dependencies: uv_workspace::pyproject::ExtraBuildDependencies::default(), + }; let build_options = BuildOptions::default(); let concurrency = Concurrency::default(); let config_settings = ConfigSettings::default(); @@ -188,7 +190,7 @@ mod resolver { &config_settings, &config_settings_package, build_isolation, - &extra_build_dependencies, + &extra_build_requires, LinkMode::default(), &build_options, &hashes, diff --git a/crates/uv-dispatch/src/lib.rs b/crates/uv-dispatch/src/lib.rs index 51b727406..e8c67bf43 100644 --- a/crates/uv-dispatch/src/lib.rs +++ b/crates/uv-dispatch/src/lib.rs @@ -40,7 +40,7 @@ use uv_types::{ HashStrategy, InFlight, }; use uv_workspace::WorkspaceCache; -use uv_workspace::pyproject::ExtraBuildDependencies; +use uv_distribution::ExtraBuildRequires; #[derive(Debug, Error)] pub enum BuildDispatchError { @@ -89,7 +89,7 @@ pub struct BuildDispatch<'a> { shared_state: SharedState, dependency_metadata: &'a DependencyMetadata, build_isolation: BuildIsolation<'a>, - extra_build_dependencies: &'a ExtraBuildDependencies, + extra_build_requires: &'a ExtraBuildRequires, link_mode: uv_install_wheel::LinkMode, build_options: &'a BuildOptions, config_settings: &'a ConfigSettings, @@ -118,7 +118,7 @@ impl<'a> BuildDispatch<'a> { config_settings: &'a ConfigSettings, config_settings_package: &'a PackageConfigSettings, build_isolation: BuildIsolation<'a>, - extra_build_dependencies: &'a ExtraBuildDependencies, + extra_build_requires: &'a ExtraBuildRequires, link_mode: uv_install_wheel::LinkMode, build_options: &'a BuildOptions, hasher: &'a HashStrategy, @@ -141,7 +141,7 @@ impl<'a> BuildDispatch<'a> { config_settings, config_settings_package, build_isolation, - extra_build_dependencies, + extra_build_requires, link_mode, build_options, hasher, @@ -223,6 +223,10 @@ impl BuildContext for BuildDispatch<'_> { &self.workspace_cache } + fn extra_build_dependencies(&self) -> &uv_workspace::pyproject::ExtraBuildDependencies { + &self.extra_build_requires.extra_build_dependencies + } + async fn resolve<'data>( &'data self, requirements: &'data [Requirement], @@ -456,7 +460,7 @@ impl BuildContext for BuildDispatch<'_> { self.workspace_cache(), config_settings, self.build_isolation, - self.extra_build_dependencies, + &self.extra_build_requires.extra_build_dependencies, &build_stack, build_kind, self.build_extra_env_vars.clone(), diff --git a/crates/uv-distribution/src/lib.rs b/crates/uv-distribution/src/lib.rs index 07958f715..6371d58af 100644 --- a/crates/uv-distribution/src/lib.rs +++ b/crates/uv-distribution/src/lib.rs @@ -3,8 +3,8 @@ pub use download::LocalWheel; pub use error::Error; pub use index::{BuiltWheelIndex, RegistryWheelIndex}; pub use metadata::{ - ArchiveMetadata, BuildRequires, FlatRequiresDist, LoweredRequirement, LoweringError, Metadata, - MetadataError, RequiresDist, SourcedDependencyGroups, + ArchiveMetadata, BuildRequires, ExtraBuildRequires, FlatRequiresDist, LoweredRequirement, + LoweringError, Metadata, MetadataError, RequiresDist, SourcedDependencyGroups, }; pub use reporter::Reporter; pub use source::prune; diff --git a/crates/uv-distribution/src/metadata/build_requires.rs b/crates/uv-distribution/src/metadata/build_requires.rs index 99b528017..7a746b5de 100644 --- a/crates/uv-distribution/src/metadata/build_requires.rs +++ b/crates/uv-distribution/src/metadata/build_requires.rs @@ -4,7 +4,8 @@ use std::path::Path; use uv_configuration::SourceStrategy; use uv_distribution_types::{IndexLocations, Requirement}; use uv_normalize::PackageName; -use uv_workspace::pyproject::ToolUvSources; +use uv_pypi_types::VerbatimParsedUrl; +use uv_workspace::pyproject::{ExtraBuildDependencies, ToolUvSources}; use uv_workspace::{ DiscoveryOptions, MemberDiscovery, ProjectWorkspace, Workspace, WorkspaceCache, }; @@ -203,3 +204,91 @@ impl BuildRequires { }) } } + +/// Lowered extra build dependencies with source resolution applied. +#[derive(Debug, Clone)] +pub struct ExtraBuildRequires { + pub extra_build_dependencies: ExtraBuildDependencies, +} + +impl ExtraBuildRequires { + /// Lower extra build dependencies from a workspace, applying source resolution. + pub fn from_workspace( + extra_build_dependencies: ExtraBuildDependencies, + workspace: &Workspace, + index_locations: &IndexLocations, + source_strategy: SourceStrategy, + ) -> Result { + match source_strategy { + SourceStrategy::Enabled => { + // Collect project sources and indexes + let project_indexes = workspace + .pyproject_toml() + .tool + .as_ref() + .and_then(|tool| tool.uv.as_ref()) + .and_then(|uv| uv.index.as_deref()) + .unwrap_or(&[]); + + let empty_sources = BTreeMap::default(); + let project_sources = workspace + .pyproject_toml() + .tool + .as_ref() + .and_then(|tool| tool.uv.as_ref()) + .and_then(|uv| uv.sources.as_ref()) + .map(ToolUvSources::inner) + .unwrap_or(&empty_sources); + + // Lower each package's extra build dependencies + let mut result = ExtraBuildDependencies::default(); + for (package_name, requirements) in extra_build_dependencies { + let lowered: Vec> = requirements + .into_iter() + .flat_map(|requirement| { + let requirement_name = requirement.name.clone(); + let extra = requirement.marker.top_level_extra_name(); + let group = None; + LoweredRequirement::from_requirement( + requirement, + None, + workspace.install_path(), + project_sources, + project_indexes, + extra.as_deref(), + group, + index_locations, + workspace, + None, + ) + .map(move |requirement| match requirement { + Ok(requirement) => Ok(requirement.into_inner().into()), + Err(err) => Err(MetadataError::LoweringError( + requirement_name.clone(), + Box::new(err), + )), + }) + }) + .collect::, _>>()?; + result.insert(package_name, lowered); + } + Ok(Self { + extra_build_dependencies: result, + }) + } + SourceStrategy::Disabled => { + // Without source resolution, just return the dependencies as-is + Ok(Self { + extra_build_dependencies, + }) + } + } + } + + /// Create from pre-lowered dependencies (for non-workspace contexts). + pub fn from_lowered(extra_build_dependencies: ExtraBuildDependencies) -> Self { + Self { + extra_build_dependencies, + } + } +} diff --git a/crates/uv-distribution/src/metadata/mod.rs b/crates/uv-distribution/src/metadata/mod.rs index a56a1c354..3375f9fe2 100644 --- a/crates/uv-distribution/src/metadata/mod.rs +++ b/crates/uv-distribution/src/metadata/mod.rs @@ -11,7 +11,7 @@ use uv_pypi_types::{HashDigests, ResolutionMetadata}; use uv_workspace::dependency_groups::DependencyGroupError; use uv_workspace::{WorkspaceCache, WorkspaceError}; -pub use crate::metadata::build_requires::BuildRequires; +pub use crate::metadata::build_requires::{BuildRequires, ExtraBuildRequires}; pub use crate::metadata::dependency_groups::SourcedDependencyGroups; pub use crate::metadata::lowering::LoweredRequirement; pub use crate::metadata::lowering::LoweringError; diff --git a/crates/uv-distribution/src/source/mod.rs b/crates/uv-distribution/src/source/mod.rs index 080a1e52d..14d9ca03c 100644 --- a/crates/uv-distribution/src/source/mod.rs +++ b/crates/uv-distribution/src/source/mod.rs @@ -390,6 +390,20 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { } } + /// Determine the extra build dependencies for the given package name. + fn extra_build_dependencies_for( + &self, + name: Option<&PackageName>, + ) -> &[uv_pep508::Requirement] { + name.and_then(|name| { + self.build_context + .extra_build_dependencies() + .get(name) + .map(|v| v.as_slice()) + }) + .unwrap_or(&[]) + } + /// Build a source distribution from a remote URL. async fn url<'data>( &self, @@ -423,12 +437,13 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { let cache_shard = cache_shard.shard(revision.id()); let source_dist_entry = cache_shard.entry(SOURCE); - // If there are build settings, we need to scope to a cache shard. + // If there are build settings or extra build dependencies, we need to scope to a cache shard. let config_settings = self.config_settings_for(source.name()); - let cache_shard = if config_settings.is_empty() { + let extra_build_deps = self.extra_build_dependencies_for(source.name()); + let cache_shard = if config_settings.is_empty() && extra_build_deps.is_empty() { cache_shard } else { - cache_shard.shard(cache_digest(&&config_settings)) + cache_shard.shard(cache_digest(&(&config_settings, extra_build_deps))) }; // If the cache contains a compatible wheel, return it. @@ -596,12 +611,13 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { } } - // If there are build settings, we need to scope to a cache shard. + // If there are build settings or extra build dependencies, we need to scope to a cache shard. let config_settings = self.config_settings_for(source.name()); - let cache_shard = if config_settings.is_empty() { + let extra_build_deps = self.extra_build_dependencies_for(source.name()); + let cache_shard = if config_settings.is_empty() && extra_build_deps.is_empty() { cache_shard } else { - cache_shard.shard(cache_digest(&config_settings)) + cache_shard.shard(cache_digest(&(&config_settings, extra_build_deps))) }; // Otherwise, we either need to build the metadata. @@ -795,12 +811,13 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { let cache_shard = cache_shard.shard(revision.id()); let source_entry = cache_shard.entry(SOURCE); - // If there are build settings, we need to scope to a cache shard. + // If there are build settings or extra build dependencies, we need to scope to a cache shard. let config_settings = self.config_settings_for(source.name()); - let cache_shard = if config_settings.is_empty() { + let extra_build_deps = self.extra_build_dependencies_for(source.name()); + let cache_shard = if config_settings.is_empty() && extra_build_deps.is_empty() { cache_shard } else { - cache_shard.shard(cache_digest(&config_settings)) + cache_shard.shard(cache_digest(&(&config_settings, extra_build_deps))) }; // If the cache contains a compatible wheel, return it. @@ -957,12 +974,13 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { }); } - // If there are build settings, we need to scope to a cache shard. + // If there are build settings or extra build dependencies, we need to scope to a cache shard. let config_settings = self.config_settings_for(source.name()); - let cache_shard = if config_settings.is_empty() { + let extra_build_deps = self.extra_build_dependencies_for(source.name()); + let cache_shard = if config_settings.is_empty() && extra_build_deps.is_empty() { cache_shard } else { - cache_shard.shard(cache_digest(&config_settings)) + cache_shard.shard(cache_digest(&(&config_settings, extra_build_deps))) }; // Otherwise, we need to build a wheel. @@ -1099,12 +1117,13 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { // freshness, since entries have to be fresher than the revision itself. let cache_shard = cache_shard.shard(revision.id()); - // If there are build settings, we need to scope to a cache shard. + // If there are build settings or extra build dependencies, we need to scope to a cache shard. let config_settings = self.config_settings_for(source.name()); - let cache_shard = if config_settings.is_empty() { + let extra_build_deps = self.extra_build_dependencies_for(source.name()); + let cache_shard = if config_settings.is_empty() && extra_build_deps.is_empty() { cache_shard } else { - cache_shard.shard(cache_digest(&config_settings)) + cache_shard.shard(cache_digest(&(&config_settings, extra_build_deps))) }; // If the cache contains a compatible wheel, return it. @@ -1287,12 +1306,13 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { )); } - // If there are build settings, we need to scope to a cache shard. + // If there are build settings or extra build dependencies, we need to scope to a cache shard. let config_settings = self.config_settings_for(source.name()); - let cache_shard = if config_settings.is_empty() { + let extra_build_deps = self.extra_build_dependencies_for(source.name()); + let cache_shard = if config_settings.is_empty() && extra_build_deps.is_empty() { cache_shard } else { - cache_shard.shard(cache_digest(&config_settings)) + cache_shard.shard(cache_digest(&(&config_settings, extra_build_deps))) }; // Otherwise, we need to build a wheel. @@ -1492,12 +1512,13 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { // Acquire the advisory lock. let _lock = cache_shard.lock().await.map_err(Error::CacheWrite)?; - // If there are build settings, we need to scope to a cache shard. + // If there are build settings or extra build dependencies, we need to scope to a cache shard. let config_settings = self.config_settings_for(source.name()); - let cache_shard = if config_settings.is_empty() { + let extra_build_deps = self.extra_build_dependencies_for(source.name()); + let cache_shard = if config_settings.is_empty() && extra_build_deps.is_empty() { cache_shard } else { - cache_shard.shard(cache_digest(&config_settings)) + cache_shard.shard(cache_digest(&(&config_settings, extra_build_deps))) }; // If the cache contains a compatible wheel, return it. @@ -1795,12 +1816,13 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { )); } - // If there are build settings, we need to scope to a cache shard. + // If there are build settings or extra build dependencies, we need to scope to a cache shard. let config_settings = self.config_settings_for(source.name()); - let cache_shard = if config_settings.is_empty() { + let extra_build_deps = self.extra_build_dependencies_for(source.name()); + let cache_shard = if config_settings.is_empty() && extra_build_deps.is_empty() { cache_shard } else { - cache_shard.shard(cache_digest(&config_settings)) + cache_shard.shard(cache_digest(&(&config_settings, extra_build_deps))) }; // Otherwise, we need to build a wheel. diff --git a/crates/uv-pep440/src/version.rs b/crates/uv-pep440/src/version.rs index 223701692..bd1e0ffe2 100644 --- a/crates/uv-pep440/src/version.rs +++ b/crates/uv-pep440/src/version.rs @@ -114,6 +114,24 @@ impl Operator { pub fn is_star(self) -> bool { matches!(self, Self::EqualStar | Self::NotEqualStar) } + + /// Returns the string representation of this operator. + pub fn as_str(self) -> &'static str { + match self { + Self::Equal => "==", + // Beware, this doesn't print the star + Self::EqualStar => "==", + #[allow(deprecated)] + Self::ExactEqual => "===", + Self::NotEqual => "!=", + Self::NotEqualStar => "!=", + Self::TildeEqual => "~=", + Self::LessThan => "<", + Self::LessThanEqual => "<=", + Self::GreaterThan => ">", + Self::GreaterThanEqual => ">=", + } + } } impl FromStr for Operator { @@ -150,21 +168,7 @@ impl FromStr for Operator { impl std::fmt::Display for Operator { /// Note the `EqualStar` is also `==`. fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let operator = match self { - Self::Equal => "==", - // Beware, this doesn't print the star - Self::EqualStar => "==", - #[allow(deprecated)] - Self::ExactEqual => "===", - Self::NotEqual => "!=", - Self::NotEqualStar => "!=", - Self::TildeEqual => "~=", - Self::LessThan => "<", - Self::LessThanEqual => "<=", - Self::GreaterThan => ">", - Self::GreaterThanEqual => ">=", - }; - + let operator = self.as_str(); write!(f, "{operator}") } } diff --git a/crates/uv-pep440/src/version_specifier.rs b/crates/uv-pep440/src/version_specifier.rs index e111c5118..13e2687cf 100644 --- a/crates/uv-pep440/src/version_specifier.rs +++ b/crates/uv-pep440/src/version_specifier.rs @@ -48,6 +48,11 @@ impl VersionSpecifiers { Self(Box::new([])) } + /// The number of specifiers. + pub fn len(&self) -> usize { + self.0.len() + } + /// Whether all specifiers match the given version. pub fn contains(&self, version: &Version) -> bool { self.iter().all(|specifier| specifier.contains(version)) diff --git a/crates/uv-pep508/Cargo.toml b/crates/uv-pep508/Cargo.toml index e9306da00..85b4e9c5d 100644 --- a/crates/uv-pep508/Cargo.toml +++ b/crates/uv-pep508/Cargo.toml @@ -19,6 +19,7 @@ doctest = false workspace = true [dependencies] +uv-cache-key = { workspace = true } uv-fs = { workspace = true } uv-normalize = { workspace = true } uv-pep440 = { workspace = true } diff --git a/crates/uv-pep508/src/lib.rs b/crates/uv-pep508/src/lib.rs index f63d46206..794981e67 100644 --- a/crates/uv-pep508/src/lib.rs +++ b/crates/uv-pep508/src/lib.rs @@ -26,6 +26,7 @@ use std::str::FromStr; use serde::{Deserialize, Deserializer, Serialize, Serializer, de}; use thiserror::Error; use url::Url; +use uv_cache_key::{CacheKey, CacheKeyHasher}; use cursor::Cursor; pub use marker::{ @@ -251,6 +252,52 @@ impl Serialize for Requirement { } } +impl CacheKey for Requirement +where + T: Display, +{ + fn cache_key(&self, state: &mut CacheKeyHasher) { + self.name.as_str().cache_key(state); + + self.extras.len().cache_key(state); + for extra in &self.extras { + extra.as_str().cache_key(state); + } + + // TODO(zanieb): We inline cache key handling for the child types here, but we could + // move the implementations to the children. The intent here was to limit the scope of + // types exposing the `CacheKey` trait for now. + if let Some(version_or_url) = &self.version_or_url { + 1u8.cache_key(state); + match version_or_url { + VersionOrUrl::VersionSpecifier(spec) => { + 0u8.cache_key(state); + spec.len().cache_key(state); + for specifier in spec.iter() { + specifier.operator().as_str().cache_key(state); + specifier.version().to_string().cache_key(state); + } + } + VersionOrUrl::Url(url) => { + 1u8.cache_key(state); + url.to_string().cache_key(state); + } + } + } else { + 0u8.cache_key(state); + } + + if let Some(marker) = self.marker.contents() { + 1u8.cache_key(state); + marker.to_string().cache_key(state); + } else { + 0u8.cache_key(state); + } + + // `origin` is intentionally omitted + } +} + impl Requirement { /// Returns whether the markers apply for the given environment pub fn evaluate_markers(&self, env: &MarkerEnvironment, extras: &[ExtraName]) -> bool { diff --git a/crates/uv-types/src/traits.rs b/crates/uv-types/src/traits.rs index e3f4ee012..875742da9 100644 --- a/crates/uv-types/src/traits.rs +++ b/crates/uv-types/src/traits.rs @@ -101,6 +101,9 @@ pub trait BuildContext { /// Workspace discovery caching. fn workspace_cache(&self) -> &WorkspaceCache; + /// Get the extra build dependencies. + fn extra_build_dependencies(&self) -> &uv_workspace::pyproject::ExtraBuildDependencies; + /// Resolve the given requirements into a ready-to-install set of package versions. fn resolve<'a>( &'a self, diff --git a/crates/uv/src/commands/build_frontend.rs b/crates/uv/src/commands/build_frontend.rs index a367b7a2e..7c62a30f7 100644 --- a/crates/uv/src/commands/build_frontend.rs +++ b/crates/uv/src/commands/build_frontend.rs @@ -564,6 +564,9 @@ async fn build_package( let workspace_cache = WorkspaceCache::default(); // Create a build dispatch. + let extra_build_requires = uv_distribution::ExtraBuildRequires::from_lowered( + extra_build_dependencies.clone(), + ); let build_dispatch = BuildDispatch::new( &client, cache, @@ -577,7 +580,7 @@ async fn build_package( config_setting, config_settings_package, build_isolation, - extra_build_dependencies, + &extra_build_requires, link_mode, build_options, &hasher, diff --git a/crates/uv/src/commands/pip/compile.rs b/crates/uv/src/commands/pip/compile.rs index 036c43435..94cc2b388 100644 --- a/crates/uv/src/commands/pip/compile.rs +++ b/crates/uv/src/commands/pip/compile.rs @@ -476,6 +476,9 @@ pub(crate) async fn pip_compile( .map(|constraint| constraint.requirement.clone()), ); + let extra_build_requires = uv_distribution::ExtraBuildRequires::from_lowered( + extra_build_dependencies.clone(), + ); let build_dispatch = BuildDispatch::new( &client, &cache, @@ -489,7 +492,7 @@ pub(crate) async fn pip_compile( &config_settings, &config_settings_package, build_isolation, - extra_build_dependencies, + &extra_build_requires, link_mode, &build_options, &build_hashes, diff --git a/crates/uv/src/commands/pip/install.rs b/crates/uv/src/commands/pip/install.rs index 548a2b14e..f558e7355 100644 --- a/crates/uv/src/commands/pip/install.rs +++ b/crates/uv/src/commands/pip/install.rs @@ -421,6 +421,9 @@ pub(crate) async fn pip_install( let state = SharedState::default(); // Create a build dispatch. + let extra_build_requires = uv_distribution::ExtraBuildRequires::from_lowered( + extra_build_dependencies.clone(), + ); let build_dispatch = BuildDispatch::new( &client, &cache, @@ -434,7 +437,7 @@ pub(crate) async fn pip_install( config_settings, config_settings_package, build_isolation, - extra_build_dependencies, + &extra_build_requires, link_mode, &build_options, &build_hasher, diff --git a/crates/uv/src/commands/pip/sync.rs b/crates/uv/src/commands/pip/sync.rs index 963fd0659..5bbf8b071 100644 --- a/crates/uv/src/commands/pip/sync.rs +++ b/crates/uv/src/commands/pip/sync.rs @@ -354,6 +354,9 @@ pub(crate) async fn pip_sync( let state = SharedState::default(); // Create a build dispatch. + let extra_build_requires = uv_distribution::ExtraBuildRequires::from_lowered( + extra_build_dependencies.clone(), + ); let build_dispatch = BuildDispatch::new( &client, &cache, @@ -367,7 +370,7 @@ pub(crate) async fn pip_sync( config_settings, config_settings_package, build_isolation, - extra_build_dependencies, + &extra_build_requires, link_mode, &build_options, &build_hasher, diff --git a/crates/uv/src/commands/project/add.rs b/crates/uv/src/commands/project/add.rs index 42cba36f6..611edc05b 100644 --- a/crates/uv/src/commands/project/add.rs +++ b/crates/uv/src/commands/project/add.rs @@ -431,6 +431,18 @@ pub(crate) async fn add( }; // Create a build dispatch. + let extra_build_requires = if let AddTarget::Project(project, _) = &target { + uv_distribution::ExtraBuildRequires::from_workspace( + settings.resolver.extra_build_dependencies.clone(), + project.workspace(), + &settings.resolver.index_locations, + settings.resolver.sources, + )? + } else { + uv_distribution::ExtraBuildRequires::from_lowered( + settings.resolver.extra_build_dependencies.clone(), + ) + }; let build_dispatch = BuildDispatch::new( &client, cache, @@ -444,7 +456,7 @@ pub(crate) async fn add( &settings.resolver.config_setting, &settings.resolver.config_settings_package, build_isolation, - &settings.resolver.extra_build_dependencies, + &extra_build_requires, settings.resolver.link_mode, &settings.resolver.build_options, &build_hasher, diff --git a/crates/uv/src/commands/project/lock.rs b/crates/uv/src/commands/project/lock.rs index 9d2b33ba5..73b89d084 100644 --- a/crates/uv/src/commands/project/lock.rs +++ b/crates/uv/src/commands/project/lock.rs @@ -671,6 +671,17 @@ async fn do_lock( }; // Create a build dispatch. + let extra_build_requires = match &target { + LockTarget::Workspace(workspace) => uv_distribution::ExtraBuildRequires::from_workspace( + extra_build_dependencies.clone(), + workspace, + index_locations, + *sources, + )?, + LockTarget::Script(_) => uv_distribution::ExtraBuildRequires::from_lowered( + extra_build_dependencies.clone(), + ), + }; let build_dispatch = BuildDispatch::new( &client, cache, @@ -684,7 +695,7 @@ async fn do_lock( config_setting, config_settings_package, build_isolation, - extra_build_dependencies, + &extra_build_requires, *link_mode, build_options, &build_hasher, diff --git a/crates/uv/src/commands/project/mod.rs b/crates/uv/src/commands/project/mod.rs index 6c5ea1e6f..f2a03a51d 100644 --- a/crates/uv/src/commands/project/mod.rs +++ b/crates/uv/src/commands/project/mod.rs @@ -1733,6 +1733,9 @@ pub(crate) async fn resolve_names( let build_hasher = HashStrategy::default(); // Create a build dispatch. + let extra_build_requires = uv_distribution::ExtraBuildRequires::from_lowered( + extra_build_dependencies.clone(), + ); let build_dispatch = BuildDispatch::new( &client, cache, @@ -1746,7 +1749,7 @@ pub(crate) async fn resolve_names( config_setting, config_settings_package, build_isolation, - extra_build_dependencies, + &extra_build_requires, *link_mode, build_options, &build_hasher, @@ -1943,6 +1946,9 @@ pub(crate) async fn resolve_environment( let workspace_cache = WorkspaceCache::default(); // Create a build dispatch. + let extra_build_requires = uv_distribution::ExtraBuildRequires::from_lowered( + extra_build_dependencies.clone(), + ); let resolve_dispatch = BuildDispatch::new( &client, cache, @@ -1956,7 +1962,7 @@ pub(crate) async fn resolve_environment( config_setting, config_settings_package, build_isolation, - extra_build_dependencies, + &extra_build_requires, *link_mode, build_options, &build_hasher, @@ -2083,6 +2089,9 @@ pub(crate) async fn sync_environment( }; // Create a build dispatch. + let extra_build_requires = uv_distribution::ExtraBuildRequires::from_lowered( + extra_build_dependencies.clone(), + ); let build_dispatch = BuildDispatch::new( &client, cache, @@ -2096,7 +2105,7 @@ pub(crate) async fn sync_environment( config_setting, config_settings_package, build_isolation, - extra_build_dependencies, + &extra_build_requires, link_mode, build_options, &build_hasher, @@ -2309,6 +2318,9 @@ pub(crate) async fn update_environment( }; // Create a build dispatch. + let extra_build_requires = uv_distribution::ExtraBuildRequires::from_lowered( + extra_build_dependencies.clone(), + ); let build_dispatch = BuildDispatch::new( &client, cache, @@ -2322,7 +2334,7 @@ pub(crate) async fn update_environment( config_setting, config_settings_package, build_isolation, - extra_build_dependencies, + &extra_build_requires, *link_mode, build_options, &build_hasher, diff --git a/crates/uv/src/commands/project/sync.rs b/crates/uv/src/commands/project/sync.rs index 89721fb93..91989ac50 100644 --- a/crates/uv/src/commands/project/sync.rs +++ b/crates/uv/src/commands/project/sync.rs @@ -18,24 +18,23 @@ use uv_configuration::{ }; use uv_dispatch::BuildDispatch; use uv_distribution_types::{ - DirectorySourceDist, Dist, Index, IndexLocations, Requirement, Resolution, ResolvedDist, + DirectorySourceDist, Dist, Index, Requirement, Resolution, ResolvedDist, SourceDist, }; use uv_fs::{PortablePathBuf, Simplified}; use uv_installer::SitePackages; use uv_normalize::{DefaultExtras, DefaultGroups, PackageName}; use uv_pep508::{MarkerTree, VersionOrUrl}; -use uv_pypi_types::{ParsedArchiveUrl, ParsedGitUrl, ParsedUrl, VerbatimParsedUrl}; +use uv_pypi_types::{ParsedArchiveUrl, ParsedGitUrl, ParsedUrl}; use uv_python::{PythonDownloads, PythonEnvironment, PythonPreference, PythonRequest}; use uv_resolver::{FlatIndex, Installable, Lock}; use uv_scripts::{Pep723ItemRef, Pep723Script}; use uv_settings::PythonInstallMirrors; use uv_types::{BuildIsolation, HashStrategy}; use uv_warnings::{warn_user, warn_user_once}; -use uv_workspace::pyproject::Source; +use uv_workspace::pyproject::{ExtraBuildDependencies, Source}; use uv_workspace::{ DiscoveryOptions, MemberDiscovery, VirtualProject, Workspace, WorkspaceCache, - pyproject::ExtraBuildDependencies, }; use crate::commands::pip::loggers::{DefaultInstallLogger, DefaultResolveLogger, InstallLogger}; @@ -547,60 +546,6 @@ impl Deref for SyncEnvironment { } } -/// Lower extra build dependencies using workspace sources. -/// -/// This ensures that extra build dependencies respect source configurations -/// from the project's `tool.uv.sources` table. -#[allow(clippy::result_large_err)] -fn lower_extra_build_dependencies( - extra_build_dependencies: &ExtraBuildDependencies, - workspace: &Workspace, - index_locations: &IndexLocations, -) -> Result { - use std::collections::BTreeMap; - use uv_configuration::SourceStrategy; - - let mut lowered_dependencies = BTreeMap::new(); - - for (package_name, requirements) in extra_build_dependencies { - // Use BuildRequires to lower the requirements - let metadata = uv_distribution::BuildRequires::from_workspace( - uv_pypi_types::BuildRequires { - name: Some(package_name.clone()), - requires_dist: requirements.clone(), - }, - workspace, - index_locations, - SourceStrategy::Enabled, - )?; - - // Extract the lowered requirements and convert them - let lowered_requirements: Vec> = - metadata.requires_dist.into_iter().map(Into::into).collect(); - lowered_dependencies.insert(package_name.clone(), lowered_requirements); - } - - Ok(ExtraBuildDependencies::from(lowered_dependencies)) -} - -/// Lower extra build dependencies using script sources. -/// -/// This ensures that extra build dependencies respect source configurations -/// from the script's metadata. -fn lower_extra_build_dependencies_for_script( - extra_build_dependencies: &ExtraBuildDependencies, - _script: &Pep723Script, - _index_locations: &IndexLocations, -) -> ExtraBuildDependencies { - // Scripts don't have extra build dependencies per se, but we still need to handle - // the case for consistency. Since scripts don't define extra build dependencies - // for other packages, we just return the dependencies as-is. - // - // If in the future scripts support defining extra build dependencies for packages - // they depend on, we would need to implement proper lowering here using the - // script's sources. - extra_build_dependencies.clone() -} /// Sync a lockfile with an environment. #[allow(clippy::fn_params_excessive_bools)] @@ -650,20 +595,24 @@ pub(super) async fn do_sync( ); } - // Lower extra build dependencies to apply source configurations - let extra_build_dependencies = match &target { + // Lower the extra build dependencies with source resolution + let extra_build_requires = match &target { InstallTarget::Workspace { workspace, .. } | InstallTarget::Project { workspace, .. } | InstallTarget::NonProjectWorkspace { workspace, .. } => { - lower_extra_build_dependencies(extra_build_dependencies, workspace, index_locations)? + uv_distribution::ExtraBuildRequires::from_workspace( + extra_build_dependencies.clone(), + workspace, + index_locations, + sources, + )? } - InstallTarget::Script { script, .. } => lower_extra_build_dependencies_for_script( - extra_build_dependencies, - script, - index_locations, - ), + InstallTarget::Script { .. } => uv_distribution::ExtraBuildRequires { + extra_build_dependencies: ExtraBuildDependencies::default(), + }, }; + let client_builder = BaseClientBuilder::new() .retries_from_env()? .connectivity(network_settings.connectivity) @@ -792,7 +741,7 @@ pub(super) async fn do_sync( config_setting, config_settings_package, build_isolation, - &extra_build_dependencies, + &extra_build_requires, link_mode, build_options, &build_hasher, diff --git a/crates/uv/src/commands/venv.rs b/crates/uv/src/commands/venv.rs index 116dc0553..77477afe8 100644 --- a/crates/uv/src/commands/venv.rs +++ b/crates/uv/src/commands/venv.rs @@ -275,7 +275,9 @@ pub(crate) async fn venv( // Do not allow builds let build_options = BuildOptions::new(NoBinary::None, NoBuild::All); - let extra_build_dependencies = ExtraBuildDependencies::default(); + let extra_build_requires = uv_distribution::ExtraBuildRequires::from_lowered( + ExtraBuildDependencies::default(), + ); // Prep the build context. let build_dispatch = BuildDispatch::new( &client, @@ -290,7 +292,7 @@ pub(crate) async fn venv( &config_settings, &config_settings_package, BuildIsolation::Isolated, - &extra_build_dependencies, + &extra_build_requires, link_mode, &build_options, &build_hasher, diff --git a/crates/uv/tests/it/sync.rs b/crates/uv/tests/it/sync.rs index 5f87bbbb5..b2dff78ab 100644 --- a/crates/uv/tests/it/sync.rs +++ b/crates/uv/tests/it/sync.rs @@ -1573,8 +1573,9 @@ fn sync_extra_build_dependencies() -> Result<()> { child = { path = "child" } "#})?; + context.venv().arg("--clear").assert().success(); // Running `uv sync` should fail due to missing build-dependencies - uv_snapshot!(context.filters(), context.sync().arg("--reinstall").arg("--refresh"), @r" + uv_snapshot!(context.filters(), context.sync(), @r" success: false exit_code: 1 ----- stdout ----- @@ -1607,7 +1608,8 @@ fn sync_extra_build_dependencies() -> Result<()> { child = ["anyio"] "#})?; - uv_snapshot!(context.filters(), context.sync().arg("--reinstall").arg("--refresh"), @r" + context.venv().arg("--clear").assert().success(); + uv_snapshot!(context.filters(), context.sync(), @r" success: true exit_code: 0 ----- stdout ----- @@ -1620,7 +1622,8 @@ fn sync_extra_build_dependencies() -> Result<()> { + child==0.1.0 (from file://[TEMP_DIR]/child) "); - // Adding `extra-build-dependencies` with the wrong name should not + // Adding `extra-build-dependencies` with the wrong name should fail the build + // (the cache is invalidated when extra build dependencies change) pyproject_toml.write_str(indoc! {r#" [project] name = "parent" @@ -1635,7 +1638,8 @@ fn sync_extra_build_dependencies() -> Result<()> { wrong_name = ["anyio"] "#})?; - uv_snapshot!(context.filters(), context.sync().arg("--reinstall").arg("--refresh"), @r" + context.venv().arg("--clear").assert().success(); + uv_snapshot!(context.filters(), context.sync(), @r" success: false exit_code: 1 ----- stdout ----- @@ -1703,7 +1707,8 @@ fn sync_extra_build_dependencies() -> Result<()> { "#})?; // Confirm that `bad_child` fails if anyio is provided - uv_snapshot!(context.filters(), context.sync().arg("--reinstall").arg("--refresh"), @r" + context.venv().arg("--clear").assert().success(); + uv_snapshot!(context.filters(), context.sync(), @r" success: false exit_code: 1 ----- stdout ----- @@ -1738,7 +1743,8 @@ fn sync_extra_build_dependencies() -> Result<()> { child = ["anyio"] "#})?; - uv_snapshot!(context.filters(), context.sync().arg("--reinstall").arg("--refresh"), @r" + context.venv().arg("--clear").assert().success(); + uv_snapshot!(context.filters(), context.sync(), @r" success: true exit_code: 0 ----- stdout ----- @@ -1747,10 +1753,9 @@ fn sync_extra_build_dependencies() -> Result<()> { warning: The `extra-build-dependencies` option is experimental and may change without warning. Pass `--preview` to disable this warning. Resolved [N] packages in [TIME] Prepared [N] packages in [TIME] - Uninstalled [N] packages in [TIME] Installed [N] packages in [TIME] + bad-child==0.1.0 (from file://[TEMP_DIR]/bad_child) - ~ child==0.1.0 (from file://[TEMP_DIR]/child) + + child==0.1.0 (from file://[TEMP_DIR]/child) "); Ok(()) @@ -1815,7 +1820,7 @@ fn sync_extra_build_dependencies_sources() -> Result<()> { })?; // Running `uv sync` should succeed, as `anyio` is provided as a source - uv_snapshot!(context.filters(), context.sync().arg("--reinstall").arg("--refresh"), @r" + uv_snapshot!(context.filters(), context.sync(), @r" success: true exit_code: 0 ----- stdout ----- @@ -1828,6 +1833,9 @@ fn sync_extra_build_dependencies_sources() -> Result<()> { + child==0.1.0 (from file://[TEMP_DIR]/child) "); + // TODO(zanieb): We want to test with `--no-sources` too but unfortunately that's not easy + // because it'll disable the `child` path source too! + Ok(()) } From a06aa47793c21b2dbb7b5fde8e94d100e732565a Mon Sep 17 00:00:00 2001 From: Zanie Blue Date: Wed, 23 Jul 2025 11:38:54 -0500 Subject: [PATCH 10/14] Revert some noise --- crates/uv-build-frontend/src/lib.rs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/crates/uv-build-frontend/src/lib.rs b/crates/uv-build-frontend/src/lib.rs index 7f1685bc1..a9ff286d1 100644 --- a/crates/uv-build-frontend/src/lib.rs +++ b/crates/uv-build-frontend/src/lib.rs @@ -546,17 +546,17 @@ impl SourceBuild { let pyproject_toml: PyProjectToml = PyProjectToml::deserialize(pyproject_toml.into_deserializer()) .map_err(Error::InvalidPyprojectTomlSchema)?; - let name = pyproject_toml - .project - .as_ref() - .map(|project| &project.name) - .or(package_name); let backend = if let Some(build_system) = pyproject_toml.build_system { // If necessary, lower the requirements. let requirements = match source_strategy { SourceStrategy::Enabled => { - if let Some(name) = name { + if let Some(name) = pyproject_toml + .project + .as_ref() + .map(|project| &project.name) + .or(package_name) + { let build_requires = uv_pypi_types::BuildRequires { name: Some(name.clone()), requires_dist: build_system.requires, @@ -647,12 +647,12 @@ impl SourceBuild { source_tree.to_path_buf(), ))); } + // If no `pyproject.toml` is present, by default, proceed with a PEP 517 build using // the default backend, to match `build`. `pip` uses `setup.py` directly in this // case, but plans to make PEP 517 builds the default in the future. // See: https://github.com/pypa/pip/issues/9175. - let backend = default_backend.clone(); - Ok((backend, None)) + Ok((default_backend.clone(), None)) } Err(err) => Err(Box::new(err.into())), } From db10dcbf24ca4c76b6220e74f8876794431d0bd4 Mon Sep 17 00:00:00 2001 From: Zanie Blue Date: Wed, 23 Jul 2025 18:22:58 -0500 Subject: [PATCH 11/14] Add script sources support --- crates/uv-dispatch/src/lib.rs | 2 +- .../src/metadata/build_requires.rs | 16 +- crates/uv-scripts/src/lib.rs | 2 + crates/uv/src/commands/build_frontend.rs | 5 +- crates/uv/src/commands/pip/compile.rs | 5 +- crates/uv/src/commands/pip/install.rs | 5 +- crates/uv/src/commands/pip/sync.rs | 5 +- crates/uv/src/commands/project/add.rs | 2 + crates/uv/src/commands/project/export.rs | 1 + crates/uv/src/commands/project/lock.rs | 15 +- crates/uv/src/commands/project/mod.rs | 81 ++++++-- crates/uv/src/commands/project/remove.rs | 1 + crates/uv/src/commands/project/run.rs | 5 +- crates/uv/src/commands/project/sync.rs | 64 ++++-- crates/uv/src/commands/tool/install.rs | 3 +- crates/uv/src/commands/tool/upgrade.rs | 3 +- crates/uv/src/commands/venv.rs | 5 +- crates/uv/tests/it/show_settings.rs | 1 + crates/uv/tests/it/sync.rs | 187 +++++++++++++++++- 19 files changed, 340 insertions(+), 68 deletions(-) diff --git a/crates/uv-dispatch/src/lib.rs b/crates/uv-dispatch/src/lib.rs index e8c67bf43..0af691b6e 100644 --- a/crates/uv-dispatch/src/lib.rs +++ b/crates/uv-dispatch/src/lib.rs @@ -22,6 +22,7 @@ use uv_configuration::{ }; use uv_configuration::{BuildOutput, Concurrency}; use uv_distribution::DistributionDatabase; +use uv_distribution::ExtraBuildRequires; use uv_distribution_filename::DistFilename; use uv_distribution_types::{ CachedDist, DependencyMetadata, Identifier, IndexCapabilities, IndexLocations, @@ -40,7 +41,6 @@ use uv_types::{ HashStrategy, InFlight, }; use uv_workspace::WorkspaceCache; -use uv_distribution::ExtraBuildRequires; #[derive(Debug, Error)] pub enum BuildDispatchError { diff --git a/crates/uv-distribution/src/metadata/build_requires.rs b/crates/uv-distribution/src/metadata/build_requires.rs index 7a746b5de..af218b470 100644 --- a/crates/uv-distribution/src/metadata/build_requires.rs +++ b/crates/uv-distribution/src/metadata/build_requires.rs @@ -261,13 +261,15 @@ impl ExtraBuildRequires { workspace, None, ) - .map(move |requirement| match requirement { - Ok(requirement) => Ok(requirement.into_inner().into()), - Err(err) => Err(MetadataError::LoweringError( - requirement_name.clone(), - Box::new(err), - )), - }) + .map( + move |requirement| match requirement { + Ok(requirement) => Ok(requirement.into_inner().into()), + Err(err) => Err(MetadataError::LoweringError( + requirement_name.clone(), + Box::new(err), + )), + }, + ) }) .collect::, _>>()?; result.insert(package_name, lowered); diff --git a/crates/uv-scripts/src/lib.rs b/crates/uv-scripts/src/lib.rs index b80cdc219..b0b9fde2e 100644 --- a/crates/uv-scripts/src/lib.rs +++ b/crates/uv-scripts/src/lib.rs @@ -381,6 +381,8 @@ pub struct ToolUv { pub override_dependencies: Option>>, pub constraint_dependencies: Option>>, pub build_constraint_dependencies: Option>>, + pub extra_build_dependencies: + Option>>>, pub sources: Option>, } diff --git a/crates/uv/src/commands/build_frontend.rs b/crates/uv/src/commands/build_frontend.rs index 7c62a30f7..bd39bad18 100644 --- a/crates/uv/src/commands/build_frontend.rs +++ b/crates/uv/src/commands/build_frontend.rs @@ -564,9 +564,8 @@ async fn build_package( let workspace_cache = WorkspaceCache::default(); // Create a build dispatch. - let extra_build_requires = uv_distribution::ExtraBuildRequires::from_lowered( - extra_build_dependencies.clone(), - ); + let extra_build_requires = + uv_distribution::ExtraBuildRequires::from_lowered(extra_build_dependencies.clone()); let build_dispatch = BuildDispatch::new( &client, cache, diff --git a/crates/uv/src/commands/pip/compile.rs b/crates/uv/src/commands/pip/compile.rs index ed54bb15c..7dbaaf428 100644 --- a/crates/uv/src/commands/pip/compile.rs +++ b/crates/uv/src/commands/pip/compile.rs @@ -477,9 +477,8 @@ pub(crate) async fn pip_compile( .map(|constraint| constraint.requirement.clone()), ); - let extra_build_requires = uv_distribution::ExtraBuildRequires::from_lowered( - extra_build_dependencies.clone(), - ); + let extra_build_requires = + uv_distribution::ExtraBuildRequires::from_lowered(extra_build_dependencies.clone()); let build_dispatch = BuildDispatch::new( &client, &cache, diff --git a/crates/uv/src/commands/pip/install.rs b/crates/uv/src/commands/pip/install.rs index 2a536f8ca..f202f3ab0 100644 --- a/crates/uv/src/commands/pip/install.rs +++ b/crates/uv/src/commands/pip/install.rs @@ -420,9 +420,8 @@ pub(crate) async fn pip_install( let state = SharedState::default(); // Create a build dispatch. - let extra_build_requires = uv_distribution::ExtraBuildRequires::from_lowered( - extra_build_dependencies.clone(), - ); + let extra_build_requires = + uv_distribution::ExtraBuildRequires::from_lowered(extra_build_dependencies.clone()); let build_dispatch = BuildDispatch::new( &client, &cache, diff --git a/crates/uv/src/commands/pip/sync.rs b/crates/uv/src/commands/pip/sync.rs index dbcb78fdd..be1161c8e 100644 --- a/crates/uv/src/commands/pip/sync.rs +++ b/crates/uv/src/commands/pip/sync.rs @@ -355,9 +355,8 @@ pub(crate) async fn pip_sync( let state = SharedState::default(); // Create a build dispatch. - let extra_build_requires = uv_distribution::ExtraBuildRequires::from_lowered( - extra_build_dependencies.clone(), - ); + let extra_build_requires = + uv_distribution::ExtraBuildRequires::from_lowered(extra_build_dependencies.clone()); let build_dispatch = BuildDispatch::new( &client, &cache, diff --git a/crates/uv/src/commands/project/add.rs b/crates/uv/src/commands/project/add.rs index 19b4d0d42..73b6a81ab 100644 --- a/crates/uv/src/commands/project/add.rs +++ b/crates/uv/src/commands/project/add.rs @@ -1298,6 +1298,7 @@ impl PythonTarget { /// Represents the destination where dependencies are added, either to a project or a script. #[derive(Debug, Clone)] +#[allow(clippy::large_enum_variant)] pub(super) enum AddTarget { /// A PEP 723 script, with inline metadata. Script(Pep723Script, Box), @@ -1398,6 +1399,7 @@ impl AddTarget { } #[derive(Debug, Clone)] +#[allow(clippy::large_enum_variant)] enum AddTargetSnapshot { Script(Pep723Script, Option>), Project(VirtualProject, Option>), diff --git a/crates/uv/src/commands/project/export.rs b/crates/uv/src/commands/project/export.rs index c14bfd904..b78fc8ce8 100644 --- a/crates/uv/src/commands/project/export.rs +++ b/crates/uv/src/commands/project/export.rs @@ -32,6 +32,7 @@ use crate::printer::Printer; use crate::settings::{NetworkSettings, ResolverSettings}; #[derive(Debug, Clone)] +#[allow(clippy::large_enum_variant)] enum ExportTarget { /// A PEP 723 script, with inline metadata. Script(Pep723Script), diff --git a/crates/uv/src/commands/project/lock.rs b/crates/uv/src/commands/project/lock.rs index 73b89d084..ff6686691 100644 --- a/crates/uv/src/commands/project/lock.rs +++ b/crates/uv/src/commands/project/lock.rs @@ -42,7 +42,7 @@ use crate::commands::pip::loggers::{DefaultResolveLogger, ResolveLogger, Summary use crate::commands::project::lock_target::LockTarget; use crate::commands::project::{ ProjectError, ProjectInterpreter, ScriptInterpreter, UniversalState, - init_script_python_requirement, + init_script_python_requirement, script_specification, }; use crate::commands::reporters::{PythonDownloadReporter, ResolverReporter}; use crate::commands::{ExitStatus, ScriptPath, diagnostics, pip}; @@ -678,9 +678,16 @@ async fn do_lock( index_locations, *sources, )?, - LockTarget::Script(_) => uv_distribution::ExtraBuildRequires::from_lowered( - extra_build_dependencies.clone(), - ), + LockTarget::Script(script) => { + // Try to get extra build dependencies from the script metadata + script_specification(Pep723ItemRef::Script(script), settings)? + .map(|spec| spec.extra_build_requires) + .unwrap_or_else(|| { + uv_distribution::ExtraBuildRequires::from_lowered( + extra_build_dependencies.clone(), + ) + }) + } }; let build_dispatch = BuildDispatch::new( &client, diff --git a/crates/uv/src/commands/project/mod.rs b/crates/uv/src/commands/project/mod.rs index e91026db8..96860b911 100644 --- a/crates/uv/src/commands/project/mod.rs +++ b/crates/uv/src/commands/project/mod.rs @@ -46,6 +46,7 @@ use uv_types::{BuildIsolation, EmptyInstalledPackages, HashStrategy}; use uv_virtualenv::remove_virtualenv; use uv_warnings::{warn_user, warn_user_once}; use uv_workspace::dependency_groups::DependencyGroupError; +use uv_workspace::pyproject::ExtraBuildDependencies; use uv_workspace::pyproject::PyProjectToml; use uv_workspace::{RequiresPythonSources, Workspace, WorkspaceCache}; @@ -1741,9 +1742,8 @@ pub(crate) async fn resolve_names( let build_hasher = HashStrategy::default(); // Create a build dispatch. - let extra_build_requires = uv_distribution::ExtraBuildRequires::from_lowered( - extra_build_dependencies.clone(), - ); + let extra_build_requires = + uv_distribution::ExtraBuildRequires::from_lowered(extra_build_dependencies.clone()); let build_dispatch = BuildDispatch::new( &client, cache, @@ -1954,9 +1954,8 @@ pub(crate) async fn resolve_environment( let workspace_cache = WorkspaceCache::default(); // Create a build dispatch. - let extra_build_requires = uv_distribution::ExtraBuildRequires::from_lowered( - extra_build_dependencies.clone(), - ); + let extra_build_requires = + uv_distribution::ExtraBuildRequires::from_lowered(extra_build_dependencies.clone()); let resolve_dispatch = BuildDispatch::new( &client, cache, @@ -2097,9 +2096,8 @@ pub(crate) async fn sync_environment( }; // Create a build dispatch. - let extra_build_requires = uv_distribution::ExtraBuildRequires::from_lowered( - extra_build_dependencies.clone(), - ); + let extra_build_requires = + uv_distribution::ExtraBuildRequires::from_lowered(extra_build_dependencies.clone()); let build_dispatch = BuildDispatch::new( &client, cache, @@ -2157,6 +2155,15 @@ pub(crate) async fn sync_environment( Ok(venv) } +/// A script specification that includes both requirements and extra build dependencies. +#[derive(Debug)] +pub(crate) struct ScriptSpecification { + /// The requirements specification for the script. + pub(crate) requirements: RequirementsSpecification, + /// The extra build dependencies for the script. + pub(crate) extra_build_requires: uv_distribution::ExtraBuildRequires, +} + /// The result of updating a [`PythonEnvironment`] to satisfy a set of [`RequirementsSource`]s. #[derive(Debug)] pub(crate) struct EnvironmentUpdate { @@ -2179,6 +2186,7 @@ pub(crate) async fn update_environment( spec: RequirementsSpecification, modifications: Modifications, build_constraints: Constraints, + extra_build_requires: uv_distribution::ExtraBuildRequires, settings: &ResolverInstallerSettings, network_settings: &NetworkSettings, state: &SharedState, @@ -2209,7 +2217,7 @@ pub(crate) async fn update_environment( link_mode, no_build_isolation, no_build_isolation_package, - extra_build_dependencies, + extra_build_dependencies: _, prerelease, resolution, sources, @@ -2326,9 +2334,6 @@ pub(crate) async fn update_environment( }; // Create a build dispatch. - let extra_build_requires = uv_distribution::ExtraBuildRequires::from_lowered( - extra_build_dependencies.clone(), - ); let build_dispatch = BuildDispatch::new( &client, cache, @@ -2547,12 +2552,12 @@ pub(crate) fn detect_conflicts( Ok(()) } -/// Determine the [`RequirementsSpecification`] for a script. +/// Determine the [`ScriptSpecification`] for a script. #[allow(clippy::result_large_err)] pub(crate) fn script_specification( script: Pep723ItemRef<'_>, settings: &ResolverSettings, -) -> Result, ProjectError> { +) -> Result, ProjectError> { let Some(dependencies) = script.metadata().dependencies.as_ref() else { return Ok(None); }; @@ -2647,11 +2652,47 @@ pub(crate) fn script_specification( }) .collect::, _>>()?; - Ok(Some(RequirementsSpecification::from_overrides( - requirements, - constraints, - overrides, - ))) + // Collect any `tool.uv.extra-build-dependencies` from the script. + let empty = BTreeMap::default(); + let script_extra_build_dependencies = script + .metadata() + .tool + .as_ref() + .and_then(|tool| tool.uv.as_ref()) + .and_then(|uv| uv.extra_build_dependencies.as_ref()) + .unwrap_or(&empty); + + // Lower the extra build dependencies + let mut extra_build_dependencies = ExtraBuildDependencies::default(); + for (name, requirements) in script_extra_build_dependencies { + let lowered_requirements: Vec<_> = requirements + .iter() + .cloned() + .flat_map(|requirement| { + LoweredRequirement::from_non_workspace_requirement( + requirement, + script_dir.as_ref(), + script_sources, + script_indexes, + &settings.index_locations, + ) + .map_ok(|req| req.into_inner().into()) + }) + .collect::, _>>()?; + extra_build_dependencies.insert(name.clone(), lowered_requirements); + } + + let extra_build_requires = + uv_distribution::ExtraBuildRequires::from_lowered(extra_build_dependencies); + + Ok(Some(ScriptSpecification { + requirements: RequirementsSpecification::from_overrides( + requirements, + constraints, + overrides, + ), + extra_build_requires, + })) } /// Warn if the user provides (e.g.) an `--index-url` in a requirements file. diff --git a/crates/uv/src/commands/project/remove.rs b/crates/uv/src/commands/project/remove.rs index 50615699e..cfda7327b 100644 --- a/crates/uv/src/commands/project/remove.rs +++ b/crates/uv/src/commands/project/remove.rs @@ -386,6 +386,7 @@ pub(crate) async fn remove( /// Represents the destination where dependencies are added, either to a project or a script. #[derive(Debug)] +#[allow(clippy::large_enum_variant)] enum RemoveTarget { /// A PEP 723 script, with inline metadata. Project(VirtualProject), diff --git a/crates/uv/src/commands/project/run.rs b/crates/uv/src/commands/project/run.rs index a1476c63c..8b863eb10 100644 --- a/crates/uv/src/commands/project/run.rs +++ b/crates/uv/src/commands/project/run.rs @@ -358,7 +358,9 @@ hint: If you are running a script with `{}` in the shebang, you may need to incl } // Install the script requirements, if necessary. Otherwise, use an isolated environment. - if let Some(spec) = script_specification((&script).into(), &settings.resolver)? { + if let Some(script_spec) = script_specification((&script).into(), &settings.resolver)? { + let spec = script_spec.requirements; + let script_extra_build_requires = script_spec.extra_build_requires; let environment = ScriptEnvironment::get_or_init( (&script).into(), python.as_deref().map(PythonRequest::parse), @@ -407,6 +409,7 @@ hint: If you are running a script with `{}` in the shebang, you may need to incl spec, modifications, build_constraints.unwrap_or_default(), + script_extra_build_requires, &settings, &network_settings, &sync_state, diff --git a/crates/uv/src/commands/project/sync.rs b/crates/uv/src/commands/project/sync.rs index 9dbb47888..c93e1dc39 100644 --- a/crates/uv/src/commands/project/sync.rs +++ b/crates/uv/src/commands/project/sync.rs @@ -14,12 +14,11 @@ use uv_client::{BaseClientBuilder, FlatIndexClient, RegistryClientBuilder}; use uv_configuration::{ Concurrency, Constraints, DependencyGroups, DependencyGroupsWithDefaults, DryRun, EditableMode, ExtrasSpecification, ExtrasSpecificationWithDefaults, HashCheckingMode, InstallOptions, - PreviewMode, TargetTriple, + PreviewMode, TargetTriple, Upgrade, }; use uv_dispatch::BuildDispatch; use uv_distribution_types::{ - DirectorySourceDist, Dist, Index, Requirement, Resolution, ResolvedDist, - SourceDist, + DirectorySourceDist, Dist, Index, Requirement, Resolution, ResolvedDist, SourceDist, }; use uv_fs::{PortablePathBuf, Simplified}; use uv_installer::SitePackages; @@ -27,15 +26,14 @@ use uv_normalize::{DefaultExtras, DefaultGroups, PackageName}; use uv_pep508::{MarkerTree, VersionOrUrl}; use uv_pypi_types::{ParsedArchiveUrl, ParsedGitUrl, ParsedUrl}; use uv_python::{PythonDownloads, PythonEnvironment, PythonPreference, PythonRequest}; -use uv_resolver::{FlatIndex, Installable, Lock}; +use uv_requirements::RequirementsSpecification; +use uv_resolver::{FlatIndex, ForkStrategy, Installable, Lock, PrereleaseMode, ResolutionMode}; use uv_scripts::{Pep723ItemRef, Pep723Script}; use uv_settings::PythonInstallMirrors; use uv_types::{BuildIsolation, HashStrategy}; use uv_warnings::{warn_user, warn_user_once}; use uv_workspace::pyproject::{ExtraBuildDependencies, Source}; -use uv_workspace::{ - DiscoveryOptions, MemberDiscovery, VirtualProject, Workspace, WorkspaceCache, -}; +use uv_workspace::{DiscoveryOptions, MemberDiscovery, VirtualProject, Workspace, WorkspaceCache}; use crate::commands::pip::loggers::{DefaultInstallLogger, DefaultResolveLogger, InstallLogger}; use crate::commands::pip::operations::Modifications; @@ -50,7 +48,9 @@ use crate::commands::project::{ }; use crate::commands::{ExitStatus, diagnostics}; use crate::printer::Printer; -use crate::settings::{InstallerSettingsRef, NetworkSettings, ResolverInstallerSettings}; +use crate::settings::{ + InstallerSettingsRef, NetworkSettings, ResolverInstallerSettings, ResolverSettings, +}; /// Sync the project environment. #[allow(clippy::fn_params_excessive_bools)] @@ -223,8 +223,18 @@ pub(crate) async fn sync( } // Parse the requirements from the script. - let spec = script_specification(Pep723ItemRef::Script(script), &settings.resolver)? - .unwrap_or_default(); + let script_spec = + script_specification(Pep723ItemRef::Script(script), &settings.resolver)?; + let (spec, script_extra_build_requires) = if let Some(script_spec) = script_spec { + (script_spec.requirements, script_spec.extra_build_requires) + } else { + ( + RequirementsSpecification::default(), + uv_distribution::ExtraBuildRequires::from_lowered( + ExtraBuildDependencies::default(), + ), + ) + }; // Parse the build constraints from the script. let build_constraints = script @@ -249,6 +259,7 @@ pub(crate) async fn sync( spec, modifications, build_constraints.unwrap_or_default(), + script_extra_build_requires, &settings, &network_settings, &PlatformState::default(), @@ -495,6 +506,7 @@ fn identify_installation_target<'a>( } #[derive(Debug, Clone)] +#[allow(clippy::large_enum_variant)] enum SyncTarget { /// Sync a project environment. Project(VirtualProject), @@ -546,7 +558,6 @@ impl Deref for SyncEnvironment { } } - /// Sync a lockfile with an environment. #[allow(clippy::fn_params_excessive_bools)] pub(super) async fn do_sync( @@ -607,12 +618,35 @@ pub(super) async fn do_sync( sources, )? } - InstallTarget::Script { .. } => uv_distribution::ExtraBuildRequires { - extra_build_dependencies: ExtraBuildDependencies::default(), - }, + InstallTarget::Script { script, .. } => { + // Try to get extra build dependencies from the script metadata + let resolver_settings = ResolverSettings { + build_options: build_options.clone(), + config_setting: config_setting.clone(), + config_settings_package: config_settings_package.clone(), + dependency_metadata: dependency_metadata.clone(), + exclude_newer, + fork_strategy: ForkStrategy::default(), + index_locations: index_locations.clone(), + index_strategy, + keyring_provider, + link_mode, + no_build_isolation, + no_build_isolation_package: no_build_isolation_package.to_vec(), + extra_build_dependencies: extra_build_dependencies.clone(), + prerelease: PrereleaseMode::default(), + resolution: ResolutionMode::default(), + sources, + upgrade: Upgrade::default(), + }; + script_specification(Pep723ItemRef::Script(script), &resolver_settings)? + .map(|spec| spec.extra_build_requires) + .unwrap_or_else(|| uv_distribution::ExtraBuildRequires { + extra_build_dependencies: ExtraBuildDependencies::default(), + }) + } }; - let client_builder = BaseClientBuilder::new() .retries_from_env()? .connectivity(network_settings.connectivity) diff --git a/crates/uv/src/commands/tool/install.rs b/crates/uv/src/commands/tool/install.rs index 12de5fd1f..fa4ae243a 100644 --- a/crates/uv/src/commands/tool/install.rs +++ b/crates/uv/src/commands/tool/install.rs @@ -23,7 +23,7 @@ use uv_requirements::{RequirementsSource, RequirementsSpecification}; use uv_settings::{PythonInstallMirrors, ResolverInstallerOptions, ToolOptions}; use uv_tool::InstalledTools; use uv_warnings::warn_user; -use uv_workspace::WorkspaceCache; +use uv_workspace::{WorkspaceCache, pyproject::ExtraBuildDependencies}; use crate::commands::ExitStatus; use crate::commands::pip::loggers::{DefaultInstallLogger, DefaultResolveLogger}; @@ -439,6 +439,7 @@ pub(crate) async fn install( spec, Modifications::Exact, Constraints::from_requirements(build_constraints.iter().cloned()), + uv_distribution::ExtraBuildRequires::from_lowered(ExtraBuildDependencies::default()), &settings, &network_settings, &state, diff --git a/crates/uv/src/commands/tool/upgrade.rs b/crates/uv/src/commands/tool/upgrade.rs index 9d2d32a21..1c511524c 100644 --- a/crates/uv/src/commands/tool/upgrade.rs +++ b/crates/uv/src/commands/tool/upgrade.rs @@ -18,7 +18,7 @@ use uv_python::{ use uv_requirements::RequirementsSpecification; use uv_settings::{Combine, PythonInstallMirrors, ResolverInstallerOptions, ToolOptions}; use uv_tool::InstalledTools; -use uv_workspace::WorkspaceCache; +use uv_workspace::{WorkspaceCache, pyproject::ExtraBuildDependencies}; use crate::commands::pip::loggers::{ DefaultInstallLogger, SummaryResolveLogger, UpgradeInstallLogger, @@ -343,6 +343,7 @@ async fn upgrade_tool( spec, Modifications::Exact, build_constraints, + uv_distribution::ExtraBuildRequires::from_lowered(ExtraBuildDependencies::default()), &settings, network_settings, &state, diff --git a/crates/uv/src/commands/venv.rs b/crates/uv/src/commands/venv.rs index 77477afe8..c2395fc4f 100644 --- a/crates/uv/src/commands/venv.rs +++ b/crates/uv/src/commands/venv.rs @@ -275,9 +275,8 @@ pub(crate) async fn venv( // Do not allow builds let build_options = BuildOptions::new(NoBinary::None, NoBuild::All); - let extra_build_requires = uv_distribution::ExtraBuildRequires::from_lowered( - ExtraBuildDependencies::default(), - ); + let extra_build_requires = + uv_distribution::ExtraBuildRequires::from_lowered(ExtraBuildDependencies::default()); // Prep the build context. let build_dispatch = BuildDispatch::new( &client, diff --git a/crates/uv/tests/it/show_settings.rs b/crates/uv/tests/it/show_settings.rs index 00a8b5563..b30f7d6b2 100644 --- a/crates/uv/tests/it/show_settings.rs +++ b/crates/uv/tests/it/show_settings.rs @@ -3931,6 +3931,7 @@ fn resolve_both_special_fields() -> anyhow::Result<()> { torch_backend: None, no_build_isolation: false, no_build_isolation_package: [], + extra_build_dependencies: {}, build_options: BuildOptions { no_binary: None, no_build: None, diff --git a/crates/uv/tests/it/sync.rs b/crates/uv/tests/it/sync.rs index 74451f35e..6c612fceb 100644 --- a/crates/uv/tests/it/sync.rs +++ b/crates/uv/tests/it/sync.rs @@ -1778,7 +1778,7 @@ fn sync_extra_build_dependencies() -> Result<()> { [tool.uv.sources] child = { path = "child" } bad_child = { path = "bad_child" } - + [tool.uv.extra-build-dependencies] child = ["anyio"] "#})?; @@ -1833,7 +1833,7 @@ fn sync_extra_build_dependencies_sources() -> Result<()> { except ModuleNotFoundError: print("Missing `anyio` module", file=sys.stderr) sys.exit(1) - + # Check that we got the local version of anyio by checking for the marker if not hasattr(anyio, 'LOCAL_ANYIO_MARKER'): print("Found system anyio instead of local anyio", file=sys.stderr) @@ -1915,7 +1915,7 @@ fn sync_extra_build_dependencies_sources_from_child() -> Result<()> { except ModuleNotFoundError: print("Missing `anyio` module", file=sys.stderr) sys.exit(1) - + # Check that we got the local version of anyio by checking for the marker if not hasattr(anyio, 'LOCAL_ANYIO_MARKER'): print("Found system anyio instead of local anyio", file=sys.stderr) @@ -4593,6 +4593,187 @@ fn no_install_project_no_build() -> Result<()> { Ok(()) } +#[test] +fn sync_extra_build_dependencies_script() -> Result<()> { + let context = TestContext::new("3.12").with_filtered_counts(); + + // Write a test package that arbitrarily requires `anyio` at build time + let child = context.temp_dir.child("child"); + child.create_dir_all()?; + let child_pyproject_toml = child.child("pyproject.toml"); + child_pyproject_toml.write_str(indoc! {r#" + [project] + name = "child" + version = "0.1.0" + requires-python = ">=3.9" + [build-system] + requires = ["hatchling"] + backend-path = ["."] + build-backend = "build_backend" + "#})?; + let build_backend = child.child("build_backend.py"); + build_backend.write_str(indoc! {r#" + import sys + from hatchling.build import * + try: + import anyio + except ModuleNotFoundError: + print("Missing `anyio` module", file=sys.stderr) + sys.exit(1) + "#})?; + child.child("src/child/__init__.py").touch()?; + + // Create a script that depends on the child package + let script = context.temp_dir.child("script.py"); + script.write_str(indoc! {r#" + # /// script + # requires-python = ">=3.12" + # dependencies = ["child"] + # + # [tool.uv.sources] + # child = { path = "child" } + # /// + "#})?; + + let filters = context + .filters() + .into_iter() + .chain(vec![( + r"environments-v2/script-[a-z0-9]+", + "environments-v2/script-[HASH]", + )]) + .collect::>(); + + // Running `uv sync` should fail due to missing build-dependencies + uv_snapshot!(filters, context.sync().arg("--script").arg("script.py"), @r" + success: false + exit_code: 1 + ----- stdout ----- + + ----- stderr ----- + Creating script environment at: [CACHE_DIR]/environments-v2/script-[HASH] + Resolved [N] packages in [TIME] + × Failed to build `child @ file://[TEMP_DIR]/child` + ├─▶ The build backend returned an error + ╰─▶ Call to `build_backend.build_wheel` failed (exit status: 1) + + [stderr] + Missing `anyio` module + + hint: This usually indicates a problem with the package or the build environment. + "); + + // Add extra build dependencies to the script + script.write_str(indoc! {r#" + # /// script + # requires-python = ">=3.12" + # dependencies = ["child"] + # + # [tool.uv.sources] + # child = { path = "child" } + # + # [tool.uv.extra-build-dependencies] + # child = ["anyio"] + # /// + "#})?; + + // Running `uv sync` should now succeed due to extra build-dependencies + context.venv().arg("--clear").assert().success(); + uv_snapshot!(filters, context.sync().arg("--script").arg("script.py"), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Using script environment at: [CACHE_DIR]/environments-v2/script-[HASH] + Resolved [N] packages in [TIME] + Prepared [N] packages in [TIME] + Installed [N] packages in [TIME] + + child==0.1.0 (from file://[TEMP_DIR]/child) + "); + + Ok(()) +} + +#[test] +fn sync_extra_build_dependencies_script_sources() -> Result<()> { + let context = TestContext::new("3.12").with_filtered_counts(); + let anyio_local = context.workspace_root.join("scripts/packages/anyio_local"); + + // Write a test package that arbitrarily requires `anyio` at a specific _path_ at build time + let child = context.temp_dir.child("child"); + child.create_dir_all()?; + let child_pyproject_toml = child.child("pyproject.toml"); + child_pyproject_toml.write_str(indoc! {r#" + [project] + name = "child" + version = "0.1.0" + requires-python = ">=3.9" + [build-system] + requires = ["hatchling"] + backend-path = ["."] + build-backend = "build_backend" + "#})?; + let build_backend = child.child("build_backend.py"); + build_backend.write_str(&formatdoc! {r#" + import sys + from hatchling.build import * + try: + import anyio + except ModuleNotFoundError: + print("Missing `anyio` module", file=sys.stderr) + sys.exit(1) + + # Check that we got the local version of anyio by checking for the marker + if not hasattr(anyio, 'LOCAL_ANYIO_MARKER'): + print("Found system anyio instead of local anyio", file=sys.stderr) + sys.exit(1) + "#})?; + child.child("src/child/__init__.py").touch()?; + + // Create a script that depends on the child package + let script = context.temp_dir.child("script.py"); + script.write_str(&formatdoc! {r#" + # /// script + # requires-python = ">=3.12" + # dependencies = ["child"] + # + # [tool.uv.sources] + # anyio = {{ path = "{}" }} + # child = {{ path = "child" }} + # + # [tool.uv.extra-build-dependencies] + # child = ["anyio"] + # /// + "#, anyio_local.display() + })?; + + let filters = context + .filters() + .into_iter() + .chain(vec![( + r"environments-v2/script-[a-z0-9]+", + "environments-v2/script-[HASH]", + )]) + .collect::>(); + + // Running `uv sync` should succeed with the sources applied + uv_snapshot!(filters, context.sync().arg("--script").arg("script.py"), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Creating script environment at: [CACHE_DIR]/environments-v2/script-[HASH] + Resolved [N] packages in [TIME] + Prepared [N] packages in [TIME] + Installed [N] packages in [TIME] + + child==0.1.0 (from file://[TEMP_DIR]/child) + "); + + Ok(()) +} + #[test] fn virtual_no_build() -> Result<()> { let context = TestContext::new("3.12"); From c1020e6cf683007701df890191c3de16abd6af3a Mon Sep 17 00:00:00 2001 From: Zanie Blue Date: Thu, 24 Jul 2025 17:35:28 -0500 Subject: [PATCH 12/14] Update snapshot --- crates/uv/tests/it/pip_install.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/crates/uv/tests/it/pip_install.rs b/crates/uv/tests/it/pip_install.rs index 936f77aff..577fe79f4 100644 --- a/crates/uv/tests/it/pip_install.rs +++ b/crates/uv/tests/it/pip_install.rs @@ -3981,16 +3981,17 @@ fn config_settings_registry() { .arg("iniconfig") .arg("--no-binary") .arg("iniconfig") - .arg("-C=global-option=build_ext"), @r###" + .arg("-C=global-option=build_ext"), @r" success: true exit_code: 0 ----- stdout ----- ----- stderr ----- Resolved 1 package in [TIME] + Prepared 1 package in [TIME] Installed 1 package in [TIME] + iniconfig==2.0.0 - "### + " ); // Uninstall the package. From 5d9e877c84bedf65958d5448fb30e25b91fcdb76 Mon Sep 17 00:00:00 2001 From: Zanie Blue Date: Fri, 25 Jul 2025 15:42:10 -0500 Subject: [PATCH 13/14] Fix Windows? --- crates/uv/tests/it/sync.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/uv/tests/it/sync.rs b/crates/uv/tests/it/sync.rs index 597a9db87..0093628ec 100644 --- a/crates/uv/tests/it/sync.rs +++ b/crates/uv/tests/it/sync.rs @@ -1902,7 +1902,7 @@ fn sync_extra_build_dependencies_sources_from_child() -> Result<()> { [tool.uv.sources] anyio = {{ path = "{}" }} - "#, anyio_local.display() + "#, anyio_local.portable_display() })?; let build_backend = child.child("build_backend.py"); build_backend.write_str(&formatdoc! {r#" @@ -4745,7 +4745,7 @@ fn sync_extra_build_dependencies_script_sources() -> Result<()> { # [tool.uv.extra-build-dependencies] # child = ["anyio"] # /// - "#, anyio_local.display() + "#, anyio_local.portable_display() })?; let filters = context From 7c0b08c1abb7d6e83a08e1b8cbe6ad4d171ced83 Mon Sep 17 00:00:00 2001 From: Zanie Blue Date: Mon, 28 Jul 2025 11:45:41 -0500 Subject: [PATCH 14/14] Update doc --- crates/uv-settings/src/settings.rs | 16 ++++++++-------- crates/uv-workspace/src/pyproject.rs | 8 ++++---- docs/reference/settings.md | 24 ++++++++++++------------ uv.schema.json | 4 ++-- 4 files changed, 26 insertions(+), 26 deletions(-) diff --git a/crates/uv-settings/src/settings.rs b/crates/uv-settings/src/settings.rs index 23b11e87b..aa9243f24 100644 --- a/crates/uv-settings/src/settings.rs +++ b/crates/uv-settings/src/settings.rs @@ -625,11 +625,11 @@ pub struct ResolverInstallerOptions { "# )] pub no_build_isolation_package: Option>, - /// Additional build dependencies for dependencies. + /// Additional build dependencies for packages. /// - /// This is intended for enabling more packages to be built with - /// build-isolation, by adding dependencies that they ambiently - /// assume to exist (`setuptools` and `pip` being common). + /// This allows extending the PEP 517 build environment for the project's dependencies with + /// additional packages. This is useful for packages that assume the presence of packages, like, + /// `pip`, and do not declare them as build dependencies. #[option( default = "[]", value_type = "dict", @@ -1135,11 +1135,11 @@ pub struct PipOptions { "# )] pub no_build_isolation_package: Option>, - /// Additional build dependencies for dependencies. + /// Additional build dependencies for packages. /// - /// This is intended for enabling more packages to be built with - /// build-isolation, by adding dependencies that they ambiently - /// assume to exist (`setuptools` and `pip` being common). + /// This allows extending the PEP 517 build environment for the project's dependencies with + /// additional packages. This is useful for packages that assume the presence of packages, like, + /// `pip`, and do not declare them as build dependencies. #[option( default = "[]", value_type = "dict", diff --git a/crates/uv-workspace/src/pyproject.rs b/crates/uv-workspace/src/pyproject.rs index d04c87efb..4a1c9231c 100644 --- a/crates/uv-workspace/src/pyproject.rs +++ b/crates/uv-workspace/src/pyproject.rs @@ -378,11 +378,11 @@ pub struct ToolUv { )] pub dependency_groups: Option, - /// Additional build dependencies for dependencies. + /// Additional build dependencies for packages. /// - /// This is intended for enabling more packages to be built with - /// build-isolation, by adding dependencies that they ambiently - /// assume to exist (`setuptools` and `pip` being common). + /// This allows extending the PEP 517 build environment for the project's dependencies with + /// additional packages. This is useful for packages that assume the presence of packages, like, + /// `pip`, and do not declare them as build dependencies. #[option( default = "[]", value_type = "dict", diff --git a/docs/reference/settings.md b/docs/reference/settings.md index 3cca74d95..ebb3bc958 100644 --- a/docs/reference/settings.md +++ b/docs/reference/settings.md @@ -204,11 +204,11 @@ environments = ["sys_platform == 'darwin'"] ### [`extra-build-dependencies`](#extra-build-dependencies) {: #extra-build-dependencies } -Additional build dependencies for dependencies. +Additional build dependencies for packages. -This is intended for enabling more packages to be built with -build-isolation, by adding dependencies that they ambiently -assume to exist (`setuptools` and `pip` being common). +This allows extending the PEP 517 build environment for the project's dependencies with +additional packages. This is useful for packages that assume the presence of packages, like, +`pip`, and do not declare them as build dependencies. **Default value**: `[]` @@ -1125,11 +1125,11 @@ behave consistently across timezones. ### [`extra-build-dependencies`](#extra-build-dependencies) {: #extra-build-dependencies } -Additional build dependencies for dependencies. +Additional build dependencies for packages. -This is intended for enabling more packages to be built with -build-isolation, by adding dependencies that they ambiently -assume to exist (`setuptools` and `pip` being common). +This allows extending the PEP 517 build environment for the project's dependencies with +additional packages. This is useful for packages that assume the presence of packages, like, +`pip`, and do not declare them as build dependencies. **Default value**: `[]` @@ -2617,11 +2617,11 @@ Only applies to `pyproject.toml`, `setup.py`, and `setup.cfg` sources. #### [`extra-build-dependencies`](#pip_extra-build-dependencies) {: #pip_extra-build-dependencies } -Additional build dependencies for dependencies. +Additional build dependencies for packages. -This is intended for enabling more packages to be built with -build-isolation, by adding dependencies that they ambiently -assume to exist (`setuptools` and `pip` being common). +This allows extending the PEP 517 build environment for the project's dependencies with +additional packages. This is useful for packages that assume the presence of packages, like, +`pip`, and do not declare them as build dependencies. **Default value**: `[]` diff --git a/uv.schema.json b/uv.schema.json index eaf8d4104..619e8dff3 100644 --- a/uv.schema.json +++ b/uv.schema.json @@ -215,7 +215,7 @@ ] }, "extra-build-dependencies": { - "description": "Additional build dependencies for dependencies.\n\nThis is intended for enabling more packages to be built with\nbuild-isolation, by adding dependencies that they ambiently\nassume to exist (`setuptools` and `pip` being common).", + "description": "Additional build dependencies for packages.\n\nThis allows extending the PEP 517 build environment for the project's dependencies with\nadditional packages. This is useful for packages that assume the presence of packages, like,\n`pip`, and do not declare them as build dependencies.", "anyOf": [ { "$ref": "#/definitions/ToolUvExtraBuildDependencies" @@ -1299,7 +1299,7 @@ } }, "extra-build-dependencies": { - "description": "Additional build dependencies for dependencies.\n\nThis is intended for enabling more packages to be built with\nbuild-isolation, by adding dependencies that they ambiently\nassume to exist (`setuptools` and `pip` being common).", + "description": "Additional build dependencies for packages.\n\nThis allows extending the PEP 517 build environment for the project's dependencies with\nadditional packages. This is useful for packages that assume the presence of packages, like,\n`pip`, and do not declare them as build dependencies.", "type": [ "object", "null"