Merge branch 'zb/extra-build-dependencies' into zb/flash-attn

This commit is contained in:
Zanie Blue 2025-07-28 16:37:28 -05:00
commit 1c52d9ad35
44 changed files with 1474 additions and 78 deletions

1
Cargo.lock generated
View File

@ -5538,6 +5538,7 @@ dependencies = [
"tracing-test", "tracing-test",
"unicode-width 0.2.1", "unicode-width 0.2.1",
"url", "url",
"uv-cache-key",
"uv-fs", "uv-fs",
"uv-normalize", "uv-normalize",
"uv-pep440", "uv-pep440",

View File

@ -141,6 +141,9 @@ mod resolver {
universal: bool, universal: bool,
) -> Result<ResolverOutput> { ) -> Result<ResolverOutput> {
let build_isolation = BuildIsolation::default(); let build_isolation = BuildIsolation::default();
let extra_build_requires = uv_distribution::ExtraBuildRequires {
extra_build_dependencies: uv_workspace::pyproject::ExtraBuildDependencies::default(),
};
let build_options = BuildOptions::default(); let build_options = BuildOptions::default();
let concurrency = Concurrency::default(); let concurrency = Concurrency::default();
let config_settings = ConfigSettings::default(); let config_settings = ConfigSettings::default();
@ -187,6 +190,7 @@ mod resolver {
&config_settings, &config_settings,
&config_settings_package, &config_settings_package,
build_isolation, build_isolation,
&extra_build_requires,
LinkMode::default(), LinkMode::default(),
&build_options, &build_options,
&hashes, &hashes,

View File

@ -4,6 +4,7 @@
mod error; mod error;
use std::borrow::Cow;
use std::ffi::OsString; use std::ffi::OsString;
use std::fmt::Formatter; use std::fmt::Formatter;
use std::fmt::Write; use std::fmt::Write;
@ -42,6 +43,7 @@ use uv_static::EnvVars;
use uv_types::{AnyErrorBuild, BuildContext, BuildIsolation, BuildStack, SourceBuildTrait}; use uv_types::{AnyErrorBuild, BuildContext, BuildIsolation, BuildStack, SourceBuildTrait};
use uv_warnings::warn_user_once; use uv_warnings::warn_user_once;
use uv_workspace::WorkspaceCache; use uv_workspace::WorkspaceCache;
use uv_workspace::pyproject::ExtraBuildDependencies;
pub use crate::error::{Error, MissingHeaderCause}; pub use crate::error::{Error, MissingHeaderCause};
@ -281,6 +283,7 @@ impl SourceBuild {
workspace_cache: &WorkspaceCache, workspace_cache: &WorkspaceCache,
config_settings: ConfigSettings, config_settings: ConfigSettings,
build_isolation: BuildIsolation<'_>, build_isolation: BuildIsolation<'_>,
extra_build_dependencies: &ExtraBuildDependencies,
build_stack: &BuildStack, build_stack: &BuildStack,
build_kind: BuildKind, build_kind: BuildKind,
mut environment_variables: FxHashMap<OsString, OsString>, mut environment_variables: FxHashMap<OsString, OsString>,
@ -297,7 +300,6 @@ impl SourceBuild {
}; };
let default_backend: Pep517Backend = DEFAULT_BACKEND.clone(); let default_backend: Pep517Backend = DEFAULT_BACKEND.clone();
// Check if we have a PEP 517 build backend. // Check if we have a PEP 517 build backend.
let (pep517_backend, project) = Self::extract_pep517_backend( let (pep517_backend, project) = Self::extract_pep517_backend(
&source_tree, &source_tree,
@ -322,6 +324,14 @@ impl SourceBuild {
.or(fallback_package_version) .or(fallback_package_version)
.cloned(); .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. // 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()) { let venv = if let Some(venv) = build_isolation.shared_environment(package_name.as_ref()) {
venv.clone() venv.clone()
@ -349,10 +359,13 @@ impl SourceBuild {
source_build_context, source_build_context,
&default_backend, &default_backend,
&pep517_backend, &pep517_backend,
extra_build_dependencies,
build_stack, build_stack,
) )
.await?; .await?;
// TODO(zanieb): We'll report `build-system.requires` here but it may include
// `extra-build-dependencies`
build_context build_context
.install(&resolved_requirements, &venv, build_stack) .install(&resolved_requirements, &venv, build_stack)
.await .await
@ -471,10 +484,13 @@ impl SourceBuild {
source_build_context: SourceBuildContext, source_build_context: SourceBuildContext,
default_backend: &Pep517Backend, default_backend: &Pep517Backend,
pep517_backend: &Pep517Backend, pep517_backend: &Pep517Backend,
extra_build_dependencies: Vec<Requirement>,
build_stack: &BuildStack, build_stack: &BuildStack,
) -> Result<Resolution, Error> { ) -> Result<Resolution, Error> {
Ok( 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; let mut resolution = source_build_context.default_resolution.lock().await;
if let Some(resolved_requirements) = &*resolution { if let Some(resolved_requirements) = &*resolution {
resolved_requirements.clone() resolved_requirements.clone()
@ -489,8 +505,21 @@ impl SourceBuild {
resolved_requirements resolved_requirements
} }
} else { } 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 build_context
.resolve(&pep517_backend.requirements, build_stack) .resolve(&requirements, build_stack)
.await .await
.map_err(|err| { .map_err(|err| {
Error::RequirementsResolve("`build-system.requires`", err.into()) Error::RequirementsResolve("`build-system.requires`", err.into())
@ -604,6 +633,7 @@ impl SourceBuild {
); );
} }
} }
default_backend.clone() default_backend.clone()
}; };
Ok((backend, pyproject_toml.project)) Ok((backend, pyproject_toml.project))

View File

@ -347,6 +347,7 @@ pub fn resolver_options(
}), }),
no_build_isolation: flag(no_build_isolation, build_isolation, "build-isolation"), no_build_isolation: flag(no_build_isolation, build_isolation, "build-isolation"),
no_build_isolation_package: Some(no_build_isolation_package), no_build_isolation_package: Some(no_build_isolation_package),
extra_build_dependencies: None,
exclude_newer, exclude_newer,
link_mode, link_mode,
no_build: flag(no_build, build, "build"), no_build: flag(no_build, build, "build"),
@ -465,6 +466,7 @@ pub fn resolver_installer_options(
} else { } else {
Some(no_build_isolation_package) Some(no_build_isolation_package)
}, },
extra_build_dependencies: None,
exclude_newer, exclude_newer,
link_mode, link_mode,
compile_bytecode: flag(compile_bytecode, no_compile_bytecode, "compile-bytecode"), compile_bytecode: flag(compile_bytecode, no_compile_bytecode, "compile-bytecode"),

View File

@ -15,6 +15,7 @@ bitflags::bitflags! {
const PYLOCK = 1 << 3; const PYLOCK = 1 << 3;
const ADD_BOUNDS = 1 << 4; const ADD_BOUNDS = 1 << 4;
const PREFER_LOCKED_BUILDS = 1 << 5; const PREFER_LOCKED_BUILDS = 1 << 5;
const EXTRA_BUILD_DEPENDENCIES = 1 << 6;
} }
} }
@ -30,6 +31,7 @@ impl PreviewFeatures {
Self::PYLOCK => "pylock", Self::PYLOCK => "pylock",
Self::ADD_BOUNDS => "add-bounds", Self::ADD_BOUNDS => "add-bounds",
Self::PREFER_LOCKED_BUILDS => "prefer-locked-builds", Self::PREFER_LOCKED_BUILDS => "prefer-locked-builds",
Self::EXTRA_BUILD_DEPENDENCIES => "extra-build-dependencies",
_ => panic!("`flag_as_str` can only be used for exactly one feature flag"), _ => panic!("`flag_as_str` can only be used for exactly one feature flag"),
} }
} }
@ -73,6 +75,7 @@ impl FromStr for PreviewFeatures {
"pylock" => Self::PYLOCK, "pylock" => Self::PYLOCK,
"add-bounds" => Self::ADD_BOUNDS, "add-bounds" => Self::ADD_BOUNDS,
"prefer-locked-builds" => Self::PREFER_LOCKED_BUILDS, "prefer-locked-builds" => Self::PREFER_LOCKED_BUILDS,
"extra-build-dependencies" => Self::EXTRA_BUILD_DEPENDENCIES,
_ => { _ => {
warn_user_once!("Unknown preview feature: `{part}`"); warn_user_once!("Unknown preview feature: `{part}`");
continue; continue;
@ -239,6 +242,10 @@ mod tests {
PreviewFeatures::PREFER_LOCKED_BUILDS.flag_as_str(), PreviewFeatures::PREFER_LOCKED_BUILDS.flag_as_str(),
"prefer-locked-builds" "prefer-locked-builds"
); );
assert_eq!(
PreviewFeatures::EXTRA_BUILD_DEPENDENCIES.flag_as_str(),
"extra-build-dependencies"
);
} }
#[test] #[test]

View File

@ -22,6 +22,7 @@ use uv_configuration::{
}; };
use uv_configuration::{BuildOutput, Concurrency}; use uv_configuration::{BuildOutput, Concurrency};
use uv_distribution::DistributionDatabase; use uv_distribution::DistributionDatabase;
use uv_distribution::ExtraBuildRequires;
use uv_distribution_filename::DistFilename; use uv_distribution_filename::DistFilename;
use uv_distribution_types::{ use uv_distribution_types::{
CachedDist, DependencyMetadata, Identifier, IndexCapabilities, IndexLocations, CachedDist, DependencyMetadata, Identifier, IndexCapabilities, IndexLocations,
@ -88,6 +89,7 @@ pub struct BuildDispatch<'a> {
shared_state: SharedState, shared_state: SharedState,
dependency_metadata: &'a DependencyMetadata, dependency_metadata: &'a DependencyMetadata,
build_isolation: BuildIsolation<'a>, build_isolation: BuildIsolation<'a>,
extra_build_requires: &'a ExtraBuildRequires,
link_mode: uv_install_wheel::LinkMode, link_mode: uv_install_wheel::LinkMode,
build_options: &'a BuildOptions, build_options: &'a BuildOptions,
config_settings: &'a ConfigSettings, config_settings: &'a ConfigSettings,
@ -117,6 +119,7 @@ impl<'a> BuildDispatch<'a> {
config_settings: &'a ConfigSettings, config_settings: &'a ConfigSettings,
config_settings_package: &'a PackageConfigSettings, config_settings_package: &'a PackageConfigSettings,
build_isolation: BuildIsolation<'a>, build_isolation: BuildIsolation<'a>,
extra_build_requires: &'a ExtraBuildRequires,
link_mode: uv_install_wheel::LinkMode, link_mode: uv_install_wheel::LinkMode,
build_options: &'a BuildOptions, build_options: &'a BuildOptions,
hasher: &'a HashStrategy, hasher: &'a HashStrategy,
@ -140,6 +143,7 @@ impl<'a> BuildDispatch<'a> {
config_settings, config_settings,
config_settings_package, config_settings_package,
build_isolation, build_isolation,
extra_build_requires,
link_mode, link_mode,
build_options, build_options,
hasher, hasher,
@ -222,6 +226,10 @@ impl BuildContext for BuildDispatch<'_> {
&self.workspace_cache &self.workspace_cache
} }
fn extra_build_dependencies(&self) -> &uv_workspace::pyproject::ExtraBuildDependencies {
&self.extra_build_requires.extra_build_dependencies
}
async fn resolve<'data>( async fn resolve<'data>(
&'data self, &'data self,
requirements: &'data [Requirement], requirements: &'data [Requirement],
@ -457,6 +465,7 @@ impl BuildContext for BuildDispatch<'_> {
self.workspace_cache(), self.workspace_cache(),
config_settings, config_settings,
self.build_isolation, self.build_isolation,
&self.extra_build_requires.extra_build_dependencies,
&build_stack, &build_stack,
build_kind, build_kind,
self.build_extra_env_vars.clone(), self.build_extra_env_vars.clone(),

View File

@ -3,8 +3,8 @@ pub use download::LocalWheel;
pub use error::Error; pub use error::Error;
pub use index::{BuiltWheelIndex, RegistryWheelIndex}; pub use index::{BuiltWheelIndex, RegistryWheelIndex};
pub use metadata::{ pub use metadata::{
ArchiveMetadata, BuildRequires, FlatRequiresDist, LoweredRequirement, LoweringError, Metadata, ArchiveMetadata, BuildRequires, ExtraBuildRequires, FlatRequiresDist, LoweredRequirement,
MetadataError, RequiresDist, SourcedDependencyGroups, LoweringError, Metadata, MetadataError, RequiresDist, SourcedDependencyGroups,
}; };
pub use reporter::Reporter; pub use reporter::Reporter;
pub use source::prune; pub use source::prune;

View File

@ -4,7 +4,8 @@ use std::path::Path;
use uv_configuration::SourceStrategy; use uv_configuration::SourceStrategy;
use uv_distribution_types::{IndexLocations, Requirement}; use uv_distribution_types::{IndexLocations, Requirement};
use uv_normalize::PackageName; use uv_normalize::PackageName;
use uv_workspace::pyproject::ToolUvSources; use uv_pypi_types::VerbatimParsedUrl;
use uv_workspace::pyproject::{ExtraBuildDependencies, ToolUvSources};
use uv_workspace::{ use uv_workspace::{
DiscoveryOptions, MemberDiscovery, ProjectWorkspace, Workspace, WorkspaceCache, DiscoveryOptions, MemberDiscovery, ProjectWorkspace, Workspace, WorkspaceCache,
}; };
@ -203,3 +204,93 @@ 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<Self, MetadataError> {
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<uv_pep508::Requirement<VerbatimParsedUrl>> = 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<Vec<_>, _>>()?;
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,
}
}
}

View File

@ -11,7 +11,7 @@ use uv_pypi_types::{HashDigests, ResolutionMetadata};
use uv_workspace::dependency_groups::DependencyGroupError; use uv_workspace::dependency_groups::DependencyGroupError;
use uv_workspace::{WorkspaceCache, WorkspaceError}; 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::dependency_groups::SourcedDependencyGroups;
pub use crate::metadata::lowering::LoweredRequirement; pub use crate::metadata::lowering::LoweredRequirement;
pub use crate::metadata::lowering::LoweringError; pub use crate::metadata::lowering::LoweringError;

View File

@ -404,6 +404,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<uv_pypi_types::VerbatimParsedUrl>] {
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. /// Build a source distribution from a remote URL.
async fn url<'data>( async fn url<'data>(
&self, &self,
@ -438,12 +452,13 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> {
let cache_shard = cache_shard.shard(revision.id()); let cache_shard = cache_shard.shard(revision.id());
let source_dist_entry = cache_shard.entry(SOURCE); 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 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 cache_shard
} else { } 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. // If the cache contains a compatible wheel, return it.
@ -614,12 +629,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 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 cache_shard
} else { } 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. // Otherwise, we either need to build the metadata.
@ -827,12 +843,13 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> {
let cache_shard = cache_shard.shard(revision.id()); let cache_shard = cache_shard.shard(revision.id());
let source_entry = cache_shard.entry(SOURCE); 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 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 cache_shard
} else { } 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. // If the cache contains a compatible wheel, return it.
@ -989,12 +1006,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 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 cache_shard
} else { } 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. // Otherwise, we need to build a wheel.
@ -1131,12 +1149,13 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> {
// freshness, since entries have to be fresher than the revision itself. // freshness, since entries have to be fresher than the revision itself.
let cache_shard = cache_shard.shard(revision.id()); 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 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 cache_shard
} else { } 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. // If the cache contains a compatible wheel, return it.
@ -1319,12 +1338,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 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 cache_shard
} else { } 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. // Otherwise, we need to build a wheel.
@ -1524,12 +1544,13 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> {
// Acquire the advisory lock. // Acquire the advisory lock.
let _lock = cache_shard.lock().await.map_err(Error::CacheWrite)?; 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 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 cache_shard
} else { } 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. // If the cache contains a compatible wheel, return it.
@ -1827,12 +1848,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 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 cache_shard
} else { } 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. // Otherwise, we need to build a wheel.

View File

@ -114,6 +114,24 @@ impl Operator {
pub fn is_star(self) -> bool { pub fn is_star(self) -> bool {
matches!(self, Self::EqualStar | Self::NotEqualStar) 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 { impl FromStr for Operator {
@ -150,21 +168,7 @@ impl FromStr for Operator {
impl std::fmt::Display for Operator { impl std::fmt::Display for Operator {
/// Note the `EqualStar` is also `==`. /// Note the `EqualStar` is also `==`.
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let operator = match self { let operator = self.as_str();
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 => ">=",
};
write!(f, "{operator}") write!(f, "{operator}")
} }
} }

View File

@ -48,6 +48,11 @@ impl VersionSpecifiers {
Self(Box::new([])) Self(Box::new([]))
} }
/// The number of specifiers.
pub fn len(&self) -> usize {
self.0.len()
}
/// Whether all specifiers match the given version. /// Whether all specifiers match the given version.
pub fn contains(&self, version: &Version) -> bool { pub fn contains(&self, version: &Version) -> bool {
self.iter().all(|specifier| specifier.contains(version)) self.iter().all(|specifier| specifier.contains(version))

View File

@ -19,6 +19,7 @@ doctest = false
workspace = true workspace = true
[dependencies] [dependencies]
uv-cache-key = { workspace = true }
uv-fs = { workspace = true } uv-fs = { workspace = true }
uv-normalize = { workspace = true } uv-normalize = { workspace = true }
uv-pep440 = { workspace = true } uv-pep440 = { workspace = true }

View File

@ -26,6 +26,7 @@ use std::str::FromStr;
use serde::{Deserialize, Deserializer, Serialize, Serializer, de}; use serde::{Deserialize, Deserializer, Serialize, Serializer, de};
use thiserror::Error; use thiserror::Error;
use url::Url; use url::Url;
use uv_cache_key::{CacheKey, CacheKeyHasher};
use cursor::Cursor; use cursor::Cursor;
pub use marker::{ pub use marker::{
@ -251,6 +252,52 @@ impl<T: Pep508Url> Serialize for Requirement<T> {
} }
} }
impl<T: Pep508Url> CacheKey for Requirement<T>
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<T: Pep508Url> Requirement<T> { impl<T: Pep508Url> Requirement<T> {
/// Returns whether the markers apply for the given environment /// Returns whether the markers apply for the given environment
pub fn evaluate_markers(&self, env: &MarkerEnvironment, extras: &[ExtraName]) -> bool { pub fn evaluate_markers(&self, env: &MarkerEnvironment, extras: &[ExtraName]) -> bool {

View File

@ -381,6 +381,8 @@ pub struct ToolUv {
pub override_dependencies: Option<Vec<uv_pep508::Requirement<VerbatimParsedUrl>>>, pub override_dependencies: Option<Vec<uv_pep508::Requirement<VerbatimParsedUrl>>>,
pub constraint_dependencies: Option<Vec<uv_pep508::Requirement<VerbatimParsedUrl>>>, pub constraint_dependencies: Option<Vec<uv_pep508::Requirement<VerbatimParsedUrl>>>,
pub build_constraint_dependencies: Option<Vec<uv_pep508::Requirement<VerbatimParsedUrl>>>, pub build_constraint_dependencies: Option<Vec<uv_pep508::Requirement<VerbatimParsedUrl>>>,
pub extra_build_dependencies:
Option<BTreeMap<PackageName, Vec<uv_pep508::Requirement<VerbatimParsedUrl>>>>,
pub sources: Option<BTreeMap<PackageName, Sources>>, pub sources: Option<BTreeMap<PackageName, Sources>>,
} }

View File

@ -1,5 +1,5 @@
use std::num::NonZeroUsize;
use std::path::PathBuf; use std::path::PathBuf;
use std::{collections::BTreeMap, num::NonZeroUsize};
use url::Url; use url::Url;
@ -121,6 +121,21 @@ impl<T> Combine for Option<Vec<T>> {
} }
} }
impl<K: Ord, T> Combine for Option<BTreeMap<K, Vec<T>>> {
/// Combine two maps of vecs by combining their vecs
fn combine(self, other: Option<BTreeMap<K, Vec<T>>>) -> Option<BTreeMap<K, Vec<T>>> {
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<ConfigSettings> { impl Combine for Option<ConfigSettings> {
/// Combine two maps by merging the map in `self` with the map in `other`, if they're both /// Combine two maps by merging the map in `self` with the map in `other`, if they're both
/// `Some`. /// `Some`.

View File

@ -317,6 +317,7 @@ fn warn_uv_toml_masked_fields(options: &Options) {
config_settings_package, config_settings_package,
no_build_isolation, no_build_isolation,
no_build_isolation_package, no_build_isolation_package,
extra_build_dependencies,
exclude_newer, exclude_newer,
link_mode, link_mode,
compile_bytecode, compile_bytecode,
@ -445,6 +446,9 @@ fn warn_uv_toml_masked_fields(options: &Options) {
if no_build_isolation_package.is_some() { if no_build_isolation_package.is_some() {
masked_fields.push("no-build-isolation-package"); masked_fields.push("no-build-isolation-package");
} }
if extra_build_dependencies.is_some() {
masked_fields.push("extra-build-dependencies");
}
if exclude_newer.is_some() { if exclude_newer.is_some() {
masked_fields.push("exclude-newer"); masked_fields.push("exclude-newer");
} }

View File

@ -21,7 +21,7 @@ use uv_redacted::DisplaySafeUrl;
use uv_resolver::{AnnotationStyle, ExcludeNewer, ForkStrategy, PrereleaseMode, ResolutionMode}; use uv_resolver::{AnnotationStyle, ExcludeNewer, ForkStrategy, PrereleaseMode, ResolutionMode};
use uv_static::EnvVars; use uv_static::EnvVars;
use uv_torch::TorchMode; 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. /// A `pyproject.toml` with an (optional) `[tool.uv]` section.
#[allow(dead_code)] #[allow(dead_code)]
@ -373,6 +373,7 @@ pub struct ResolverOptions {
pub no_binary_package: Option<Vec<PackageName>>, pub no_binary_package: Option<Vec<PackageName>>,
pub no_build_isolation: Option<bool>, pub no_build_isolation: Option<bool>,
pub no_build_isolation_package: Option<Vec<PackageName>>, pub no_build_isolation_package: Option<Vec<PackageName>>,
pub extra_build_dependencies: Option<ExtraBuildDependencies>,
pub no_sources: Option<bool>, pub no_sources: Option<bool>,
pub build_dependency_strategy: Option<BuildDependencyStrategy>, pub build_dependency_strategy: Option<BuildDependencyStrategy>,
} }
@ -643,6 +644,20 @@ pub struct ResolverInstallerOptions {
"# "#
)] )]
pub no_build_isolation_package: Option<Vec<PackageName>>, pub no_build_isolation_package: Option<Vec<PackageName>>,
/// Additional build dependencies for packages.
///
/// 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",
example = r#"
[extra-build-dependencies]
pytest = ["setuptools"]
"#
)]
pub extra_build_dependencies: Option<ExtraBuildDependencies>,
/// Limit candidate packages to those that were uploaded prior to a given point in time. /// 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., /// Accepts a superset of [RFC 3339](https://www.rfc-editor.org/rfc/rfc3339.html) (e.g.,
@ -1139,6 +1154,20 @@ pub struct PipOptions {
"# "#
)] )]
pub no_build_isolation_package: Option<Vec<PackageName>>, pub no_build_isolation_package: Option<Vec<PackageName>>,
/// Additional build dependencies for packages.
///
/// 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",
example = r#"
[extra-build-dependencies]
pytest = ["setuptools"]
"#
)]
pub extra_build_dependencies: Option<ExtraBuildDependencies>,
/// Validate the Python environment, to detect packages with missing dependencies and other /// Validate the Python environment, to detect packages with missing dependencies and other
/// issues. /// issues.
#[option( #[option(
@ -1704,6 +1733,7 @@ impl From<ResolverInstallerOptions> for ResolverOptions {
no_binary_package: value.no_binary_package, no_binary_package: value.no_binary_package,
no_build_isolation: value.no_build_isolation, no_build_isolation: value.no_build_isolation,
no_build_isolation_package: value.no_build_isolation_package, no_build_isolation_package: value.no_build_isolation_package,
extra_build_dependencies: value.extra_build_dependencies,
no_sources: value.no_sources, no_sources: value.no_sources,
build_dependency_strategy: value.build_dependency_strategy, build_dependency_strategy: value.build_dependency_strategy,
} }
@ -1761,6 +1791,7 @@ pub struct ToolOptions {
pub config_settings_package: Option<PackageConfigSettings>, pub config_settings_package: Option<PackageConfigSettings>,
pub no_build_isolation: Option<bool>, pub no_build_isolation: Option<bool>,
pub no_build_isolation_package: Option<Vec<PackageName>>, pub no_build_isolation_package: Option<Vec<PackageName>>,
pub extra_build_dependencies: Option<ExtraBuildDependencies>,
pub exclude_newer: Option<ExcludeNewer>, pub exclude_newer: Option<ExcludeNewer>,
pub link_mode: Option<LinkMode>, pub link_mode: Option<LinkMode>,
pub compile_bytecode: Option<bool>, pub compile_bytecode: Option<bool>,
@ -1789,6 +1820,7 @@ impl From<ResolverInstallerOptions> for ToolOptions {
config_settings_package: value.config_settings_package, config_settings_package: value.config_settings_package,
no_build_isolation: value.no_build_isolation, no_build_isolation: value.no_build_isolation,
no_build_isolation_package: value.no_build_isolation_package, no_build_isolation_package: value.no_build_isolation_package,
extra_build_dependencies: value.extra_build_dependencies,
exclude_newer: value.exclude_newer, exclude_newer: value.exclude_newer,
link_mode: value.link_mode, link_mode: value.link_mode,
compile_bytecode: value.compile_bytecode, compile_bytecode: value.compile_bytecode,
@ -1819,6 +1851,7 @@ impl From<ToolOptions> for ResolverInstallerOptions {
config_settings_package: value.config_settings_package, config_settings_package: value.config_settings_package,
no_build_isolation: value.no_build_isolation, no_build_isolation: value.no_build_isolation,
no_build_isolation_package: value.no_build_isolation_package, no_build_isolation_package: value.no_build_isolation_package,
extra_build_dependencies: value.extra_build_dependencies,
exclude_newer: value.exclude_newer, exclude_newer: value.exclude_newer,
link_mode: value.link_mode, link_mode: value.link_mode,
compile_bytecode: value.compile_bytecode, compile_bytecode: value.compile_bytecode,
@ -1873,6 +1906,7 @@ pub struct OptionsWire {
config_settings_package: Option<PackageConfigSettings>, config_settings_package: Option<PackageConfigSettings>,
no_build_isolation: Option<bool>, no_build_isolation: Option<bool>,
no_build_isolation_package: Option<Vec<PackageName>>, no_build_isolation_package: Option<Vec<PackageName>>,
extra_build_dependencies: Option<ExtraBuildDependencies>,
exclude_newer: Option<ExcludeNewer>, exclude_newer: Option<ExcludeNewer>,
link_mode: Option<LinkMode>, link_mode: Option<LinkMode>,
compile_bytecode: Option<bool>, compile_bytecode: Option<bool>,
@ -1992,6 +2026,7 @@ impl From<OptionsWire> for Options {
sources, sources,
default_groups, default_groups,
dependency_groups, dependency_groups,
extra_build_dependencies,
dev_dependencies, dev_dependencies,
managed, managed,
package, package,
@ -2033,6 +2068,7 @@ impl From<OptionsWire> for Options {
config_settings_package, config_settings_package,
no_build_isolation, no_build_isolation,
no_build_isolation_package, no_build_isolation_package,
extra_build_dependencies,
exclude_newer, exclude_newer,
link_mode, link_mode,
compile_bytecode, compile_bytecode,

View File

@ -101,6 +101,9 @@ pub trait BuildContext {
/// Workspace discovery caching. /// Workspace discovery caching.
fn workspace_cache(&self) -> &WorkspaceCache; 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. /// Resolve the given requirements into a ready-to-install set of package versions.
fn resolve<'a>( fn resolve<'a>(
&'a self, &'a self,

View File

@ -378,6 +378,21 @@ pub struct ToolUv {
)] )]
pub dependency_groups: Option<ToolUvDependencyGroups>, pub dependency_groups: Option<ToolUvDependencyGroups>,
/// Additional build dependencies for packages.
///
/// 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",
example = r#"
[tool.uv.extra-build-dependencies]
pytest = ["pip"]
"#
)]
pub extra_build_dependencies: Option<ToolUvExtraBuildDependencies>,
/// The project's development dependencies. /// The project's development dependencies.
/// ///
/// Development dependencies will be installed by default in `uv run` and `uv sync`, but will /// Development dependencies will be installed by default in `uv run` and `uv sync`, but will
@ -749,6 +764,70 @@ pub struct DependencyGroupSettings {
pub requires_python: Option<VersionSpecifiers>, pub requires_python: Option<VersionSpecifiers>,
} }
pub type ExtraBuildDependencies =
BTreeMap<PackageName, Vec<uv_pep508::Requirement<VerbatimParsedUrl>>>;
#[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<D>(deserializer: D) -> Result<Self, D::Error>
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<M>(self, mut access: M) -> Result<Self::Value, M::Error>
where
M: serde::de::MapAccess<'de>,
{
let mut groups = BTreeMap::new();
while let Some((key, value)) = access
.next_entry::<PackageName, Vec<uv_pep508::Requirement<VerbatimParsedUrl>>>()?
{
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)] #[derive(Deserialize, OptionsMetadata, Default, Debug, Clone, PartialEq, Eq)]
#[cfg_attr(test, derive(Serialize))] #[cfg_attr(test, derive(Serialize))]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]

View File

@ -1970,6 +1970,7 @@ mod tests {
"package": null, "package": null,
"default-groups": null, "default-groups": null,
"dependency-groups": null, "dependency-groups": null,
"extra-build-dependencies": null,
"dev-dependencies": null, "dev-dependencies": null,
"override-dependencies": null, "override-dependencies": null,
"constraint-dependencies": null, "constraint-dependencies": null,
@ -2070,6 +2071,7 @@ mod tests {
"package": null, "package": null,
"default-groups": null, "default-groups": null,
"dependency-groups": null, "dependency-groups": null,
"extra-build-dependencies": null,
"dev-dependencies": null, "dev-dependencies": null,
"override-dependencies": null, "override-dependencies": null,
"constraint-dependencies": null, "constraint-dependencies": null,
@ -2283,6 +2285,7 @@ mod tests {
"package": null, "package": null,
"default-groups": null, "default-groups": null,
"dependency-groups": null, "dependency-groups": null,
"extra-build-dependencies": null,
"dev-dependencies": null, "dev-dependencies": null,
"override-dependencies": null, "override-dependencies": null,
"constraint-dependencies": null, "constraint-dependencies": null,
@ -2392,6 +2395,7 @@ mod tests {
"package": null, "package": null,
"default-groups": null, "default-groups": null,
"dependency-groups": null, "dependency-groups": null,
"extra-build-dependencies": null,
"dev-dependencies": null, "dev-dependencies": null,
"override-dependencies": null, "override-dependencies": null,
"constraint-dependencies": null, "constraint-dependencies": null,
@ -2514,6 +2518,7 @@ mod tests {
"package": null, "package": null,
"default-groups": null, "default-groups": null,
"dependency-groups": null, "dependency-groups": null,
"extra-build-dependencies": null,
"dev-dependencies": null, "dev-dependencies": null,
"override-dependencies": null, "override-dependencies": null,
"constraint-dependencies": null, "constraint-dependencies": null,
@ -2610,6 +2615,7 @@ mod tests {
"package": null, "package": null,
"default-groups": null, "default-groups": null,
"dependency-groups": null, "dependency-groups": null,
"extra-build-dependencies": null,
"dev-dependencies": null, "dev-dependencies": null,
"override-dependencies": null, "override-dependencies": null,
"constraint-dependencies": null, "constraint-dependencies": null,

View File

@ -38,6 +38,7 @@ use uv_requirements::RequirementsSource;
use uv_resolver::{ExcludeNewer, FlatIndex, Preferences}; use uv_resolver::{ExcludeNewer, FlatIndex, Preferences};
use uv_settings::PythonInstallMirrors; use uv_settings::PythonInstallMirrors;
use uv_types::{AnyErrorBuild, BuildContext, BuildIsolation, BuildStack, HashStrategy}; use uv_types::{AnyErrorBuild, BuildContext, BuildIsolation, BuildStack, HashStrategy};
use uv_workspace::pyproject::ExtraBuildDependencies;
use uv_workspace::{DiscoveryOptions, Workspace, WorkspaceCache, WorkspaceError}; use uv_workspace::{DiscoveryOptions, Workspace, WorkspaceCache, WorkspaceError};
use crate::commands::ExitStatus; use crate::commands::ExitStatus;
@ -200,6 +201,7 @@ async fn build_impl(
config_settings_package, config_settings_package,
no_build_isolation, no_build_isolation,
no_build_isolation_package, no_build_isolation_package,
extra_build_dependencies,
exclude_newer, exclude_newer,
link_mode, link_mode,
upgrade: _, upgrade: _,
@ -347,6 +349,7 @@ async fn build_impl(
build_constraints, build_constraints,
*no_build_isolation, *no_build_isolation,
no_build_isolation_package, no_build_isolation_package,
extra_build_dependencies,
*index_strategy, *index_strategy,
*keyring_provider, *keyring_provider,
*exclude_newer, *exclude_newer,
@ -425,6 +428,7 @@ async fn build_package(
build_constraints: &[RequirementsSource], build_constraints: &[RequirementsSource],
no_build_isolation: bool, no_build_isolation: bool,
no_build_isolation_package: &[PackageName], no_build_isolation_package: &[PackageName],
extra_build_dependencies: &ExtraBuildDependencies,
index_strategy: IndexStrategy, index_strategy: IndexStrategy,
keyring_provider: KeyringProviderType, keyring_provider: KeyringProviderType,
exclude_newer: Option<ExcludeNewer>, exclude_newer: Option<ExcludeNewer>,
@ -561,6 +565,8 @@ async fn build_package(
let workspace_cache = WorkspaceCache::default(); let workspace_cache = WorkspaceCache::default();
// Create a build dispatch. // Create a build dispatch.
let extra_build_requires =
uv_distribution::ExtraBuildRequires::from_lowered(extra_build_dependencies.clone());
let build_dispatch = BuildDispatch::new( let build_dispatch = BuildDispatch::new(
&client, &client,
cache, cache,
@ -574,6 +580,7 @@ async fn build_package(
config_setting, config_setting,
config_settings_package, config_settings_package,
build_isolation, build_isolation,
&extra_build_requires,
link_mode, link_mode,
build_options, build_options,
&hasher, &hasher,

View File

@ -14,8 +14,8 @@ use uv_cache::Cache;
use uv_client::{BaseClientBuilder, FlatIndexClient, RegistryClientBuilder}; use uv_client::{BaseClientBuilder, FlatIndexClient, RegistryClientBuilder};
use uv_configuration::{ use uv_configuration::{
BuildOptions, Concurrency, ConfigSettings, Constraints, ExportFormat, ExtrasSpecification, BuildOptions, Concurrency, ConfigSettings, Constraints, ExportFormat, ExtrasSpecification,
IndexStrategy, NoBinary, NoBuild, PackageConfigSettings, Preview, Reinstall, SourceStrategy, IndexStrategy, NoBinary, NoBuild, PackageConfigSettings, Preview, PreviewFeatures, Reinstall,
Upgrade, SourceStrategy, Upgrade,
}; };
use uv_configuration::{KeyringProviderType, TargetTriple}; use uv_configuration::{KeyringProviderType, TargetTriple};
use uv_dispatch::{BuildDispatch, SharedState}; use uv_dispatch::{BuildDispatch, SharedState};
@ -44,8 +44,9 @@ use uv_resolver::{
}; };
use uv_torch::{TorchMode, TorchStrategy}; use uv_torch::{TorchMode, TorchStrategy};
use uv_types::{BuildIsolation, EmptyInstalledPackages, HashStrategy}; 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::WorkspaceCache;
use uv_workspace::pyproject::ExtraBuildDependencies;
use crate::commands::pip::loggers::DefaultResolveLogger; use crate::commands::pip::loggers::DefaultResolveLogger;
use crate::commands::pip::{operations, resolution_environment}; use crate::commands::pip::{operations, resolution_environment};
@ -95,6 +96,7 @@ pub(crate) async fn pip_compile(
config_settings_package: PackageConfigSettings, config_settings_package: PackageConfigSettings,
no_build_isolation: bool, no_build_isolation: bool,
no_build_isolation_package: Vec<PackageName>, no_build_isolation_package: Vec<PackageName>,
extra_build_dependencies: &ExtraBuildDependencies,
build_options: BuildOptions, build_options: BuildOptions,
mut python_version: Option<PythonVersion>, mut python_version: Option<PythonVersion>,
python_platform: Option<TargetTriple>, python_platform: Option<TargetTriple>,
@ -112,6 +114,15 @@ pub(crate) async fn pip_compile(
printer: Printer, printer: Printer,
preview: Preview, preview: Preview,
) -> Result<ExitStatus> { ) -> Result<ExitStatus> {
if !preview.is_enabled(PreviewFeatures::EXTRA_BUILD_DEPENDENCIES)
&& !extra_build_dependencies.is_empty()
{
warn_user_once!(
"The `extra-build-dependencies` option is experimental and may change without warning. Pass `--preview-features {}` to disable this warning.",
PreviewFeatures::EXTRA_BUILD_DEPENDENCIES
);
}
// If the user provides a `pyproject.toml` or other TOML file as the output file, raise an // If the user provides a `pyproject.toml` or other TOML file as the output file, raise an
// error. // error.
if output_file if output_file
@ -469,6 +480,8 @@ pub(crate) async fn pip_compile(
.map(|constraint| constraint.requirement.clone()), .map(|constraint| constraint.requirement.clone()),
); );
let extra_build_requires =
uv_distribution::ExtraBuildRequires::from_lowered(extra_build_dependencies.clone());
let build_dispatch = BuildDispatch::new( let build_dispatch = BuildDispatch::new(
&client, &client,
&cache, &cache,
@ -482,6 +495,7 @@ pub(crate) async fn pip_compile(
&config_settings, &config_settings,
&config_settings_package, &config_settings_package,
build_isolation, build_isolation,
&extra_build_requires,
link_mode, link_mode,
&build_options, &build_options,
&build_hashes, &build_hashes,

View File

@ -36,8 +36,9 @@ use uv_resolver::{
}; };
use uv_torch::{TorchMode, TorchStrategy}; use uv_torch::{TorchMode, TorchStrategy};
use uv_types::{BuildIsolation, HashStrategy}; 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::WorkspaceCache;
use uv_workspace::pyproject::ExtraBuildDependencies;
use crate::commands::pip::loggers::{DefaultInstallLogger, DefaultResolveLogger, InstallLogger}; use crate::commands::pip::loggers::{DefaultInstallLogger, DefaultResolveLogger, InstallLogger};
use crate::commands::pip::operations::Modifications; use crate::commands::pip::operations::Modifications;
@ -78,6 +79,7 @@ pub(crate) async fn pip_install(
config_settings_package: &PackageConfigSettings, config_settings_package: &PackageConfigSettings,
no_build_isolation: bool, no_build_isolation: bool,
no_build_isolation_package: Vec<PackageName>, no_build_isolation_package: Vec<PackageName>,
extra_build_dependencies: &ExtraBuildDependencies,
build_options: BuildOptions, build_options: BuildOptions,
modifications: Modifications, modifications: Modifications,
python_version: Option<PythonVersion>, python_version: Option<PythonVersion>,
@ -99,6 +101,15 @@ pub(crate) async fn pip_install(
) -> anyhow::Result<ExitStatus> { ) -> anyhow::Result<ExitStatus> {
let start = std::time::Instant::now(); let start = std::time::Instant::now();
if !preview.is_enabled(PreviewFeatures::EXTRA_BUILD_DEPENDENCIES)
&& !extra_build_dependencies.is_empty()
{
warn_user_once!(
"The `extra-build-dependencies` option is experimental and may change without warning. Pass `--preview-features {}` to disable this warning.",
PreviewFeatures::EXTRA_BUILD_DEPENDENCIES
);
}
let client_builder = BaseClientBuilder::new() let client_builder = BaseClientBuilder::new()
.retries_from_env()? .retries_from_env()?
.connectivity(network_settings.connectivity) .connectivity(network_settings.connectivity)
@ -413,6 +424,8 @@ pub(crate) async fn pip_install(
let state = SharedState::default(); let state = SharedState::default();
// Create a build dispatch. // Create a build dispatch.
let extra_build_requires =
uv_distribution::ExtraBuildRequires::from_lowered(extra_build_dependencies.clone());
let build_dispatch = BuildDispatch::new( let build_dispatch = BuildDispatch::new(
&client, &client,
&cache, &cache,
@ -426,6 +439,7 @@ pub(crate) async fn pip_install(
config_settings, config_settings,
config_settings_package, config_settings_package,
build_isolation, build_isolation,
&extra_build_requires,
link_mode, link_mode,
&build_options, &build_options,
&build_hasher, &build_hasher,

View File

@ -32,8 +32,9 @@ use uv_resolver::{
}; };
use uv_torch::{TorchMode, TorchStrategy}; use uv_torch::{TorchMode, TorchStrategy};
use uv_types::{BuildIsolation, HashStrategy}; 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::WorkspaceCache;
use uv_workspace::pyproject::ExtraBuildDependencies;
use crate::commands::pip::loggers::{DefaultInstallLogger, DefaultResolveLogger}; use crate::commands::pip::loggers::{DefaultInstallLogger, DefaultResolveLogger};
use crate::commands::pip::operations::Modifications; use crate::commands::pip::operations::Modifications;
@ -67,6 +68,7 @@ pub(crate) async fn pip_sync(
config_settings_package: &PackageConfigSettings, config_settings_package: &PackageConfigSettings,
no_build_isolation: bool, no_build_isolation: bool,
no_build_isolation_package: Vec<PackageName>, no_build_isolation_package: Vec<PackageName>,
extra_build_dependencies: &ExtraBuildDependencies,
build_options: BuildOptions, build_options: BuildOptions,
python_version: Option<PythonVersion>, python_version: Option<PythonVersion>,
python_platform: Option<TargetTriple>, python_platform: Option<TargetTriple>,
@ -85,6 +87,15 @@ pub(crate) async fn pip_sync(
printer: Printer, printer: Printer,
preview: Preview, preview: Preview,
) -> Result<ExitStatus> { ) -> Result<ExitStatus> {
if !preview.is_enabled(PreviewFeatures::EXTRA_BUILD_DEPENDENCIES)
&& !extra_build_dependencies.is_empty()
{
warn_user_once!(
"The `extra-build-dependencies` option is experimental and may change without warning. Pass `--preview-features {}` to disable this warning.",
PreviewFeatures::EXTRA_BUILD_DEPENDENCIES
);
}
let client_builder = BaseClientBuilder::new() let client_builder = BaseClientBuilder::new()
.retries_from_env()? .retries_from_env()?
.connectivity(network_settings.connectivity) .connectivity(network_settings.connectivity)
@ -348,6 +359,8 @@ pub(crate) async fn pip_sync(
let state = SharedState::default(); let state = SharedState::default();
// Create a build dispatch. // Create a build dispatch.
let extra_build_requires =
uv_distribution::ExtraBuildRequires::from_lowered(extra_build_dependencies.clone());
let build_dispatch = BuildDispatch::new( let build_dispatch = BuildDispatch::new(
&client, &client,
&cache, &cache,
@ -361,6 +374,7 @@ pub(crate) async fn pip_sync(
config_settings, config_settings,
config_settings_package, config_settings_package,
build_isolation, build_isolation,
&extra_build_requires,
link_mode, link_mode,
&build_options, &build_options,
&build_hasher, &build_hasher,

View File

@ -104,6 +104,15 @@ pub(crate) async fn add(
); );
} }
if !preview.is_enabled(PreviewFeatures::EXTRA_BUILD_DEPENDENCIES)
&& !settings.resolver.extra_build_dependencies.is_empty()
{
warn_user_once!(
"The `extra-build-dependencies` option is experimental and may change without warning. Pass `--preview-features {}` to disable this warning.",
PreviewFeatures::EXTRA_BUILD_DEPENDENCIES
);
}
for source in &requirements { for source in &requirements {
match source { match source {
RequirementsSource::PyprojectToml(_) => { RequirementsSource::PyprojectToml(_) => {
@ -457,6 +466,18 @@ pub(crate) async fn add(
}; };
// Create a build dispatch. // 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( let build_dispatch = BuildDispatch::new(
&client, &client,
cache, cache,
@ -470,6 +491,7 @@ pub(crate) async fn add(
&settings.resolver.config_setting, &settings.resolver.config_setting,
&settings.resolver.config_settings_package, &settings.resolver.config_settings_package,
build_isolation, build_isolation,
&extra_build_requires,
settings.resolver.link_mode, settings.resolver.link_mode,
&settings.resolver.build_options, &settings.resolver.build_options,
&build_hasher, &build_hasher,
@ -1312,6 +1334,7 @@ impl PythonTarget {
/// Represents the destination where dependencies are added, either to a project or a script. /// Represents the destination where dependencies are added, either to a project or a script.
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
#[allow(clippy::large_enum_variant)]
pub(super) enum AddTarget { pub(super) enum AddTarget {
/// A PEP 723 script, with inline metadata. /// A PEP 723 script, with inline metadata.
Script(Pep723Script, Box<Interpreter>), Script(Pep723Script, Box<Interpreter>),
@ -1420,6 +1443,7 @@ impl AddTarget {
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
#[allow(clippy::large_enum_variant)]
enum AddTargetSnapshot { enum AddTargetSnapshot {
Script(Pep723Script, Option<Vec<u8>>), Script(Pep723Script, Option<Vec<u8>>),
Project(VirtualProject, Option<Vec<u8>>), Project(VirtualProject, Option<Vec<u8>>),

View File

@ -32,6 +32,7 @@ use crate::printer::Printer;
use crate::settings::{NetworkSettings, ResolverSettings}; use crate::settings::{NetworkSettings, ResolverSettings};
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
#[allow(clippy::large_enum_variant)]
enum ExportTarget { enum ExportTarget {
/// A PEP 723 script, with inline metadata. /// A PEP 723 script, with inline metadata.
Script(Pep723Script), Script(Pep723Script),

View File

@ -42,7 +42,7 @@ use crate::commands::pip::loggers::{DefaultResolveLogger, ResolveLogger, Summary
use crate::commands::project::lock_target::LockTarget; use crate::commands::project::lock_target::LockTarget;
use crate::commands::project::{ use crate::commands::project::{
ProjectError, ProjectInterpreter, ScriptInterpreter, UniversalState, ProjectError, ProjectInterpreter, ScriptInterpreter, UniversalState,
init_script_python_requirement, init_script_python_requirement, script_specification,
}; };
use crate::commands::reporters::{PythonDownloadReporter, ResolverReporter}; use crate::commands::reporters::{PythonDownloadReporter, ResolverReporter};
use crate::commands::{ExitStatus, ScriptPath, diagnostics, pip}; use crate::commands::{ExitStatus, ScriptPath, diagnostics, pip};
@ -435,6 +435,7 @@ async fn do_lock(
config_settings_package, config_settings_package,
no_build_isolation, no_build_isolation,
no_build_isolation_package, no_build_isolation_package,
extra_build_dependencies,
exclude_newer, exclude_newer,
link_mode, link_mode,
upgrade, upgrade,
@ -453,6 +454,15 @@ async fn do_lock(
); );
} }
if !preview.is_enabled(PreviewFeatures::EXTRA_BUILD_DEPENDENCIES)
&& !extra_build_dependencies.is_empty()
{
warn_user_once!(
"The `extra-build-dependencies` option is experimental and may change without warning. Pass `--preview-features {}` to disable this warning.",
PreviewFeatures::EXTRA_BUILD_DEPENDENCIES
);
}
// Collect the requirements, etc. // Collect the requirements, etc.
let members = target.members(); let members = target.members();
let packages = target.packages(); let packages = target.packages();
@ -695,6 +705,24 @@ async fn do_lock(
}; };
// Create a build dispatch. // 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(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( let build_dispatch = BuildDispatch::new(
&client, &client,
cache, cache,
@ -708,6 +736,7 @@ async fn do_lock(
config_setting, config_setting,
config_settings_package, config_settings_package,
build_isolation, build_isolation,
&extra_build_requires,
*link_mode, *link_mode,
build_options, build_options,
&build_hasher, &build_hasher,

View File

@ -46,6 +46,7 @@ use uv_types::{BuildIsolation, EmptyInstalledPackages, HashStrategy};
use uv_virtualenv::remove_virtualenv; use uv_virtualenv::remove_virtualenv;
use uv_warnings::{warn_user, warn_user_once}; use uv_warnings::{warn_user, warn_user_once};
use uv_workspace::dependency_groups::DependencyGroupError; use uv_workspace::dependency_groups::DependencyGroupError;
use uv_workspace::pyproject::ExtraBuildDependencies;
use uv_workspace::pyproject::PyProjectToml; use uv_workspace::pyproject::PyProjectToml;
use uv_workspace::{RequiresPythonSources, Workspace, WorkspaceCache}; use uv_workspace::{RequiresPythonSources, Workspace, WorkspaceCache};
@ -1692,6 +1693,7 @@ pub(crate) async fn resolve_names(
link_mode, link_mode,
no_build_isolation, no_build_isolation,
no_build_isolation_package, no_build_isolation_package,
extra_build_dependencies,
prerelease: _, prerelease: _,
resolution: _, resolution: _,
sources, sources,
@ -1741,6 +1743,8 @@ pub(crate) async fn resolve_names(
let build_hasher = HashStrategy::default(); let build_hasher = HashStrategy::default();
// Create a build dispatch. // Create a build dispatch.
let extra_build_requires =
uv_distribution::ExtraBuildRequires::from_lowered(extra_build_dependencies.clone());
let build_dispatch = BuildDispatch::new( let build_dispatch = BuildDispatch::new(
&client, &client,
cache, cache,
@ -1754,6 +1758,7 @@ pub(crate) async fn resolve_names(
config_setting, config_setting,
config_settings_package, config_settings_package,
build_isolation, build_isolation,
&extra_build_requires,
*link_mode, *link_mode,
build_options, build_options,
&build_hasher, &build_hasher,
@ -1847,6 +1852,7 @@ pub(crate) async fn resolve_environment(
config_settings_package, config_settings_package,
no_build_isolation, no_build_isolation,
no_build_isolation_package, no_build_isolation_package,
extra_build_dependencies,
exclude_newer, exclude_newer,
link_mode, link_mode,
upgrade: _, upgrade: _,
@ -1951,6 +1957,8 @@ pub(crate) async fn resolve_environment(
let workspace_cache = WorkspaceCache::default(); let workspace_cache = WorkspaceCache::default();
// Create a build dispatch. // Create a build dispatch.
let extra_build_requires =
uv_distribution::ExtraBuildRequires::from_lowered(extra_build_dependencies.clone());
let resolve_dispatch = BuildDispatch::new( let resolve_dispatch = BuildDispatch::new(
&client, &client,
cache, cache,
@ -1964,6 +1972,7 @@ pub(crate) async fn resolve_environment(
config_setting, config_setting,
config_settings_package, config_settings_package,
build_isolation, build_isolation,
&extra_build_requires,
*link_mode, *link_mode,
build_options, build_options,
&build_hasher, &build_hasher,
@ -2032,6 +2041,7 @@ pub(crate) async fn sync_environment(
config_settings_package, config_settings_package,
no_build_isolation, no_build_isolation,
no_build_isolation_package, no_build_isolation_package,
extra_build_dependencies,
exclude_newer, exclude_newer,
link_mode, link_mode,
compile_bytecode, compile_bytecode,
@ -2091,6 +2101,8 @@ pub(crate) async fn sync_environment(
}; };
// Create a build dispatch. // Create a build dispatch.
let extra_build_requires =
uv_distribution::ExtraBuildRequires::from_lowered(extra_build_dependencies.clone());
let build_dispatch = BuildDispatch::new( let build_dispatch = BuildDispatch::new(
&client, &client,
cache, cache,
@ -2104,6 +2116,7 @@ pub(crate) async fn sync_environment(
config_setting, config_setting,
config_settings_package, config_settings_package,
build_isolation, build_isolation,
&extra_build_requires,
link_mode, link_mode,
build_options, build_options,
&build_hasher, &build_hasher,
@ -2148,6 +2161,15 @@ pub(crate) async fn sync_environment(
Ok(venv) 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. /// The result of updating a [`PythonEnvironment`] to satisfy a set of [`RequirementsSource`]s.
#[derive(Debug)] #[derive(Debug)]
pub(crate) struct EnvironmentUpdate { pub(crate) struct EnvironmentUpdate {
@ -2170,6 +2192,7 @@ pub(crate) async fn update_environment(
spec: RequirementsSpecification, spec: RequirementsSpecification,
modifications: Modifications, modifications: Modifications,
build_constraints: Constraints, build_constraints: Constraints,
extra_build_requires: uv_distribution::ExtraBuildRequires,
settings: &ResolverInstallerSettings, settings: &ResolverInstallerSettings,
network_settings: &NetworkSettings, network_settings: &NetworkSettings,
state: &SharedState, state: &SharedState,
@ -2200,6 +2223,7 @@ pub(crate) async fn update_environment(
link_mode, link_mode,
no_build_isolation, no_build_isolation,
no_build_isolation_package, no_build_isolation_package,
extra_build_dependencies: _,
prerelease, prerelease,
resolution, resolution,
sources, sources,
@ -2330,6 +2354,7 @@ pub(crate) async fn update_environment(
config_setting, config_setting,
config_settings_package, config_settings_package,
build_isolation, build_isolation,
&extra_build_requires,
*link_mode, *link_mode,
build_options, build_options,
&build_hasher, &build_hasher,
@ -2535,12 +2560,12 @@ pub(crate) fn detect_conflicts(
Ok(()) Ok(())
} }
/// Determine the [`RequirementsSpecification`] for a script. /// Determine the [`ScriptSpecification`] for a script.
#[allow(clippy::result_large_err)] #[allow(clippy::result_large_err)]
pub(crate) fn script_specification( pub(crate) fn script_specification(
script: Pep723ItemRef<'_>, script: Pep723ItemRef<'_>,
settings: &ResolverSettings, settings: &ResolverSettings,
) -> Result<Option<RequirementsSpecification>, ProjectError> { ) -> Result<Option<ScriptSpecification>, ProjectError> {
let Some(dependencies) = script.metadata().dependencies.as_ref() else { let Some(dependencies) = script.metadata().dependencies.as_ref() else {
return Ok(None); return Ok(None);
}; };
@ -2635,11 +2660,47 @@ pub(crate) fn script_specification(
}) })
.collect::<Result<Vec<_>, _>>()?; .collect::<Result<Vec<_>, _>>()?;
Ok(Some(RequirementsSpecification::from_overrides( // Collect any `tool.uv.extra-build-dependencies` from the script.
requirements, let empty = BTreeMap::default();
constraints, let script_extra_build_dependencies = script
overrides, .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::<Result<Vec<_>, _>>()?;
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. /// Warn if the user provides (e.g.) an `--index-url` in a requirements file.

View File

@ -386,6 +386,7 @@ pub(crate) async fn remove(
/// Represents the destination where dependencies are added, either to a project or a script. /// Represents the destination where dependencies are added, either to a project or a script.
#[derive(Debug)] #[derive(Debug)]
#[allow(clippy::large_enum_variant)]
enum RemoveTarget { enum RemoveTarget {
/// A PEP 723 script, with inline metadata. /// A PEP 723 script, with inline metadata.
Project(VirtualProject), Project(VirtualProject),

View File

@ -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. // 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( let environment = ScriptEnvironment::get_or_init(
(&script).into(), (&script).into(),
python.as_deref().map(PythonRequest::parse), 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, spec,
modifications, modifications,
build_constraints.unwrap_or_default(), build_constraints.unwrap_or_default(),
script_extra_build_requires,
&settings, &settings,
&network_settings, &network_settings,
&sync_state, &sync_state,

View File

@ -15,7 +15,7 @@ use uv_configuration::{
BuildDependencyStrategy, Concurrency, Constraints, DependencyGroups, BuildDependencyStrategy, Concurrency, Constraints, DependencyGroups,
DependencyGroupsWithDefaults, DryRun, EditableMode, ExtrasSpecification, DependencyGroupsWithDefaults, DryRun, EditableMode, ExtrasSpecification,
ExtrasSpecificationWithDefaults, HashCheckingMode, InstallOptions, Preview, PreviewFeatures, ExtrasSpecificationWithDefaults, HashCheckingMode, InstallOptions, Preview, PreviewFeatures,
TargetTriple, TargetTriple, Upgrade,
}; };
use uv_dispatch::BuildDispatch; use uv_dispatch::BuildDispatch;
use uv_distribution_types::{ use uv_distribution_types::{
@ -27,12 +27,13 @@ use uv_normalize::{DefaultExtras, DefaultGroups, PackageName};
use uv_pep508::{MarkerTree, VersionOrUrl}; use uv_pep508::{MarkerTree, VersionOrUrl};
use uv_pypi_types::{ParsedArchiveUrl, ParsedGitUrl, ParsedUrl}; use uv_pypi_types::{ParsedArchiveUrl, ParsedGitUrl, ParsedUrl};
use uv_python::{PythonDownloads, PythonEnvironment, PythonPreference, PythonRequest}; use uv_python::{PythonDownloads, PythonEnvironment, PythonPreference, PythonRequest};
use uv_resolver::{FlatIndex, Installable, Lock, Preference, Preferences, ResolverEnvironment}; use uv_requirements::RequirementsSpecification;
use uv_resolver::{FlatIndex, ForkStrategy, Installable, Lock, Preference, Preferences, PrereleaseMode, ResolutionMode, ResolverEnvironment};
use uv_scripts::{Pep723ItemRef, Pep723Script}; use uv_scripts::{Pep723ItemRef, Pep723Script};
use uv_settings::PythonInstallMirrors; use uv_settings::PythonInstallMirrors;
use uv_types::{BuildIsolation, HashStrategy}; use uv_types::{BuildIsolation, HashStrategy};
use uv_warnings::{warn_user, warn_user_once}; 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}; use uv_workspace::{DiscoveryOptions, MemberDiscovery, VirtualProject, Workspace, WorkspaceCache};
use crate::commands::pip::loggers::{DefaultInstallLogger, DefaultResolveLogger, InstallLogger}; use crate::commands::pip::loggers::{DefaultInstallLogger, DefaultResolveLogger, InstallLogger};
@ -48,7 +49,9 @@ use crate::commands::project::{
}; };
use crate::commands::{ExitStatus, diagnostics}; use crate::commands::{ExitStatus, diagnostics};
use crate::printer::Printer; use crate::printer::Printer;
use crate::settings::{InstallerSettingsRef, NetworkSettings, ResolverInstallerSettings}; use crate::settings::{
InstallerSettingsRef, NetworkSettings, ResolverInstallerSettings, ResolverSettings,
};
/// Sync the project environment. /// Sync the project environment.
#[allow(clippy::fn_params_excessive_bools)] #[allow(clippy::fn_params_excessive_bools)]
@ -223,8 +226,18 @@ pub(crate) async fn sync(
} }
// Parse the requirements from the script. // Parse the requirements from the script.
let spec = script_specification(Pep723ItemRef::Script(script), &settings.resolver)? let script_spec =
.unwrap_or_default(); 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. // Parse the build constraints from the script.
let build_constraints = script let build_constraints = script
@ -249,6 +262,7 @@ pub(crate) async fn sync(
spec, spec,
modifications, modifications,
build_constraints.unwrap_or_default(), build_constraints.unwrap_or_default(),
script_extra_build_requires,
&settings, &settings,
&network_settings, &network_settings,
&PlatformState::default(), &PlatformState::default(),
@ -495,6 +509,7 @@ fn identify_installation_target<'a>(
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
#[allow(clippy::large_enum_variant)]
enum SyncTarget { enum SyncTarget {
/// Sync a project environment. /// Sync a project environment.
Project(VirtualProject), Project(VirtualProject),
@ -579,6 +594,7 @@ pub(super) async fn do_sync(
config_settings_package, config_settings_package,
no_build_isolation, no_build_isolation,
no_build_isolation_package, no_build_isolation_package,
extra_build_dependencies,
exclude_newer, exclude_newer,
link_mode, link_mode,
compile_bytecode, compile_bytecode,
@ -588,6 +604,57 @@ pub(super) async fn do_sync(
build_dependency_strategy, build_dependency_strategy,
} = settings; } = settings;
if !preview.is_enabled(PreviewFeatures::EXTRA_BUILD_DEPENDENCIES)
&& !extra_build_dependencies.is_empty()
{
warn_user_once!(
"The `extra-build-dependencies` option is experimental and may change without warning. Pass `--preview-features {}` to disable this warning.",
PreviewFeatures::EXTRA_BUILD_DEPENDENCIES
);
}
// Lower the extra build dependencies with source resolution
let extra_build_requires = match &target {
InstallTarget::Workspace { workspace, .. }
| InstallTarget::Project { workspace, .. }
| InstallTarget::NonProjectWorkspace { workspace, .. } => {
uv_distribution::ExtraBuildRequires::from_workspace(
extra_build_dependencies.clone(),
workspace,
index_locations,
sources,
)?
}
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(),
build_dependency_strategy: *build_dependency_strategy,
};
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() let client_builder = BaseClientBuilder::new()
.retries_from_env()? .retries_from_env()?
.connectivity(network_settings.connectivity) .connectivity(network_settings.connectivity)
@ -739,6 +806,7 @@ pub(super) async fn do_sync(
config_setting, config_setting,
config_settings_package, config_settings_package,
build_isolation, build_isolation,
&extra_build_requires,
link_mode, link_mode,
build_options, build_options,
&build_hasher, &build_hasher,

View File

@ -203,6 +203,7 @@ pub(crate) async fn tree(
config_settings_package: _, config_settings_package: _,
no_build_isolation: _, no_build_isolation: _,
no_build_isolation_package: _, no_build_isolation_package: _,
extra_build_dependencies: _,
exclude_newer: _, exclude_newer: _,
link_mode: _, link_mode: _,
upgrade: _, upgrade: _,

View File

@ -23,7 +23,7 @@ use uv_requirements::{RequirementsSource, RequirementsSpecification};
use uv_settings::{PythonInstallMirrors, ResolverInstallerOptions, ToolOptions}; use uv_settings::{PythonInstallMirrors, ResolverInstallerOptions, ToolOptions};
use uv_tool::InstalledTools; use uv_tool::InstalledTools;
use uv_warnings::warn_user; use uv_warnings::warn_user;
use uv_workspace::WorkspaceCache; use uv_workspace::{WorkspaceCache, pyproject::ExtraBuildDependencies};
use crate::commands::ExitStatus; use crate::commands::ExitStatus;
use crate::commands::pip::loggers::{DefaultInstallLogger, DefaultResolveLogger}; use crate::commands::pip::loggers::{DefaultInstallLogger, DefaultResolveLogger};
@ -439,6 +439,7 @@ pub(crate) async fn install(
spec, spec,
Modifications::Exact, Modifications::Exact,
Constraints::from_requirements(build_constraints.iter().cloned()), Constraints::from_requirements(build_constraints.iter().cloned()),
uv_distribution::ExtraBuildRequires::from_lowered(ExtraBuildDependencies::default()),
&settings, &settings,
&network_settings, &network_settings,
&state, &state,

View File

@ -19,7 +19,7 @@ use uv_requirements::RequirementsSpecification;
use uv_settings::{Combine, PythonInstallMirrors, ResolverInstallerOptions, ToolOptions}; use uv_settings::{Combine, PythonInstallMirrors, ResolverInstallerOptions, ToolOptions};
use uv_tool::InstalledTools; use uv_tool::InstalledTools;
use uv_warnings::write_error_chain; use uv_warnings::write_error_chain;
use uv_workspace::WorkspaceCache; use uv_workspace::{WorkspaceCache, pyproject::ExtraBuildDependencies};
use crate::commands::pip::loggers::{ use crate::commands::pip::loggers::{
DefaultInstallLogger, SummaryResolveLogger, UpgradeInstallLogger, DefaultInstallLogger, SummaryResolveLogger, UpgradeInstallLogger,
@ -337,6 +337,7 @@ async fn upgrade_tool(
spec, spec,
Modifications::Exact, Modifications::Exact,
build_constraints, build_constraints,
uv_distribution::ExtraBuildRequires::from_lowered(ExtraBuildDependencies::default()),
&settings, &settings,
network_settings, network_settings,
&state, &state,

View File

@ -29,6 +29,7 @@ use uv_shell::{Shell, shlex_posix, shlex_windows};
use uv_types::{AnyErrorBuild, BuildContext, BuildIsolation, BuildStack, HashStrategy}; use uv_types::{AnyErrorBuild, BuildContext, BuildIsolation, BuildStack, HashStrategy};
use uv_virtualenv::OnExisting; use uv_virtualenv::OnExisting;
use uv_warnings::warn_user; use uv_warnings::warn_user;
use uv_workspace::pyproject::ExtraBuildDependencies;
use uv_workspace::{DiscoveryOptions, VirtualProject, WorkspaceCache, WorkspaceError}; use uv_workspace::{DiscoveryOptions, VirtualProject, WorkspaceCache, WorkspaceError};
use crate::commands::ExitStatus; use crate::commands::ExitStatus;
@ -266,7 +267,8 @@ pub(crate) async fn venv(
// Do not allow builds // Do not allow builds
let build_options = BuildOptions::new(NoBinary::None, NoBuild::All); let build_options = BuildOptions::new(NoBinary::None, NoBuild::All);
let extra_build_requires =
uv_distribution::ExtraBuildRequires::from_lowered(ExtraBuildDependencies::default());
// Prep the build context. // Prep the build context.
let build_dispatch = BuildDispatch::new( let build_dispatch = BuildDispatch::new(
&client, &client,
@ -281,6 +283,7 @@ pub(crate) async fn venv(
&config_settings, &config_settings,
&config_settings_package, &config_settings_package,
BuildIsolation::Isolated, BuildIsolation::Isolated,
&extra_build_requires,
link_mode, link_mode,
&build_options, &build_options,
&build_hasher, &build_hasher,

View File

@ -28,7 +28,7 @@ use uv_cli::{
ProjectCommand, PythonCommand, PythonNamespace, SelfCommand, SelfNamespace, ToolCommand, ProjectCommand, PythonCommand, PythonNamespace, SelfCommand, SelfNamespace, ToolCommand,
ToolNamespace, TopLevelArgs, compat::CompatArgs, ToolNamespace, TopLevelArgs, compat::CompatArgs,
}; };
use uv_configuration::min_stack_size; use uv_configuration::{PreviewFeatures, min_stack_size};
use uv_fs::{CWD, Simplified}; use uv_fs::{CWD, Simplified};
#[cfg(feature = "self-update")] #[cfg(feature = "self-update")]
use uv_pep440::release_specifiers_to_ranges; use uv_pep440::release_specifiers_to_ranges;
@ -443,6 +443,16 @@ async fn run(mut cli: Cli) -> Result<ExitStatus> {
// Resolve the settings from the command-line arguments and workspace configuration. // Resolve the settings from the command-line arguments and workspace configuration.
let args = PipCompileSettings::resolve(args, filesystem); let args = PipCompileSettings::resolve(args, filesystem);
show_settings!(args); show_settings!(args);
if !args.settings.extra_build_dependencies.is_empty()
&& !globals
.preview
.is_enabled(PreviewFeatures::EXTRA_BUILD_DEPENDENCIES)
{
warn_user_once!(
"The `extra-build-dependencies` setting is experimental and may change without warning. Pass `--preview-features {}` to disable this warning.",
PreviewFeatures::EXTRA_BUILD_DEPENDENCIES
);
}
// Initialize the cache. // Initialize the cache.
let cache = cache.init()?.with_refresh( let cache = cache.init()?.with_refresh(
@ -516,6 +526,7 @@ async fn run(mut cli: Cli) -> Result<ExitStatus> {
args.settings.config_settings_package, args.settings.config_settings_package,
args.settings.no_build_isolation, args.settings.no_build_isolation,
args.settings.no_build_isolation_package, args.settings.no_build_isolation_package,
&args.settings.extra_build_dependencies,
args.settings.build_options, args.settings.build_options,
args.settings.python_version, args.settings.python_version,
args.settings.python_platform, args.settings.python_platform,
@ -543,6 +554,16 @@ async fn run(mut cli: Cli) -> Result<ExitStatus> {
// Resolve the settings from the command-line arguments and workspace configuration. // Resolve the settings from the command-line arguments and workspace configuration.
let args = PipSyncSettings::resolve(args, filesystem); let args = PipSyncSettings::resolve(args, filesystem);
show_settings!(args); show_settings!(args);
if !args.settings.extra_build_dependencies.is_empty()
&& !globals
.preview
.is_enabled(PreviewFeatures::EXTRA_BUILD_DEPENDENCIES)
{
warn_user_once!(
"The `extra-build-dependencies` setting is experimental and may change without warning. Pass `--preview-features {}` to disable this warning.",
PreviewFeatures::EXTRA_BUILD_DEPENDENCIES
);
}
// Initialize the cache. // Initialize the cache.
let cache = cache.init()?.with_refresh( let cache = cache.init()?.with_refresh(
@ -593,6 +614,7 @@ async fn run(mut cli: Cli) -> Result<ExitStatus> {
&args.settings.config_settings_package, &args.settings.config_settings_package,
args.settings.no_build_isolation, args.settings.no_build_isolation,
args.settings.no_build_isolation_package, args.settings.no_build_isolation_package,
&args.settings.extra_build_dependencies,
args.settings.build_options, args.settings.build_options,
args.settings.python_version, args.settings.python_version,
args.settings.python_platform, args.settings.python_platform,
@ -621,6 +643,16 @@ async fn run(mut cli: Cli) -> Result<ExitStatus> {
// Resolve the settings from the command-line arguments and workspace configuration. // Resolve the settings from the command-line arguments and workspace configuration.
let mut args = PipInstallSettings::resolve(args, filesystem); let mut args = PipInstallSettings::resolve(args, filesystem);
show_settings!(args); show_settings!(args);
if !args.settings.extra_build_dependencies.is_empty()
&& !globals
.preview
.is_enabled(PreviewFeatures::EXTRA_BUILD_DEPENDENCIES)
{
warn_user_once!(
"The `extra-build-dependencies` setting is experimental and may change without warning. Pass `--preview-features {}` to disable this warning.",
PreviewFeatures::EXTRA_BUILD_DEPENDENCIES
);
}
let mut requirements = Vec::with_capacity( let mut requirements = Vec::with_capacity(
args.package.len() + args.editables.len() + args.requirements.len(), args.package.len() + args.editables.len() + args.requirements.len(),
@ -735,6 +767,7 @@ async fn run(mut cli: Cli) -> Result<ExitStatus> {
&args.settings.config_settings_package, &args.settings.config_settings_package,
args.settings.no_build_isolation, args.settings.no_build_isolation,
args.settings.no_build_isolation_package, args.settings.no_build_isolation_package,
&args.settings.extra_build_dependencies,
args.settings.build_options, args.settings.build_options,
args.modifications, args.modifications,
args.settings.python_version, args.settings.python_version,

View File

@ -44,7 +44,7 @@ use uv_settings::{
use uv_static::EnvVars; use uv_static::EnvVars;
use uv_torch::TorchMode; use uv_torch::TorchMode;
use uv_warnings::warn_user_once; use uv_warnings::warn_user_once;
use uv_workspace::pyproject::DependencyType; use uv_workspace::pyproject::{DependencyType, ExtraBuildDependencies};
use uv_workspace::pyproject_mut::AddBoundsKind; use uv_workspace::pyproject_mut::AddBoundsKind;
use crate::commands::ToolRunCommand; use crate::commands::ToolRunCommand;
@ -2708,6 +2708,7 @@ pub(crate) struct InstallerSettingsRef<'a> {
pub(crate) config_settings_package: &'a PackageConfigSettings, pub(crate) config_settings_package: &'a PackageConfigSettings,
pub(crate) no_build_isolation: bool, pub(crate) no_build_isolation: bool,
pub(crate) no_build_isolation_package: &'a [PackageName], pub(crate) no_build_isolation_package: &'a [PackageName],
pub(crate) extra_build_dependencies: &'a ExtraBuildDependencies,
pub(crate) exclude_newer: Option<ExcludeNewer>, pub(crate) exclude_newer: Option<ExcludeNewer>,
pub(crate) link_mode: LinkMode, pub(crate) link_mode: LinkMode,
pub(crate) compile_bytecode: bool, pub(crate) compile_bytecode: bool,
@ -2735,6 +2736,7 @@ pub(crate) struct ResolverSettings {
pub(crate) link_mode: LinkMode, pub(crate) link_mode: LinkMode,
pub(crate) no_build_isolation: bool, pub(crate) no_build_isolation: bool,
pub(crate) no_build_isolation_package: Vec<PackageName>, pub(crate) no_build_isolation_package: Vec<PackageName>,
pub(crate) extra_build_dependencies: ExtraBuildDependencies,
pub(crate) prerelease: PrereleaseMode, pub(crate) prerelease: PrereleaseMode,
pub(crate) resolution: ResolutionMode, pub(crate) resolution: ResolutionMode,
pub(crate) sources: SourceStrategy, pub(crate) sources: SourceStrategy,
@ -2788,6 +2790,7 @@ impl From<ResolverOptions> for ResolverSettings {
config_settings_package: value.config_settings_package.unwrap_or_default(), config_settings_package: value.config_settings_package.unwrap_or_default(),
no_build_isolation: value.no_build_isolation.unwrap_or_default(), no_build_isolation: value.no_build_isolation.unwrap_or_default(),
no_build_isolation_package: value.no_build_isolation_package.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, exclude_newer: value.exclude_newer,
link_mode: value.link_mode.unwrap_or_default(), link_mode: value.link_mode.unwrap_or_default(),
sources: SourceStrategy::from_args(value.no_sources.unwrap_or_default()), sources: SourceStrategy::from_args(value.no_sources.unwrap_or_default()),
@ -2878,6 +2881,7 @@ impl From<ResolverInstallerOptions> for ResolverInstallerSettings {
link_mode: value.link_mode.unwrap_or_default(), link_mode: value.link_mode.unwrap_or_default(),
no_build_isolation: value.no_build_isolation.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(), 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(), prerelease: value.prerelease.unwrap_or_default(),
resolution: value.resolution.unwrap_or_default(), resolution: value.resolution.unwrap_or_default(),
sources: SourceStrategy::from_args(value.no_sources.unwrap_or_default()), sources: SourceStrategy::from_args(value.no_sources.unwrap_or_default()),
@ -2921,6 +2925,7 @@ pub(crate) struct PipSettings {
pub(crate) torch_backend: Option<TorchMode>, pub(crate) torch_backend: Option<TorchMode>,
pub(crate) no_build_isolation: bool, pub(crate) no_build_isolation: bool,
pub(crate) no_build_isolation_package: Vec<PackageName>, pub(crate) no_build_isolation_package: Vec<PackageName>,
pub(crate) extra_build_dependencies: ExtraBuildDependencies,
pub(crate) build_options: BuildOptions, pub(crate) build_options: BuildOptions,
pub(crate) allow_empty_requirements: bool, pub(crate) allow_empty_requirements: bool,
pub(crate) strict: bool, pub(crate) strict: bool,
@ -2988,6 +2993,7 @@ impl PipSettings {
only_binary, only_binary,
no_build_isolation, no_build_isolation,
no_build_isolation_package, no_build_isolation_package,
extra_build_dependencies,
strict, strict,
extra, extra,
all_extras, all_extras,
@ -3046,6 +3052,7 @@ impl PipSettings {
config_settings_package: top_level_config_settings_package, config_settings_package: top_level_config_settings_package,
no_build_isolation: top_level_no_build_isolation, no_build_isolation: top_level_no_build_isolation,
no_build_isolation_package: top_level_no_build_isolation_package, no_build_isolation_package: top_level_no_build_isolation_package,
extra_build_dependencies: top_level_extra_build_dependencies,
exclude_newer: top_level_exclude_newer, exclude_newer: top_level_exclude_newer,
link_mode: top_level_link_mode, link_mode: top_level_link_mode,
compile_bytecode: top_level_compile_bytecode, compile_bytecode: top_level_compile_bytecode,
@ -3082,6 +3089,8 @@ impl PipSettings {
let no_build_isolation = no_build_isolation.combine(top_level_no_build_isolation); let no_build_isolation = no_build_isolation.combine(top_level_no_build_isolation);
let no_build_isolation_package = let no_build_isolation_package =
no_build_isolation_package.combine(top_level_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 exclude_newer = exclude_newer.combine(top_level_exclude_newer);
let link_mode = link_mode.combine(top_level_link_mode); let link_mode = link_mode.combine(top_level_link_mode);
let compile_bytecode = compile_bytecode.combine(top_level_compile_bytecode); let compile_bytecode = compile_bytecode.combine(top_level_compile_bytecode);
@ -3177,6 +3186,10 @@ impl PipSettings {
.no_build_isolation_package .no_build_isolation_package
.combine(no_build_isolation_package) .combine(no_build_isolation_package)
.unwrap_or_default(), .unwrap_or_default(),
extra_build_dependencies: args
.extra_build_dependencies
.combine(extra_build_dependencies)
.unwrap_or_default(),
config_setting: args config_setting: args
.config_settings .config_settings
.combine(config_settings) .combine(config_settings)
@ -3281,6 +3294,7 @@ impl<'a> From<&'a ResolverInstallerSettings> for InstallerSettingsRef<'a> {
config_settings_package: &settings.resolver.config_settings_package, config_settings_package: &settings.resolver.config_settings_package,
no_build_isolation: settings.resolver.no_build_isolation, no_build_isolation: settings.resolver.no_build_isolation,
no_build_isolation_package: &settings.resolver.no_build_isolation_package, no_build_isolation_package: &settings.resolver.no_build_isolation_package,
extra_build_dependencies: &settings.resolver.extra_build_dependencies,
exclude_newer: settings.resolver.exclude_newer, exclude_newer: settings.resolver.exclude_newer,
link_mode: settings.resolver.link_mode, link_mode: settings.resolver.link_mode,
compile_bytecode: settings.compile_bytecode, compile_bytecode: settings.compile_bytecode,

View File

@ -3920,16 +3920,17 @@ fn config_settings_registry() {
.arg("iniconfig") .arg("iniconfig")
.arg("--no-binary") .arg("--no-binary")
.arg("iniconfig") .arg("iniconfig")
.arg("-C=global-option=build_ext"), @r###" .arg("-C=global-option=build_ext"), @r"
success: true success: true
exit_code: 0 exit_code: 0
----- stdout ----- ----- stdout -----
----- stderr ----- ----- stderr -----
Resolved 1 package in [TIME] Resolved 1 package in [TIME]
Prepared 1 package in [TIME]
Installed 1 package in [TIME] Installed 1 package in [TIME]
+ iniconfig==2.0.0 + iniconfig==2.0.0
"### "
); );
// Uninstall the package. // Uninstall the package.

View File

@ -184,6 +184,7 @@ fn resolve_uv_toml() -> anyhow::Result<()> {
torch_backend: None, torch_backend: None,
no_build_isolation: false, no_build_isolation: false,
no_build_isolation_package: [], no_build_isolation_package: [],
extra_build_dependencies: {},
build_options: BuildOptions { build_options: BuildOptions {
no_binary: None, no_binary: None,
no_build: None, no_build: None,
@ -373,6 +374,7 @@ fn resolve_uv_toml() -> anyhow::Result<()> {
torch_backend: None, torch_backend: None,
no_build_isolation: false, no_build_isolation: false,
no_build_isolation_package: [], no_build_isolation_package: [],
extra_build_dependencies: {},
build_options: BuildOptions { build_options: BuildOptions {
no_binary: None, no_binary: None,
no_build: None, no_build: None,
@ -563,6 +565,7 @@ fn resolve_uv_toml() -> anyhow::Result<()> {
torch_backend: None, torch_backend: None,
no_build_isolation: false, no_build_isolation: false,
no_build_isolation_package: [], no_build_isolation_package: [],
extra_build_dependencies: {},
build_options: BuildOptions { build_options: BuildOptions {
no_binary: None, no_binary: None,
no_build: None, no_build: None,
@ -785,6 +788,7 @@ fn resolve_pyproject_toml() -> anyhow::Result<()> {
torch_backend: None, torch_backend: None,
no_build_isolation: false, no_build_isolation: false,
no_build_isolation_package: [], no_build_isolation_package: [],
extra_build_dependencies: {},
build_options: BuildOptions { build_options: BuildOptions {
no_binary: None, no_binary: None,
no_build: None, no_build: None,
@ -942,6 +946,7 @@ fn resolve_pyproject_toml() -> anyhow::Result<()> {
torch_backend: None, torch_backend: None,
no_build_isolation: false, no_build_isolation: false,
no_build_isolation_package: [], no_build_isolation_package: [],
extra_build_dependencies: {},
build_options: BuildOptions { build_options: BuildOptions {
no_binary: None, no_binary: None,
no_build: None, no_build: None,
@ -1143,6 +1148,7 @@ fn resolve_pyproject_toml() -> anyhow::Result<()> {
torch_backend: None, torch_backend: None,
no_build_isolation: false, no_build_isolation: false,
no_build_isolation_package: [], no_build_isolation_package: [],
extra_build_dependencies: {},
build_options: BuildOptions { build_options: BuildOptions {
no_binary: None, no_binary: None,
no_build: None, no_build: None,
@ -1392,6 +1398,7 @@ fn resolve_index_url() -> anyhow::Result<()> {
torch_backend: None, torch_backend: None,
no_build_isolation: false, no_build_isolation: false,
no_build_isolation_package: [], no_build_isolation_package: [],
extra_build_dependencies: {},
build_options: BuildOptions { build_options: BuildOptions {
no_binary: None, no_binary: None,
no_build: None, no_build: None,
@ -1651,6 +1658,7 @@ fn resolve_index_url() -> anyhow::Result<()> {
torch_backend: None, torch_backend: None,
no_build_isolation: false, no_build_isolation: false,
no_build_isolation_package: [], no_build_isolation_package: [],
extra_build_dependencies: {},
build_options: BuildOptions { build_options: BuildOptions {
no_binary: None, no_binary: None,
no_build: None, no_build: None,
@ -1865,6 +1873,7 @@ fn resolve_find_links() -> anyhow::Result<()> {
torch_backend: None, torch_backend: None,
no_build_isolation: false, no_build_isolation: false,
no_build_isolation_package: [], no_build_isolation_package: [],
extra_build_dependencies: {},
build_options: BuildOptions { build_options: BuildOptions {
no_binary: None, no_binary: None,
no_build: None, no_build: None,
@ -2044,6 +2053,7 @@ fn resolve_top_level() -> anyhow::Result<()> {
torch_backend: None, torch_backend: None,
no_build_isolation: false, no_build_isolation: false,
no_build_isolation_package: [], no_build_isolation_package: [],
extra_build_dependencies: {},
build_options: BuildOptions { build_options: BuildOptions {
no_binary: None, no_binary: None,
no_build: None, no_build: None,
@ -2283,6 +2293,7 @@ fn resolve_top_level() -> anyhow::Result<()> {
torch_backend: None, torch_backend: None,
no_build_isolation: false, no_build_isolation: false,
no_build_isolation_package: [], no_build_isolation_package: [],
extra_build_dependencies: {},
build_options: BuildOptions { build_options: BuildOptions {
no_binary: None, no_binary: None,
no_build: None, no_build: None,
@ -2505,6 +2516,7 @@ fn resolve_top_level() -> anyhow::Result<()> {
torch_backend: None, torch_backend: None,
no_build_isolation: false, no_build_isolation: false,
no_build_isolation_package: [], no_build_isolation_package: [],
extra_build_dependencies: {},
build_options: BuildOptions { build_options: BuildOptions {
no_binary: None, no_binary: None,
no_build: None, no_build: None,
@ -2683,6 +2695,7 @@ fn resolve_user_configuration() -> anyhow::Result<()> {
torch_backend: None, torch_backend: None,
no_build_isolation: false, no_build_isolation: false,
no_build_isolation_package: [], no_build_isolation_package: [],
extra_build_dependencies: {},
build_options: BuildOptions { build_options: BuildOptions {
no_binary: None, no_binary: None,
no_build: None, no_build: None,
@ -2845,6 +2858,7 @@ fn resolve_user_configuration() -> anyhow::Result<()> {
torch_backend: None, torch_backend: None,
no_build_isolation: false, no_build_isolation: false,
no_build_isolation_package: [], no_build_isolation_package: [],
extra_build_dependencies: {},
build_options: BuildOptions { build_options: BuildOptions {
no_binary: None, no_binary: None,
no_build: None, no_build: None,
@ -3007,6 +3021,7 @@ fn resolve_user_configuration() -> anyhow::Result<()> {
torch_backend: None, torch_backend: None,
no_build_isolation: false, no_build_isolation: false,
no_build_isolation_package: [], no_build_isolation_package: [],
extra_build_dependencies: {},
build_options: BuildOptions { build_options: BuildOptions {
no_binary: None, no_binary: None,
no_build: None, no_build: None,
@ -3171,6 +3186,7 @@ fn resolve_user_configuration() -> anyhow::Result<()> {
torch_backend: None, torch_backend: None,
no_build_isolation: false, no_build_isolation: false,
no_build_isolation_package: [], no_build_isolation_package: [],
extra_build_dependencies: {},
build_options: BuildOptions { build_options: BuildOptions {
no_binary: None, no_binary: None,
no_build: None, no_build: None,
@ -3327,6 +3343,7 @@ fn resolve_tool() -> anyhow::Result<()> {
config_settings_package: None, config_settings_package: None,
no_build_isolation: None, no_build_isolation: None,
no_build_isolation_package: None, no_build_isolation_package: None,
extra_build_dependencies: None,
exclude_newer: None, exclude_newer: None,
link_mode: Some( link_mode: Some(
Clone, Clone,
@ -3369,6 +3386,7 @@ fn resolve_tool() -> anyhow::Result<()> {
link_mode: Clone, link_mode: Clone,
no_build_isolation: false, no_build_isolation: false,
no_build_isolation_package: [], no_build_isolation_package: [],
extra_build_dependencies: {},
prerelease: IfNecessaryOrExplicit, prerelease: IfNecessaryOrExplicit,
resolution: LowestDirect, resolution: LowestDirect,
sources: Enabled, sources: Enabled,
@ -3527,6 +3545,7 @@ fn resolve_poetry_toml() -> anyhow::Result<()> {
torch_backend: None, torch_backend: None,
no_build_isolation: false, no_build_isolation: false,
no_build_isolation_package: [], no_build_isolation_package: [],
extra_build_dependencies: {},
build_options: BuildOptions { build_options: BuildOptions {
no_binary: None, no_binary: None,
no_build: None, no_build: None,
@ -3757,6 +3776,7 @@ fn resolve_both() -> anyhow::Result<()> {
torch_backend: None, torch_backend: None,
no_build_isolation: false, no_build_isolation: false,
no_build_isolation_package: [], no_build_isolation_package: [],
extra_build_dependencies: {},
build_options: BuildOptions { build_options: BuildOptions {
no_binary: None, no_binary: None,
no_build: None, no_build: None,
@ -3991,6 +4011,7 @@ fn resolve_both_special_fields() -> anyhow::Result<()> {
torch_backend: None, torch_backend: None,
no_build_isolation: false, no_build_isolation: false,
no_build_isolation_package: [], no_build_isolation_package: [],
extra_build_dependencies: {},
build_options: BuildOptions { build_options: BuildOptions {
no_binary: None, no_binary: None,
no_build: None, no_build: None,
@ -4304,6 +4325,7 @@ fn resolve_config_file() -> anyhow::Result<()> {
torch_backend: None, torch_backend: None,
no_build_isolation: false, no_build_isolation: false,
no_build_isolation_package: [], no_build_isolation_package: [],
extra_build_dependencies: {},
build_options: BuildOptions { build_options: BuildOptions {
no_binary: None, no_binary: None,
no_build: None, no_build: None,
@ -4384,7 +4406,7 @@ fn resolve_config_file() -> anyhow::Result<()> {
| |
1 | [project] 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`
" "
); );
@ -4559,6 +4581,7 @@ fn resolve_skip_empty() -> anyhow::Result<()> {
torch_backend: None, torch_backend: None,
no_build_isolation: false, no_build_isolation: false,
no_build_isolation_package: [], no_build_isolation_package: [],
extra_build_dependencies: {},
build_options: BuildOptions { build_options: BuildOptions {
no_binary: None, no_binary: None,
no_build: None, no_build: None,
@ -4724,6 +4747,7 @@ fn resolve_skip_empty() -> anyhow::Result<()> {
torch_backend: None, torch_backend: None,
no_build_isolation: false, no_build_isolation: false,
no_build_isolation_package: [], no_build_isolation_package: [],
extra_build_dependencies: {},
build_options: BuildOptions { build_options: BuildOptions {
no_binary: None, no_binary: None,
no_build: None, no_build: None,
@ -4908,6 +4932,7 @@ fn allow_insecure_host() -> anyhow::Result<()> {
torch_backend: None, torch_backend: None,
no_build_isolation: false, no_build_isolation: false,
no_build_isolation_package: [], no_build_isolation_package: [],
extra_build_dependencies: {},
build_options: BuildOptions { build_options: BuildOptions {
no_binary: None, no_binary: None,
no_build: None, no_build: None,
@ -5153,6 +5178,7 @@ fn index_priority() -> anyhow::Result<()> {
torch_backend: None, torch_backend: None,
no_build_isolation: false, no_build_isolation: false,
no_build_isolation_package: [], no_build_isolation_package: [],
extra_build_dependencies: {},
build_options: BuildOptions { build_options: BuildOptions {
no_binary: None, no_binary: None,
no_build: None, no_build: None,
@ -5377,6 +5403,7 @@ fn index_priority() -> anyhow::Result<()> {
torch_backend: None, torch_backend: None,
no_build_isolation: false, no_build_isolation: false,
no_build_isolation_package: [], no_build_isolation_package: [],
extra_build_dependencies: {},
build_options: BuildOptions { build_options: BuildOptions {
no_binary: None, no_binary: None,
no_build: None, no_build: None,
@ -5607,6 +5634,7 @@ fn index_priority() -> anyhow::Result<()> {
torch_backend: None, torch_backend: None,
no_build_isolation: false, no_build_isolation: false,
no_build_isolation_package: [], no_build_isolation_package: [],
extra_build_dependencies: {},
build_options: BuildOptions { build_options: BuildOptions {
no_binary: None, no_binary: None,
no_build: None, no_build: None,
@ -5832,6 +5860,7 @@ fn index_priority() -> anyhow::Result<()> {
torch_backend: None, torch_backend: None,
no_build_isolation: false, no_build_isolation: false,
no_build_isolation_package: [], no_build_isolation_package: [],
extra_build_dependencies: {},
build_options: BuildOptions { build_options: BuildOptions {
no_binary: None, no_binary: None,
no_build: None, no_build: None,
@ -6064,6 +6093,7 @@ fn index_priority() -> anyhow::Result<()> {
torch_backend: None, torch_backend: None,
no_build_isolation: false, no_build_isolation: false,
no_build_isolation_package: [], no_build_isolation_package: [],
extra_build_dependencies: {},
build_options: BuildOptions { build_options: BuildOptions {
no_binary: None, no_binary: None,
no_build: None, no_build: None,
@ -6289,6 +6319,7 @@ fn index_priority() -> anyhow::Result<()> {
torch_backend: None, torch_backend: None,
no_build_isolation: false, no_build_isolation: false,
no_build_isolation_package: [], no_build_isolation_package: [],
extra_build_dependencies: {},
build_options: BuildOptions { build_options: BuildOptions {
no_binary: None, no_binary: None,
no_build: None, no_build: None,
@ -6458,6 +6489,7 @@ fn verify_hashes() -> anyhow::Result<()> {
torch_backend: None, torch_backend: None,
no_build_isolation: false, no_build_isolation: false,
no_build_isolation_package: [], no_build_isolation_package: [],
extra_build_dependencies: {},
build_options: BuildOptions { build_options: BuildOptions {
no_binary: None, no_binary: None,
no_build: None, no_build: None,
@ -6613,6 +6645,7 @@ fn verify_hashes() -> anyhow::Result<()> {
torch_backend: None, torch_backend: None,
no_build_isolation: false, no_build_isolation: false,
no_build_isolation_package: [], no_build_isolation_package: [],
extra_build_dependencies: {},
build_options: BuildOptions { build_options: BuildOptions {
no_binary: None, no_binary: None,
no_build: None, no_build: None,
@ -6766,6 +6799,7 @@ fn verify_hashes() -> anyhow::Result<()> {
torch_backend: None, torch_backend: None,
no_build_isolation: false, no_build_isolation: false,
no_build_isolation_package: [], no_build_isolation_package: [],
extra_build_dependencies: {},
build_options: BuildOptions { build_options: BuildOptions {
no_binary: None, no_binary: None,
no_build: None, no_build: None,
@ -6921,6 +6955,7 @@ fn verify_hashes() -> anyhow::Result<()> {
torch_backend: None, torch_backend: None,
no_build_isolation: false, no_build_isolation: false,
no_build_isolation_package: [], no_build_isolation_package: [],
extra_build_dependencies: {},
build_options: BuildOptions { build_options: BuildOptions {
no_binary: None, no_binary: None,
no_build: None, no_build: None,
@ -7074,6 +7109,7 @@ fn verify_hashes() -> anyhow::Result<()> {
torch_backend: None, torch_backend: None,
no_build_isolation: false, no_build_isolation: false,
no_build_isolation_package: [], no_build_isolation_package: [],
extra_build_dependencies: {},
build_options: BuildOptions { build_options: BuildOptions {
no_binary: None, no_binary: None,
no_build: None, no_build: None,
@ -7228,6 +7264,7 @@ fn verify_hashes() -> anyhow::Result<()> {
torch_backend: None, torch_backend: None,
no_build_isolation: false, no_build_isolation: false,
no_build_isolation_package: [], no_build_isolation_package: [],
extra_build_dependencies: {},
build_options: BuildOptions { build_options: BuildOptions {
no_binary: None, no_binary: None,
no_build: None, no_build: None,
@ -7320,7 +7357,7 @@ fn preview_features() {
show_settings: true, show_settings: true,
preview: Preview { preview: Preview {
flags: PreviewFeatures( flags: PreviewFeatures(
PYTHON_INSTALL_DEFAULT | PYTHON_UPGRADE | JSON_OUTPUT | PYLOCK | ADD_BOUNDS, PYTHON_INSTALL_DEFAULT | PYTHON_UPGRADE | JSON_OUTPUT | PYLOCK | ADD_BOUNDS | EXTRA_BUILD_DEPENDENCIES,
), ),
}, },
python_preference: Managed, python_preference: Managed,
@ -7386,6 +7423,7 @@ fn preview_features() {
link_mode: Clone, link_mode: Clone,
no_build_isolation: false, no_build_isolation: false,
no_build_isolation_package: [], no_build_isolation_package: [],
extra_build_dependencies: {},
prerelease: IfNecessaryOrExplicit, prerelease: IfNecessaryOrExplicit,
resolution: Highest, resolution: Highest,
sources: Enabled, sources: Enabled,
@ -7488,6 +7526,7 @@ fn preview_features() {
link_mode: Clone, link_mode: Clone,
no_build_isolation: false, no_build_isolation: false,
no_build_isolation_package: [], no_build_isolation_package: [],
extra_build_dependencies: {},
prerelease: IfNecessaryOrExplicit, prerelease: IfNecessaryOrExplicit,
resolution: Highest, resolution: Highest,
sources: Enabled, sources: Enabled,
@ -7524,7 +7563,7 @@ fn preview_features() {
show_settings: true, show_settings: true,
preview: Preview { preview: Preview {
flags: PreviewFeatures( flags: PreviewFeatures(
PYTHON_INSTALL_DEFAULT | PYTHON_UPGRADE | JSON_OUTPUT | PYLOCK | ADD_BOUNDS, PYTHON_INSTALL_DEFAULT | PYTHON_UPGRADE | JSON_OUTPUT | PYLOCK | ADD_BOUNDS | EXTRA_BUILD_DEPENDENCIES,
), ),
}, },
python_preference: Managed, python_preference: Managed,
@ -7590,6 +7629,7 @@ fn preview_features() {
link_mode: Clone, link_mode: Clone,
no_build_isolation: false, no_build_isolation: false,
no_build_isolation_package: [], no_build_isolation_package: [],
extra_build_dependencies: {},
prerelease: IfNecessaryOrExplicit, prerelease: IfNecessaryOrExplicit,
resolution: Highest, resolution: Highest,
sources: Enabled, sources: Enabled,
@ -7692,6 +7732,7 @@ fn preview_features() {
link_mode: Clone, link_mode: Clone,
no_build_isolation: false, no_build_isolation: false,
no_build_isolation_package: [], no_build_isolation_package: [],
extra_build_dependencies: {},
prerelease: IfNecessaryOrExplicit, prerelease: IfNecessaryOrExplicit,
resolution: Highest, resolution: Highest,
sources: Enabled, sources: Enabled,
@ -7794,6 +7835,7 @@ fn preview_features() {
link_mode: Clone, link_mode: Clone,
no_build_isolation: false, no_build_isolation: false,
no_build_isolation_package: [], no_build_isolation_package: [],
extra_build_dependencies: {},
prerelease: IfNecessaryOrExplicit, prerelease: IfNecessaryOrExplicit,
resolution: Highest, resolution: Highest,
sources: Enabled, sources: Enabled,
@ -7898,6 +7940,7 @@ fn preview_features() {
link_mode: Clone, link_mode: Clone,
no_build_isolation: false, no_build_isolation: false,
no_build_isolation_package: [], no_build_isolation_package: [],
extra_build_dependencies: {},
prerelease: IfNecessaryOrExplicit, prerelease: IfNecessaryOrExplicit,
resolution: Highest, resolution: Highest,
sources: Enabled, sources: Enabled,

View File

@ -1567,6 +1567,401 @@ fn sync_build_isolation_extra() -> Result<()> {
Ok(()) Ok(())
} }
#[test]
fn sync_extra_build_dependencies() -> 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()?;
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" }
"#})?;
context.venv().arg("--clear").assert().success();
// Running `uv sync` should fail due to missing build-dependencies
uv_snapshot!(context.filters(), context.sync(), @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`
");
// Adding `extra-build-dependencies` should solve the issue
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]
child = ["anyio"]
"#})?;
context.venv().arg("--clear").assert().success();
uv_snapshot!(context.filters(), context.sync(), @r"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
warning: The `extra-build-dependencies` option is experimental and may change without warning. Pass `--preview-features extra-build-dependencies` to disable this warning.
Resolved [N] packages in [TIME]
Prepared [N] packages in [TIME]
Installed [N] packages in [TIME]
+ child==0.1.0 (from file://[TEMP_DIR]/child)
");
// 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"
version = "0.1.0"
requires-python = ">=3.9"
dependencies = ["child"]
[tool.uv.sources]
child = { path = "child" }
[tool.uv.extra-build-dependencies]
wrong_name = ["anyio"]
"#})?;
context.venv().arg("--clear").assert().success();
uv_snapshot!(context.filters(), context.sync(), @r"
success: false
exit_code: 1
----- stdout -----
----- stderr -----
warning: The `extra-build-dependencies` option is experimental and may change without warning. Pass `--preview-features extra-build-dependencies` 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]
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
context.venv().arg("--clear").assert().success();
uv_snapshot!(context.filters(), context.sync(), @r"
success: false
exit_code: 1
----- stdout -----
----- stderr -----
warning: The `extra-build-dependencies` option is experimental and may change without warning. Pass `--preview-features extra-build-dependencies` 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
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"]
"#})?;
context.venv().arg("--clear").assert().success();
uv_snapshot!(context.filters(), context.sync(), @r"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
warning: The `extra-build-dependencies` option is experimental and may change without warning. Pass `--preview-features extra-build-dependencies` to disable this warning.
Resolved [N] packages in [TIME]
Prepared [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)
# 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(&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 succeed, as `anyio` is provided as a source
uv_snapshot!(context.filters(), context.sync(), @r"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
warning: The `extra-build-dependencies` option is experimental and may change without warning. Pass `--preview-features extra-build-dependencies` to disable this warning.
Resolved [N] packages in [TIME]
Prepared [N] packages in [TIME]
Installed [N] packages in [TIME]
+ 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(())
}
#[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");
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.portable_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"
success: false
exit_code: 1
----- stdout -----
----- stderr -----
warning: The `extra-build-dependencies` option is experimental and may change without warning. Pass `--preview-features extra-build-dependencies` 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 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`
");
Ok(())
}
/// Avoid using incompatible versions for build dependencies that are also part of the resolved /// 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 /// 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 /// compatibility. So, if we reuse the resolver state to install, and the install itself has to
@ -4198,6 +4593,187 @@ fn no_install_project_no_build() -> Result<()> {
Ok(()) 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::<Vec<_>>();
// 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.portable_display()
})?;
let filters = context
.filters()
.into_iter()
.chain(vec![(
r"environments-v2/script-[a-z0-9]+",
"environments-v2/script-[HASH]",
)])
.collect::<Vec<_>>();
// 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] #[test]
fn virtual_no_build() -> Result<()> { fn virtual_no_build() -> Result<()> {
let context = TestContext::new("3.12"); let context = TestContext::new("3.12");

View File

@ -202,6 +202,28 @@ environments = ["sys_platform == 'darwin'"]
--- ---
### [`extra-build-dependencies`](#extra-build-dependencies) {: #extra-build-dependencies }
Additional build dependencies for packages.
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**: `[]`
**Type**: `dict`
**Example usage**:
```toml title="pyproject.toml"
[tool.uv.extra-build-dependencies]
pytest = ["pip"]
```
---
### [`index`](#index) {: #index } ### [`index`](#index) {: #index }
The indexes to use when resolving dependencies. The indexes to use when resolving dependencies.
@ -1135,6 +1157,36 @@ behave consistently across timezones.
--- ---
### [`extra-build-dependencies`](#extra-build-dependencies) {: #extra-build-dependencies }
Additional build dependencies for packages.
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**: `[]`
**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-index-url`](#extra-index-url) {: #extra-index-url }
Extra URLs of package indexes to use, in addition to `--index-url`. Extra URLs of package indexes to use, in addition to `--index-url`.
@ -2596,6 +2648,38 @@ Only applies to `pyproject.toml`, `setup.py`, and `setup.cfg` sources.
--- ---
#### [`extra-build-dependencies`](#pip_extra-build-dependencies) {: #pip_extra-build-dependencies }
<span id="extra-build-dependencies"></span>
Additional build dependencies for packages.
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**: `[]`
**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 } #### [`extra-index-url`](#pip_extra-index-url) {: #pip_extra-index-url }
<span id="extra-index-url"></span> <span id="extra-index-url"></span>

View File

@ -0,0 +1,2 @@
# This is a local dummy anyio package
LOCAL_ANYIO_MARKER = True

33
uv.schema.json generated
View File

@ -225,6 +225,17 @@
} }
] ]
}, },
"extra-build-dependencies": {
"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"
},
{
"type": "null"
}
]
},
"extra-index-url": { "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.)", "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": [ "type": [
@ -1312,6 +1323,19 @@
"$ref": "#/definitions/ExtraName" "$ref": "#/definitions/ExtraName"
} }
}, },
"extra-build-dependencies": {
"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"
],
"additionalProperties": {
"type": "array",
"items": {
"$ref": "#/definitions/Requirement"
}
}
},
"extra-index-url": { "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).", "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": [ "type": [
@ -2341,6 +2365,15 @@
"$ref": "#/definitions/DependencyGroupSettings" "$ref": "#/definitions/DependencyGroupSettings"
} }
}, },
"ToolUvExtraBuildDependencies": {
"type": "object",
"additionalProperties": {
"type": "array",
"items": {
"$ref": "#/definitions/Requirement"
}
}
},
"ToolUvSources": { "ToolUvSources": {
"type": "object", "type": "object",
"additionalProperties": { "additionalProperties": {