Add `extra-build-dependencies` (#14735)

Replaces https://github.com/astral-sh/uv/pull/14092

Adds `tool.uv.extra-build-dependencies = {package = [dependency, ...]}`
which extends `build-system.requires` during package builds.

These are lowered via workspace sources, are applied to transitive
dependencies, and are included in the wheel cache shard hash.

There are some features we need to follow-up on, but are out of scope
here:

- Preferring locked versions for build dependencies
- Settings for requiring locked versions for build depencies

There are some quality of life follow-ups we should also do:

- Warn on `extra-build-dependencies` that do not apply to any packages
- Add test cases and improve error messaging when the
`extra-build-dependencies` resolve fails


-------

There ~are~ were a few open decisions to be made here

1. Should we resolve these dependencies alongside the
`build-system.requires` dependencies? Or should we resolve separately?
(I think the latter is more powerful? because you can override things?
but it opens the door to breaking your build)
2. Should we install these dependencies into the same environment? Or
should we layer it on top as we do elsewhere? (I think it's fine to
install into the same environment)
3. Should we respect sources defined in the parent project? (I think
yes, but then we need to lower the dependencies earlier — I don't think
that's a big deal, but it's not implemented)
4. Should we respect sources defined in the child project? (I think no,
this gets really complicated and seems weird to allow)
5. Should we apply this to transitive dependencies? (I think so)

---------

Co-authored-by: Aria Desires <aria.desires@gmail.com>
Co-authored-by: konstin <konstin@mailbox.org>
This commit is contained in:
Zanie Blue 2025-07-30 09:53:07 -05:00 committed by GitHub
parent 17f0c91896
commit 6856a27711
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
46 changed files with 1744 additions and 194 deletions

4
Cargo.lock generated
View File

@ -5516,6 +5516,7 @@ dependencies = [
"tracing",
"unicode-width 0.2.1",
"unscanny",
"uv-cache-key",
"version-ranges",
]
@ -5539,6 +5540,7 @@ dependencies = [
"tracing-test",
"unicode-width 0.2.1",
"url",
"uv-cache-key",
"uv-fs",
"uv-normalize",
"uv-pep440",
@ -5864,6 +5866,8 @@ dependencies = [
"thiserror 2.0.12",
"toml",
"url",
"uv-configuration",
"uv-distribution-types",
"uv-pep440",
"uv-pep508",
"uv-pypi-types",

View File

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

View File

@ -4,6 +4,7 @@
mod error;
use std::borrow::Cow;
use std::ffi::OsString;
use std::fmt::Formatter;
use std::fmt::Write;
@ -42,6 +43,7 @@ use uv_static::EnvVars;
use uv_types::{AnyErrorBuild, BuildContext, BuildIsolation, BuildStack, SourceBuildTrait};
use uv_warnings::warn_user_once;
use uv_workspace::WorkspaceCache;
use uv_workspace::pyproject::ExtraBuildDependencies;
pub use crate::error::{Error, MissingHeaderCause};
@ -281,6 +283,7 @@ impl SourceBuild {
workspace_cache: &WorkspaceCache,
config_settings: ConfigSettings,
build_isolation: BuildIsolation<'_>,
extra_build_dependencies: &ExtraBuildDependencies,
build_stack: &BuildStack,
build_kind: BuildKind,
mut environment_variables: FxHashMap<OsString, OsString>,
@ -297,7 +300,6 @@ impl SourceBuild {
};
let default_backend: Pep517Backend = DEFAULT_BACKEND.clone();
// Check if we have a PEP 517 build backend.
let (pep517_backend, project) = Self::extract_pep517_backend(
&source_tree,
@ -322,6 +324,14 @@ impl SourceBuild {
.or(fallback_package_version)
.cloned();
let extra_build_dependencies: Vec<Requirement> = package_name
.as_ref()
.and_then(|name| extra_build_dependencies.get(name).cloned())
.unwrap_or_default()
.into_iter()
.map(Requirement::from)
.collect();
// Create a virtual environment, or install into the shared environment if requested.
let venv = if let Some(venv) = build_isolation.shared_environment(package_name.as_ref()) {
venv.clone()
@ -344,11 +354,18 @@ impl SourceBuild {
if build_isolation.is_isolated(package_name.as_ref()) {
debug!("Resolving build requirements");
let dependency_sources = if extra_build_dependencies.is_empty() {
"`build-system.requires`"
} else {
"`build-system.requires` and `extra-build-dependencies`"
};
let resolved_requirements = Self::get_resolved_requirements(
build_context,
source_build_context,
&default_backend,
&pep517_backend,
extra_build_dependencies,
build_stack,
)
.await?;
@ -356,7 +373,7 @@ impl SourceBuild {
build_context
.install(&resolved_requirements, &venv, build_stack)
.await
.map_err(|err| Error::RequirementsInstall("`build-system.requires`", err.into()))?;
.map_err(|err| Error::RequirementsInstall(dependency_sources, err.into()))?;
} else {
debug!("Proceeding without build isolation");
}
@ -471,10 +488,13 @@ impl SourceBuild {
source_build_context: SourceBuildContext,
default_backend: &Pep517Backend,
pep517_backend: &Pep517Backend,
extra_build_dependencies: Vec<Requirement>,
build_stack: &BuildStack,
) -> Result<Resolution, Error> {
Ok(
if pep517_backend.requirements == default_backend.requirements {
if pep517_backend.requirements == default_backend.requirements
&& extra_build_dependencies.is_empty()
{
let mut resolution = source_build_context.default_resolution.lock().await;
if let Some(resolved_requirements) = &*resolution {
resolved_requirements.clone()
@ -489,12 +509,25 @@ impl SourceBuild {
resolved_requirements
}
} else {
let (requirements, dependency_sources) = if extra_build_dependencies.is_empty() {
(
Cow::Borrowed(&pep517_backend.requirements),
"`build-system.requires`",
)
} 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),
"`build-system.requires` and `extra-build-dependencies`",
)
};
build_context
.resolve(&pep517_backend.requirements, build_stack)
.resolve(&requirements, build_stack)
.await
.map_err(|err| {
Error::RequirementsResolve("`build-system.requires`", err.into())
})?
.map_err(|err| Error::RequirementsResolve(dependency_sources, err.into()))?
},
)
}
@ -604,6 +637,7 @@ impl SourceBuild {
);
}
}
default_backend.clone()
};
Ok((backend, pyproject_toml.project))

View File

@ -354,6 +354,7 @@ pub fn resolver_options(
}),
no_build_isolation: flag(no_build_isolation, build_isolation, "build-isolation"),
no_build_isolation_package: Some(no_build_isolation_package),
extra_build_dependencies: None,
exclude_newer: ExcludeNewer::from_args(
exclude_newer,
exclude_newer_package.unwrap_or_default(),
@ -475,6 +476,7 @@ pub fn resolver_installer_options(
} else {
Some(no_build_isolation_package)
},
extra_build_dependencies: None,
exclude_newer,
exclude_newer_package: exclude_newer_package.map(ExcludeNewerPackage::from_iter),
link_mode,

View File

@ -14,6 +14,7 @@ bitflags::bitflags! {
const JSON_OUTPUT = 1 << 2;
const PYLOCK = 1 << 3;
const ADD_BOUNDS = 1 << 4;
const EXTRA_BUILD_DEPENDENCIES = 1 << 5;
}
}
@ -28,6 +29,7 @@ impl PreviewFeatures {
Self::JSON_OUTPUT => "json-output",
Self::PYLOCK => "pylock",
Self::ADD_BOUNDS => "add-bounds",
Self::EXTRA_BUILD_DEPENDENCIES => "extra-build-dependencies",
_ => panic!("`flag_as_str` can only be used for exactly one feature flag"),
}
}
@ -70,6 +72,7 @@ impl FromStr for PreviewFeatures {
"json-output" => Self::JSON_OUTPUT,
"pylock" => Self::PYLOCK,
"add-bounds" => Self::ADD_BOUNDS,
"extra-build-dependencies" => Self::EXTRA_BUILD_DEPENDENCIES,
_ => {
warn_user_once!("Unknown preview feature: `{part}`");
continue;
@ -232,6 +235,10 @@ mod tests {
assert_eq!(PreviewFeatures::JSON_OUTPUT.flag_as_str(), "json-output");
assert_eq!(PreviewFeatures::PYLOCK.flag_as_str(), "pylock");
assert_eq!(PreviewFeatures::ADD_BOUNDS.flag_as_str(), "add-bounds");
assert_eq!(
PreviewFeatures::EXTRA_BUILD_DEPENDENCIES.flag_as_str(),
"extra-build-dependencies"
);
}
#[test]

View File

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

View File

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

View File

@ -4,7 +4,8 @@ use std::path::Path;
use uv_configuration::SourceStrategy;
use uv_distribution_types::{IndexLocations, Requirement};
use uv_normalize::PackageName;
use uv_workspace::pyproject::ToolUvSources;
use uv_pypi_types::VerbatimParsedUrl;
use uv_workspace::pyproject::{ExtraBuildDependencies, ToolUvSources};
use uv_workspace::{
DiscoveryOptions, MemberDiscovery, ProjectWorkspace, Workspace, WorkspaceCache,
};
@ -203,3 +204,93 @@ impl BuildRequires {
})
}
}
/// Lowered extra build dependencies with source resolution applied.
#[derive(Debug, Clone, Default)]
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::{WorkspaceCache, WorkspaceError};
pub use crate::metadata::build_requires::BuildRequires;
pub use crate::metadata::build_requires::{BuildRequires, ExtraBuildRequires};
pub use crate::metadata::dependency_groups::SourcedDependencyGroups;
pub use crate::metadata::lowering::LoweredRequirement;
pub use crate::metadata::lowering::LoweringError;

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.
async fn url<'data>(
&self,
@ -438,12 +452,13 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> {
let cache_shard = cache_shard.shard(revision.id());
let source_dist_entry = cache_shard.entry(SOURCE);
// If there are build settings, we need to scope to a cache shard.
// If there are build settings or extra build dependencies, we need to scope to a cache shard.
let config_settings = self.config_settings_for(source.name());
let cache_shard = if config_settings.is_empty() {
let extra_build_deps = self.extra_build_dependencies_for(source.name());
let cache_shard = if config_settings.is_empty() && extra_build_deps.is_empty() {
cache_shard
} else {
cache_shard.shard(cache_digest(&&config_settings))
cache_shard.shard(cache_digest(&(&config_settings, extra_build_deps)))
};
// If the cache contains a compatible wheel, return it.
@ -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 cache_shard = if config_settings.is_empty() {
let extra_build_deps = self.extra_build_dependencies_for(source.name());
let cache_shard = if config_settings.is_empty() && extra_build_deps.is_empty() {
cache_shard
} else {
cache_shard.shard(cache_digest(&config_settings))
cache_shard.shard(cache_digest(&(&config_settings, extra_build_deps)))
};
// Otherwise, we either need to build the metadata.
@ -827,12 +843,13 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> {
let cache_shard = cache_shard.shard(revision.id());
let source_entry = cache_shard.entry(SOURCE);
// If there are build settings, we need to scope to a cache shard.
// If there are build settings or extra build dependencies, we need to scope to a cache shard.
let config_settings = self.config_settings_for(source.name());
let cache_shard = if config_settings.is_empty() {
let extra_build_deps = self.extra_build_dependencies_for(source.name());
let cache_shard = if config_settings.is_empty() && extra_build_deps.is_empty() {
cache_shard
} else {
cache_shard.shard(cache_digest(&config_settings))
cache_shard.shard(cache_digest(&(&config_settings, extra_build_deps)))
};
// If the cache contains a compatible wheel, return it.
@ -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 cache_shard = if config_settings.is_empty() {
let extra_build_deps = self.extra_build_dependencies_for(source.name());
let cache_shard = if config_settings.is_empty() && extra_build_deps.is_empty() {
cache_shard
} else {
cache_shard.shard(cache_digest(&config_settings))
cache_shard.shard(cache_digest(&(&config_settings, extra_build_deps)))
};
// Otherwise, we need to build a wheel.
@ -1131,12 +1149,13 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> {
// freshness, since entries have to be fresher than the revision itself.
let cache_shard = cache_shard.shard(revision.id());
// If there are build settings, we need to scope to a cache shard.
// If there are build settings or extra build dependencies, we need to scope to a cache shard.
let config_settings = self.config_settings_for(source.name());
let cache_shard = if config_settings.is_empty() {
let extra_build_deps = self.extra_build_dependencies_for(source.name());
let cache_shard = if config_settings.is_empty() && extra_build_deps.is_empty() {
cache_shard
} else {
cache_shard.shard(cache_digest(&config_settings))
cache_shard.shard(cache_digest(&(&config_settings, extra_build_deps)))
};
// If the cache contains a compatible wheel, return it.
@ -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 cache_shard = if config_settings.is_empty() {
let extra_build_deps = self.extra_build_dependencies_for(source.name());
let cache_shard = if config_settings.is_empty() && extra_build_deps.is_empty() {
cache_shard
} else {
cache_shard.shard(cache_digest(&config_settings))
cache_shard.shard(cache_digest(&(&config_settings, extra_build_deps)))
};
// Otherwise, we need to build a wheel.
@ -1524,12 +1544,13 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> {
// Acquire the advisory lock.
let _lock = cache_shard.lock().await.map_err(Error::CacheWrite)?;
// If there are build settings, we need to scope to a cache shard.
// If there are build settings or extra build dependencies, we need to scope to a cache shard.
let config_settings = self.config_settings_for(source.name());
let cache_shard = if config_settings.is_empty() {
let extra_build_deps = self.extra_build_dependencies_for(source.name());
let cache_shard = if config_settings.is_empty() && extra_build_deps.is_empty() {
cache_shard
} else {
cache_shard.shard(cache_digest(&config_settings))
cache_shard.shard(cache_digest(&(&config_settings, extra_build_deps)))
};
// If the cache contains a compatible wheel, return it.
@ -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 cache_shard = if config_settings.is_empty() {
let extra_build_deps = self.extra_build_dependencies_for(source.name());
let cache_shard = if config_settings.is_empty() && extra_build_deps.is_empty() {
cache_shard
} else {
cache_shard.shard(cache_digest(&config_settings))
cache_shard.shard(cache_digest(&(&config_settings, extra_build_deps)))
};
// Otherwise, we need to build a wheel.

View File

@ -20,6 +20,7 @@ serde = { workspace = true, features = ["derive"] }
tracing = { workspace = true, optional = true }
unicode-width = { workspace = true }
unscanny = { workspace = true }
uv-cache-key = { workspace = true }
# Adds conversions from [`VersionSpecifiers`] to [`version_ranges::Ranges`]
version-ranges = { workspace = true, optional = true }

View File

@ -10,6 +10,7 @@ use std::{
str::FromStr,
sync::Arc,
};
use uv_cache_key::{CacheKey, CacheKeyHasher};
/// One of `~=` `==` `!=` `<=` `>=` `<` `>` `===`
#[derive(Eq, Ord, PartialEq, PartialOrd, Debug, Hash, Clone, Copy)]
@ -114,6 +115,24 @@ impl Operator {
pub fn is_star(self) -> bool {
matches!(self, Self::EqualStar | Self::NotEqualStar)
}
/// Returns the string representation of this operator.
pub fn as_str(self) -> &'static str {
match self {
Self::Equal => "==",
// Beware, this doesn't print the star
Self::EqualStar => "==",
#[allow(deprecated)]
Self::ExactEqual => "===",
Self::NotEqual => "!=",
Self::NotEqualStar => "!=",
Self::TildeEqual => "~=",
Self::LessThan => "<",
Self::LessThanEqual => "<=",
Self::GreaterThan => ">",
Self::GreaterThanEqual => ">=",
}
}
}
impl FromStr for Operator {
@ -150,21 +169,7 @@ impl FromStr for Operator {
impl std::fmt::Display for Operator {
/// Note the `EqualStar` is also `==`.
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let operator = match self {
Self::Equal => "==",
// Beware, this doesn't print the star
Self::EqualStar => "==",
#[allow(deprecated)]
Self::ExactEqual => "===",
Self::NotEqual => "!=",
Self::NotEqualStar => "!=",
Self::TildeEqual => "~=",
Self::LessThan => "<",
Self::LessThanEqual => "<=",
Self::GreaterThan => ">",
Self::GreaterThanEqual => ">=",
};
let operator = self.as_str();
write!(f, "{operator}")
}
}
@ -930,6 +935,46 @@ impl Hash for Version {
}
}
impl CacheKey for Version {
fn cache_key(&self, state: &mut CacheKeyHasher) {
self.epoch().cache_key(state);
let release = self.release();
release.len().cache_key(state);
for segment in release.iter() {
segment.cache_key(state);
}
if let Some(pre) = self.pre() {
1u8.cache_key(state);
match pre.kind {
PrereleaseKind::Alpha => 0u8.cache_key(state),
PrereleaseKind::Beta => 1u8.cache_key(state),
PrereleaseKind::Rc => 2u8.cache_key(state),
}
pre.number.cache_key(state);
} else {
0u8.cache_key(state);
}
if let Some(post) = self.post() {
1u8.cache_key(state);
post.cache_key(state);
} else {
0u8.cache_key(state);
}
if let Some(dev) = self.dev() {
1u8.cache_key(state);
dev.cache_key(state);
} else {
0u8.cache_key(state);
}
self.local().cache_key(state);
}
}
impl PartialOrd<Self> for Version {
#[inline]
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
@ -1711,6 +1756,23 @@ impl std::fmt::Display for LocalVersionSlice<'_> {
}
}
impl CacheKey for LocalVersionSlice<'_> {
fn cache_key(&self, state: &mut CacheKeyHasher) {
match self {
LocalVersionSlice::Segments(segments) => {
0u8.cache_key(state);
segments.len().cache_key(state);
for segment in *segments {
segment.cache_key(state);
}
}
LocalVersionSlice::Max => {
1u8.cache_key(state);
}
}
}
}
impl PartialOrd for LocalVersionSlice<'_> {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
@ -1777,6 +1839,21 @@ impl std::fmt::Display for LocalSegment {
}
}
impl CacheKey for LocalSegment {
fn cache_key(&self, state: &mut CacheKeyHasher) {
match self {
Self::String(string) => {
0u8.cache_key(state);
string.cache_key(state);
}
Self::Number(number) => {
1u8.cache_key(state);
number.cache_key(state);
}
}
}
}
impl PartialOrd for LocalSegment {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))

View File

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

View File

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

View File

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

View File

@ -11,6 +11,8 @@ doctest = false
workspace = true
[dependencies]
uv-configuration = { workspace = true }
uv-distribution-types = { workspace = true }
uv-pep440 = { workspace = true }
uv-pep508 = { workspace = true }
uv-pypi-types = { workspace = true }

View File

@ -9,6 +9,7 @@ use serde::Deserialize;
use thiserror::Error;
use url::Url;
use uv_configuration::SourceStrategy;
use uv_pep440::VersionSpecifiers;
use uv_pep508::PackageName;
use uv_pypi_types::VerbatimParsedUrl;
@ -96,6 +97,46 @@ impl Pep723ItemRef<'_> {
Self::Remote(..) => None,
}
}
/// Determine the working directory for the script.
pub fn directory(&self) -> Result<PathBuf, io::Error> {
match self {
Self::Script(script) => Ok(std::path::absolute(&script.path)?
.parent()
.expect("script path has no parent")
.to_owned()),
Self::Stdin(..) | Self::Remote(..) => std::env::current_dir(),
}
}
/// Collect any `tool.uv.index` from the script.
pub fn indexes(&self, source_strategy: SourceStrategy) -> &[uv_distribution_types::Index] {
match source_strategy {
SourceStrategy::Enabled => self
.metadata()
.tool
.as_ref()
.and_then(|tool| tool.uv.as_ref())
.and_then(|uv| uv.top_level.index.as_deref())
.unwrap_or(&[]),
SourceStrategy::Disabled => &[],
}
}
/// Collect any `tool.uv.sources` from the script.
pub fn sources(&self, source_strategy: SourceStrategy) -> &BTreeMap<PackageName, Sources> {
static EMPTY: BTreeMap<PackageName, Sources> = BTreeMap::new();
match source_strategy {
SourceStrategy::Enabled => self
.metadata()
.tool
.as_ref()
.and_then(|tool| tool.uv.as_ref())
.and_then(|uv| uv.sources.as_ref())
.unwrap_or(&EMPTY),
SourceStrategy::Disabled => &EMPTY,
}
}
}
impl<'item> From<&'item Pep723Item> for Pep723ItemRef<'item> {
@ -108,6 +149,12 @@ impl<'item> From<&'item Pep723Item> for Pep723ItemRef<'item> {
}
}
impl<'item> From<&'item Pep723Script> for Pep723ItemRef<'item> {
fn from(script: &'item Pep723Script) -> Self {
Self::Script(script)
}
}
/// A PEP 723 script, including its [`Pep723Metadata`].
#[derive(Debug, Clone)]
pub struct Pep723Script {
@ -381,6 +428,8 @@ pub struct ToolUv {
pub override_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 extra_build_dependencies:
Option<BTreeMap<PackageName, Vec<uv_pep508::Requirement<VerbatimParsedUrl>>>>,
pub sources: Option<BTreeMap<PackageName, Sources>>,
}

View File

@ -1,5 +1,5 @@
use std::num::NonZeroUsize;
use std::path::PathBuf;
use std::{collections::BTreeMap, num::NonZeroUsize};
use url::Url;
@ -17,6 +17,7 @@ use uv_resolver::{
PrereleaseMode, ResolutionMode,
};
use uv_torch::TorchMode;
use uv_workspace::pyproject::ExtraBuildDependencies;
use uv_workspace::pyproject_mut::AddBoundsKind;
use crate::{FilesystemOptions, Options, PipOptions};
@ -124,6 +125,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<ExcludeNewerPackage> {
/// Combine two [`ExcludeNewerPackage`] instances by merging them, with the values in `self` taking precedence.
fn combine(self, other: Option<ExcludeNewerPackage>) -> Option<ExcludeNewerPackage> {
@ -192,3 +208,30 @@ impl Combine for ExcludeNewer {
self
}
}
impl Combine for ExtraBuildDependencies {
fn combine(mut self, other: Self) -> Self {
for (key, value) in other {
match self.entry(key) {
std::collections::btree_map::Entry::Occupied(mut entry) => {
// Combine the vecs, with self taking precedence
let existing = entry.get_mut();
existing.extend(value);
}
std::collections::btree_map::Entry::Vacant(entry) => {
entry.insert(value);
}
}
}
self
}
}
impl Combine for Option<ExtraBuildDependencies> {
fn combine(self, other: Option<ExtraBuildDependencies>) -> Option<ExtraBuildDependencies> {
match (self, other) {
(Some(a), Some(b)) => Some(a.combine(b)),
(a, b) => a.or(b),
}
}
}

View File

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

View File

@ -24,7 +24,7 @@ use uv_resolver::{
};
use uv_static::EnvVars;
use uv_torch::TorchMode;
use uv_workspace::pyproject_mut::AddBoundsKind;
use uv_workspace::{pyproject::ExtraBuildDependencies, pyproject_mut::AddBoundsKind};
/// A `pyproject.toml` with an (optional) `[tool.uv]` section.
#[allow(dead_code)]
@ -376,6 +376,7 @@ pub struct ResolverOptions {
pub no_binary_package: Option<Vec<PackageName>>,
pub no_build_isolation: Option<bool>,
pub no_build_isolation_package: Option<Vec<PackageName>>,
pub extra_build_dependencies: Option<ExtraBuildDependencies>,
pub no_sources: Option<bool>,
}
@ -628,6 +629,20 @@ pub struct ResolverInstallerOptions {
"#
)]
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.
///
/// Accepts a superset of [RFC 3339](https://www.rfc-editor.org/rfc/rfc3339.html) (e.g.,
@ -1135,6 +1150,20 @@ pub struct PipOptions {
"#
)]
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
/// issues.
#[option(
@ -1719,6 +1748,7 @@ impl From<ResolverInstallerOptions> for ResolverOptions {
no_binary_package: value.no_binary_package,
no_build_isolation: value.no_build_isolation,
no_build_isolation_package: value.no_build_isolation_package,
extra_build_dependencies: value.extra_build_dependencies,
no_sources: value.no_sources,
}
}
@ -1784,6 +1814,7 @@ pub struct ToolOptions {
pub config_settings_package: Option<PackageConfigSettings>,
pub no_build_isolation: Option<bool>,
pub no_build_isolation_package: Option<Vec<PackageName>>,
pub extra_build_dependencies: Option<ExtraBuildDependencies>,
pub exclude_newer: Option<ExcludeNewerTimestamp>,
pub exclude_newer_package: Option<ExcludeNewerPackage>,
pub link_mode: Option<LinkMode>,
@ -1813,6 +1844,7 @@ impl From<ResolverInstallerOptions> for ToolOptions {
config_settings_package: value.config_settings_package,
no_build_isolation: value.no_build_isolation,
no_build_isolation_package: value.no_build_isolation_package,
extra_build_dependencies: value.extra_build_dependencies,
exclude_newer: value.exclude_newer,
exclude_newer_package: value.exclude_newer_package,
link_mode: value.link_mode,
@ -1844,6 +1876,7 @@ impl From<ToolOptions> for ResolverInstallerOptions {
config_settings_package: value.config_settings_package,
no_build_isolation: value.no_build_isolation,
no_build_isolation_package: value.no_build_isolation_package,
extra_build_dependencies: value.extra_build_dependencies,
exclude_newer: value.exclude_newer,
exclude_newer_package: value.exclude_newer_package,
link_mode: value.link_mode,
@ -1898,6 +1931,7 @@ pub struct OptionsWire {
config_settings_package: Option<PackageConfigSettings>,
no_build_isolation: Option<bool>,
no_build_isolation_package: Option<Vec<PackageName>>,
extra_build_dependencies: Option<ExtraBuildDependencies>,
exclude_newer: Option<ExcludeNewerTimestamp>,
exclude_newer_package: Option<ExcludeNewerPackage>,
link_mode: Option<LinkMode>,
@ -2017,6 +2051,7 @@ impl From<OptionsWire> for Options {
sources,
default_groups,
dependency_groups,
extra_build_dependencies,
dev_dependencies,
managed,
package,
@ -2057,6 +2092,7 @@ impl From<OptionsWire> for Options {
config_settings_package,
no_build_isolation,
no_build_isolation_package,
extra_build_dependencies,
exclude_newer,
exclude_newer_package,
link_mode,

View File

@ -101,6 +101,9 @@ pub trait BuildContext {
/// Workspace discovery caching.
fn workspace_cache(&self) -> &WorkspaceCache;
/// Get the extra build dependencies.
fn extra_build_dependencies(&self) -> &uv_workspace::pyproject::ExtraBuildDependencies;
/// Resolve the given requirements into a ready-to-install set of package versions.
fn resolve<'a>(
&'a self,

View File

@ -50,6 +50,55 @@ pub enum PyprojectTomlError {
MissingVersion,
}
/// Helper function to deserialize a map while ensuring all keys are unique.
fn deserialize_unique_map<'de, D, K, V, F>(
deserializer: D,
error_msg: F,
) -> Result<BTreeMap<K, V>, D::Error>
where
D: Deserializer<'de>,
K: Deserialize<'de> + Ord + std::fmt::Display,
V: Deserialize<'de>,
F: FnOnce(&K) -> String,
{
struct Visitor<K, V, F>(F, std::marker::PhantomData<(K, V)>);
impl<'de, K, V, F> serde::de::Visitor<'de> for Visitor<K, V, F>
where
K: Deserialize<'de> + Ord + std::fmt::Display,
V: Deserialize<'de>,
F: FnOnce(&K) -> String,
{
type Value = BTreeMap<K, V>;
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>,
{
use std::collections::btree_map::Entry;
let mut map = BTreeMap::new();
while let Some((key, value)) = access.next_entry::<K, V>()? {
match map.entry(key) {
Entry::Occupied(entry) => {
return Err(serde::de::Error::custom((self.0)(entry.key())));
}
Entry::Vacant(entry) => {
entry.insert(value);
}
}
}
Ok(map)
}
}
deserializer.deserialize_map(Visitor(error_msg, std::marker::PhantomData))
}
/// A `pyproject.toml` as specified in PEP 517.
#[derive(Deserialize, Debug, Clone)]
#[cfg_attr(test, derive(Serialize))]
@ -378,6 +427,21 @@ pub struct ToolUv {
)]
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<ExtraBuildDependencies>,
/// The project's development dependencies.
///
/// Development dependencies will be installed by default in `uv run` and `uv sync`, but will
@ -643,38 +707,10 @@ impl<'de> serde::de::Deserialize<'de> for ToolUvSources {
where
D: Deserializer<'de>,
{
struct SourcesVisitor;
impl<'de> serde::de::Visitor<'de> for SourcesVisitor {
type Value = ToolUvSources;
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 sources = BTreeMap::new();
while let Some((key, value)) = access.next_entry::<PackageName, Sources>()? {
match sources.entry(key) {
std::collections::btree_map::Entry::Occupied(entry) => {
return Err(serde::de::Error::custom(format!(
"duplicate sources for package `{}`",
entry.key()
)));
}
std::collections::btree_map::Entry::Vacant(entry) => {
entry.insert(value);
}
}
}
Ok(ToolUvSources(sources))
}
}
deserializer.deserialize_map(SourcesVisitor)
deserialize_unique_map(deserializer, |key: &PackageName| {
format!("duplicate sources for package `{key}`")
})
.map(ToolUvSources)
}
}
@ -702,40 +738,10 @@ impl<'de> serde::de::Deserialize<'de> for ToolUvDependencyGroups {
where
D: Deserializer<'de>,
{
struct SourcesVisitor;
impl<'de> serde::de::Visitor<'de> for SourcesVisitor {
type Value = ToolUvDependencyGroups;
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::<GroupName, DependencyGroupSettings>()?
{
match groups.entry(key) {
std::collections::btree_map::Entry::Occupied(entry) => {
return Err(serde::de::Error::custom(format!(
"duplicate settings for dependency group `{}`",
entry.key()
)));
}
std::collections::btree_map::Entry::Vacant(entry) => {
entry.insert(value);
}
}
}
Ok(ToolUvDependencyGroups(groups))
}
}
deserializer.deserialize_map(SourcesVisitor)
deserialize_unique_map(deserializer, |key: &GroupName| {
format!("duplicate settings for dependency group `{key}`")
})
.map(ToolUvDependencyGroups)
}
}
@ -749,6 +755,51 @@ pub struct DependencyGroupSettings {
pub requires_python: Option<VersionSpecifiers>,
}
#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct ExtraBuildDependencies(
BTreeMap<PackageName, Vec<uv_pep508::Requirement<VerbatimParsedUrl>>>,
);
impl std::ops::Deref for ExtraBuildDependencies {
type Target = BTreeMap<PackageName, Vec<uv_pep508::Requirement<VerbatimParsedUrl>>>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl std::ops::DerefMut for ExtraBuildDependencies {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl IntoIterator for ExtraBuildDependencies {
type Item = (PackageName, Vec<uv_pep508::Requirement<VerbatimParsedUrl>>);
type IntoIter = std::collections::btree_map::IntoIter<
PackageName,
Vec<uv_pep508::Requirement<VerbatimParsedUrl>>,
>;
fn into_iter(self) -> Self::IntoIter {
self.0.into_iter()
}
}
/// Ensure that all keys in the TOML table are unique.
impl<'de> serde::de::Deserialize<'de> for ExtraBuildDependencies {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
deserialize_unique_map(deserializer, |key: &PackageName| {
format!("duplicate extra-build-dependencies for `{key}`")
})
.map(ExtraBuildDependencies)
}
}
#[derive(Deserialize, OptionsMetadata, Default, Debug, Clone, PartialEq, Eq)]
#[cfg_attr(test, derive(Serialize))]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]

View File

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

View File

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

View File

@ -14,8 +14,8 @@ use uv_cache::Cache;
use uv_client::{BaseClientBuilder, FlatIndexClient, RegistryClientBuilder};
use uv_configuration::{
BuildOptions, Concurrency, ConfigSettings, Constraints, ExportFormat, ExtrasSpecification,
IndexStrategy, NoBinary, NoBuild, PackageConfigSettings, Preview, Reinstall, SourceStrategy,
Upgrade,
IndexStrategy, NoBinary, NoBuild, PackageConfigSettings, Preview, PreviewFeatures, Reinstall,
SourceStrategy, Upgrade,
};
use uv_configuration::{KeyringProviderType, TargetTriple};
use uv_dispatch::{BuildDispatch, SharedState};
@ -44,8 +44,9 @@ use uv_resolver::{
};
use uv_torch::{TorchMode, TorchStrategy};
use uv_types::{BuildIsolation, EmptyInstalledPackages, HashStrategy};
use uv_warnings::warn_user;
use uv_warnings::{warn_user, warn_user_once};
use uv_workspace::WorkspaceCache;
use uv_workspace::pyproject::ExtraBuildDependencies;
use crate::commands::pip::loggers::DefaultResolveLogger;
use crate::commands::pip::{operations, resolution_environment};
@ -95,6 +96,7 @@ pub(crate) async fn pip_compile(
config_settings_package: PackageConfigSettings,
no_build_isolation: bool,
no_build_isolation_package: Vec<PackageName>,
extra_build_dependencies: &ExtraBuildDependencies,
build_options: BuildOptions,
mut python_version: Option<PythonVersion>,
python_platform: Option<TargetTriple>,
@ -112,6 +114,15 @@ pub(crate) async fn pip_compile(
printer: Printer,
preview: Preview,
) -> 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
// error.
if output_file
@ -469,6 +480,8 @@ pub(crate) async fn pip_compile(
.map(|constraint| constraint.requirement.clone()),
);
let extra_build_requires =
uv_distribution::ExtraBuildRequires::from_lowered(extra_build_dependencies.clone());
let build_dispatch = BuildDispatch::new(
&client,
&cache,
@ -482,6 +495,7 @@ pub(crate) async fn pip_compile(
&config_settings,
&config_settings_package,
build_isolation,
&extra_build_requires,
link_mode,
&build_options,
&build_hashes,

View File

@ -36,8 +36,9 @@ use uv_resolver::{
};
use uv_torch::{TorchMode, TorchStrategy};
use uv_types::{BuildIsolation, HashStrategy};
use uv_warnings::warn_user;
use uv_warnings::{warn_user, warn_user_once};
use uv_workspace::WorkspaceCache;
use uv_workspace::pyproject::ExtraBuildDependencies;
use crate::commands::pip::loggers::{DefaultInstallLogger, DefaultResolveLogger, InstallLogger};
use crate::commands::pip::operations::Modifications;
@ -78,6 +79,7 @@ pub(crate) async fn pip_install(
config_settings_package: &PackageConfigSettings,
no_build_isolation: bool,
no_build_isolation_package: Vec<PackageName>,
extra_build_dependencies: &ExtraBuildDependencies,
build_options: BuildOptions,
modifications: Modifications,
python_version: Option<PythonVersion>,
@ -99,6 +101,15 @@ pub(crate) async fn pip_install(
) -> anyhow::Result<ExitStatus> {
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()
.retries_from_env()?
.connectivity(network_settings.connectivity)
@ -413,6 +424,8 @@ pub(crate) async fn pip_install(
let state = SharedState::default();
// Create a build dispatch.
let extra_build_requires =
uv_distribution::ExtraBuildRequires::from_lowered(extra_build_dependencies.clone());
let build_dispatch = BuildDispatch::new(
&client,
&cache,
@ -426,6 +439,7 @@ pub(crate) async fn pip_install(
config_settings,
config_settings_package,
build_isolation,
&extra_build_requires,
link_mode,
&build_options,
&build_hasher,

View File

@ -32,8 +32,9 @@ use uv_resolver::{
};
use uv_torch::{TorchMode, TorchStrategy};
use uv_types::{BuildIsolation, HashStrategy};
use uv_warnings::warn_user;
use uv_warnings::{warn_user, warn_user_once};
use uv_workspace::WorkspaceCache;
use uv_workspace::pyproject::ExtraBuildDependencies;
use crate::commands::pip::loggers::{DefaultInstallLogger, DefaultResolveLogger};
use crate::commands::pip::operations::Modifications;
@ -67,6 +68,7 @@ pub(crate) async fn pip_sync(
config_settings_package: &PackageConfigSettings,
no_build_isolation: bool,
no_build_isolation_package: Vec<PackageName>,
extra_build_dependencies: &ExtraBuildDependencies,
build_options: BuildOptions,
python_version: Option<PythonVersion>,
python_platform: Option<TargetTriple>,
@ -85,6 +87,15 @@ pub(crate) async fn pip_sync(
printer: Printer,
preview: Preview,
) -> 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()
.retries_from_env()?
.connectivity(network_settings.connectivity)
@ -348,6 +359,8 @@ pub(crate) async fn pip_sync(
let state = SharedState::default();
// Create a build dispatch.
let extra_build_requires =
uv_distribution::ExtraBuildRequires::from_lowered(extra_build_dependencies.clone());
let build_dispatch = BuildDispatch::new(
&client,
&cache,
@ -361,6 +374,7 @@ pub(crate) async fn pip_sync(
config_settings,
config_settings_package,
build_isolation,
&extra_build_requires,
link_mode,
&build_options,
&build_hasher,

View File

@ -37,7 +37,7 @@ use uv_python::{Interpreter, PythonDownloads, PythonEnvironment, PythonPreferenc
use uv_redacted::DisplaySafeUrl;
use uv_requirements::{NamedRequirementsResolver, RequirementsSource, RequirementsSpecification};
use uv_resolver::FlatIndex;
use uv_scripts::{Pep723ItemRef, Pep723Metadata, Pep723Script};
use uv_scripts::{Pep723Metadata, Pep723Script};
use uv_settings::PythonInstallMirrors;
use uv_types::{BuildIsolation, HashStrategy};
use uv_warnings::warn_user_once;
@ -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 {
match source {
RequirementsSource::PyprojectToml(_) => {
@ -212,7 +221,7 @@ pub(crate) async fn add(
// Discover the interpreter.
let interpreter = ScriptInterpreter::discover(
Pep723ItemRef::Script(&script),
(&script).into(),
python.as_deref().map(PythonRequest::parse),
&network_settings,
python_preference,
@ -428,6 +437,18 @@ pub(crate) async fn add(
};
// Create a build dispatch.
let extra_build_requires = if let AddTarget::Project(project, _) = &target {
uv_distribution::ExtraBuildRequires::from_workspace(
settings.resolver.extra_build_dependencies.clone(),
project.workspace(),
&settings.resolver.index_locations,
settings.resolver.sources,
)?
} else {
uv_distribution::ExtraBuildRequires::from_lowered(
settings.resolver.extra_build_dependencies.clone(),
)
};
let build_dispatch = BuildDispatch::new(
&client,
cache,
@ -441,6 +462,7 @@ pub(crate) async fn add(
&settings.resolver.config_setting,
&settings.resolver.config_settings_package,
build_isolation,
&extra_build_requires,
settings.resolver.link_mode,
&settings.resolver.build_options,
&build_hasher,

View File

@ -15,7 +15,7 @@ use uv_normalize::{DefaultExtras, DefaultGroups, PackageName};
use uv_python::{PythonDownloads, PythonPreference, PythonRequest};
use uv_requirements::is_pylock_toml;
use uv_resolver::{PylockToml, RequirementsTxtExport};
use uv_scripts::{Pep723ItemRef, Pep723Script};
use uv_scripts::Pep723Script;
use uv_settings::PythonInstallMirrors;
use uv_workspace::{DiscoveryOptions, MemberDiscovery, VirtualProject, Workspace, WorkspaceCache};
@ -132,7 +132,7 @@ pub(crate) async fn export(
} else {
Some(match &target {
ExportTarget::Script(script) => ScriptInterpreter::discover(
Pep723ItemRef::Script(script),
script.into(),
python.as_deref().map(PythonRequest::parse),
&network_settings,
python_preference,

View File

@ -13,7 +13,7 @@ use uv_cache::Cache;
use uv_client::{BaseClientBuilder, FlatIndexClient, RegistryClientBuilder};
use uv_configuration::{
Concurrency, Constraints, DependencyGroupsWithDefaults, DryRun, ExtrasSpecification, Preview,
Reinstall, Upgrade,
PreviewFeatures, Reinstall, Upgrade,
};
use uv_dispatch::BuildDispatch;
use uv_distribution::DistributionDatabase;
@ -32,7 +32,7 @@ use uv_resolver::{
FlatIndex, InMemoryIndex, Lock, Options, OptionsBuilder, PythonRequirement,
ResolverEnvironment, ResolverManifest, SatisfiesResult, UniversalMarker,
};
use uv_scripts::{Pep723ItemRef, Pep723Script};
use uv_scripts::Pep723Script;
use uv_settings::PythonInstallMirrors;
use uv_types::{BuildContext, BuildIsolation, EmptyInstalledPackages, HashStrategy};
use uv_warnings::{warn_user, warn_user_once};
@ -42,7 +42,7 @@ use crate::commands::pip::loggers::{DefaultResolveLogger, ResolveLogger, Summary
use crate::commands::project::lock_target::LockTarget;
use crate::commands::project::{
ProjectError, ProjectInterpreter, ScriptInterpreter, UniversalState,
init_script_python_requirement,
init_script_python_requirement, script_extra_build_requires,
};
use crate::commands::reporters::{PythonDownloadReporter, ResolverReporter};
use crate::commands::{ExitStatus, ScriptPath, diagnostics, pip};
@ -162,7 +162,7 @@ pub(crate) async fn lock(
.await?
.into_interpreter(),
LockTarget::Script(script) => ScriptInterpreter::discover(
Pep723ItemRef::Script(script),
script.into(),
python.as_deref().map(PythonRequest::parse),
&network_settings,
python_preference,
@ -435,6 +435,7 @@ async fn do_lock(
config_settings_package,
no_build_isolation,
no_build_isolation_package,
extra_build_dependencies,
exclude_newer,
link_mode,
upgrade,
@ -442,6 +443,15 @@ async fn do_lock(
sources,
} = 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
);
}
// Collect the requirements, etc.
let members = target.members();
let packages = target.packages();
@ -664,6 +674,18 @@ async fn do_lock(
};
// Create a build dispatch.
let extra_build_requires = match &target {
LockTarget::Workspace(workspace) => uv_distribution::ExtraBuildRequires::from_workspace(
extra_build_dependencies.clone(),
workspace,
index_locations,
*sources,
)?,
LockTarget::Script(script) => {
// Try to get extra build dependencies from the script metadata
script_extra_build_requires((*script).into(), settings)?
}
};
let build_dispatch = BuildDispatch::new(
&client,
cache,
@ -677,6 +699,7 @@ async fn do_lock(
config_setting,
config_settings_package,
build_isolation,
&extra_build_requires,
*link_mode,
build_options,
&build_hasher,

View File

@ -13,7 +13,7 @@ use uv_cache_key::cache_digest;
use uv_client::{BaseClientBuilder, FlatIndexClient, RegistryClientBuilder};
use uv_configuration::{
Concurrency, Constraints, DependencyGroupsWithDefaults, DryRun, ExtrasSpecification, Preview,
PreviewFeatures, Reinstall, SourceStrategy, Upgrade,
PreviewFeatures, Reinstall, Upgrade,
};
use uv_dispatch::{BuildDispatch, SharedState};
use uv_distribution::{DistributionDatabase, LoweredRequirement};
@ -46,6 +46,7 @@ use uv_types::{BuildIsolation, EmptyInstalledPackages, HashStrategy};
use uv_virtualenv::remove_virtualenv;
use uv_warnings::{warn_user, warn_user_once};
use uv_workspace::dependency_groups::DependencyGroupError;
use uv_workspace::pyproject::ExtraBuildDependencies;
use uv_workspace::pyproject::PyProjectToml;
use uv_workspace::{RequiresPythonSources, Workspace, WorkspaceCache};
@ -1692,6 +1693,7 @@ pub(crate) async fn resolve_names(
link_mode,
no_build_isolation,
no_build_isolation_package,
extra_build_dependencies,
prerelease: _,
resolution: _,
sources,
@ -1740,6 +1742,8 @@ pub(crate) async fn resolve_names(
let build_hasher = HashStrategy::default();
// Create a build dispatch.
let extra_build_requires =
uv_distribution::ExtraBuildRequires::from_lowered(extra_build_dependencies.clone());
let build_dispatch = BuildDispatch::new(
&client,
cache,
@ -1753,6 +1757,7 @@ pub(crate) async fn resolve_names(
config_setting,
config_settings_package,
build_isolation,
&extra_build_requires,
*link_mode,
build_options,
&build_hasher,
@ -1845,6 +1850,7 @@ pub(crate) async fn resolve_environment(
config_settings_package,
no_build_isolation,
no_build_isolation_package,
extra_build_dependencies,
exclude_newer,
link_mode,
upgrade: _,
@ -1948,6 +1954,8 @@ pub(crate) async fn resolve_environment(
let workspace_cache = WorkspaceCache::default();
// Create a build dispatch.
let extra_build_requires =
uv_distribution::ExtraBuildRequires::from_lowered(extra_build_dependencies.clone());
let resolve_dispatch = BuildDispatch::new(
&client,
cache,
@ -1961,6 +1969,7 @@ pub(crate) async fn resolve_environment(
config_setting,
config_settings_package,
build_isolation,
&extra_build_requires,
*link_mode,
build_options,
&build_hasher,
@ -2028,6 +2037,7 @@ pub(crate) async fn sync_environment(
config_settings_package,
no_build_isolation,
no_build_isolation_package,
extra_build_dependencies,
exclude_newer,
link_mode,
compile_bytecode,
@ -2086,6 +2096,8 @@ pub(crate) async fn sync_environment(
};
// Create a build dispatch.
let extra_build_requires =
uv_distribution::ExtraBuildRequires::from_lowered(extra_build_dependencies.clone());
let build_dispatch = BuildDispatch::new(
&client,
cache,
@ -2099,6 +2111,7 @@ pub(crate) async fn sync_environment(
config_setting,
config_settings_package,
build_isolation,
&extra_build_requires,
link_mode,
build_options,
&build_hasher,
@ -2164,6 +2177,7 @@ pub(crate) async fn update_environment(
spec: RequirementsSpecification,
modifications: Modifications,
build_constraints: Constraints,
extra_build_requires: uv_distribution::ExtraBuildRequires,
settings: &ResolverInstallerSettings,
network_settings: &NetworkSettings,
state: &SharedState,
@ -2194,6 +2208,7 @@ pub(crate) async fn update_environment(
link_mode,
no_build_isolation,
no_build_isolation_package,
extra_build_dependencies: _,
prerelease,
resolution,
sources,
@ -2323,6 +2338,7 @@ pub(crate) async fn update_environment(
config_setting,
config_settings_package,
build_isolation,
&extra_build_requires,
*link_mode,
build_options,
&build_hasher,
@ -2537,40 +2553,9 @@ pub(crate) fn script_specification(
return Ok(None);
};
// Determine the working directory for the script.
let script_dir = match &script {
Pep723ItemRef::Script(script) => std::path::absolute(&script.path)?
.parent()
.expect("script path has no parent")
.to_owned(),
Pep723ItemRef::Stdin(..) | Pep723ItemRef::Remote(..) => std::env::current_dir()?,
};
// Collect any `tool.uv.index` from the script.
let empty = Vec::default();
let script_indexes = match settings.sources {
SourceStrategy::Enabled => script
.metadata()
.tool
.as_ref()
.and_then(|tool| tool.uv.as_ref())
.and_then(|uv| uv.top_level.index.as_deref())
.unwrap_or(&empty),
SourceStrategy::Disabled => &empty,
};
// Collect any `tool.uv.sources` from the script.
let empty = BTreeMap::default();
let script_sources = match settings.sources {
SourceStrategy::Enabled => script
.metadata()
.tool
.as_ref()
.and_then(|tool| tool.uv.as_ref())
.and_then(|uv| uv.sources.as_ref())
.unwrap_or(&empty),
SourceStrategy::Disabled => &empty,
};
let script_dir = script.directory()?;
let script_indexes = script.indexes(settings.sources);
let script_sources = script.sources(settings.sources);
let requirements = dependencies
.iter()
@ -2634,6 +2619,51 @@ pub(crate) fn script_specification(
)))
}
/// Determine the extra build requires for a script.
#[allow(clippy::result_large_err)]
pub(crate) fn script_extra_build_requires(
script: Pep723ItemRef<'_>,
settings: &ResolverSettings,
) -> Result<uv_distribution::ExtraBuildRequires, ProjectError> {
let script_dir = script.directory()?;
let script_indexes = script.indexes(settings.sources);
let script_sources = script.sources(settings.sources);
// Collect any `tool.uv.extra-build-dependencies` from the script.
let empty = BTreeMap::default();
let script_extra_build_dependencies = script
.metadata()
.tool
.as_ref()
.and_then(|tool| tool.uv.as_ref())
.and_then(|uv| uv.extra_build_dependencies.as_ref())
.unwrap_or(&empty);
// Lower the extra build dependencies
let mut extra_build_dependencies = ExtraBuildDependencies::default();
for (name, requirements) in script_extra_build_dependencies {
let lowered_requirements: Vec<_> = requirements
.iter()
.cloned()
.flat_map(|requirement| {
LoweredRequirement::from_non_workspace_requirement(
requirement,
script_dir.as_ref(),
script_sources,
script_indexes,
&settings.index_locations,
)
.map_ok(|req| req.into_inner().into())
})
.collect::<Result<Vec<_>, _>>()?;
extra_build_dependencies.insert(name.clone(), lowered_requirements);
}
Ok(uv_distribution::ExtraBuildRequires::from_lowered(
extra_build_dependencies,
))
}
/// Warn if the user provides (e.g.) an `--index-url` in a requirements file.
fn warn_on_requirements_txt_setting(spec: &RequirementsSpecification, settings: &ResolverSettings) {
let RequirementsSpecification {

View File

@ -16,7 +16,7 @@ use uv_fs::Simplified;
use uv_normalize::{DEV_DEPENDENCIES, DefaultExtras, DefaultGroups};
use uv_pep508::PackageName;
use uv_python::{PythonDownloads, PythonPreference, PythonRequest};
use uv_scripts::{Pep723ItemRef, Pep723Metadata, Pep723Script};
use uv_scripts::{Pep723Metadata, Pep723Script};
use uv_settings::PythonInstallMirrors;
use uv_warnings::warn_user_once;
use uv_workspace::pyproject::DependencyType;
@ -261,7 +261,7 @@ pub(crate) async fn remove(
}
RemoveTarget::Script(script) => {
let interpreter = ScriptInterpreter::discover(
Pep723ItemRef::Script(&script),
(&script).into(),
python.as_deref().map(PythonRequest::parse),
&network_settings,
python_preference,

View File

@ -53,8 +53,8 @@ use crate::commands::project::lock_target::LockTarget;
use crate::commands::project::{
EnvironmentSpecification, PreferenceLocation, ProjectEnvironment, ProjectError,
ScriptEnvironment, ScriptInterpreter, UniversalState, WorkspacePython,
default_dependency_groups, script_specification, update_environment,
validate_project_requires_python,
default_dependency_groups, script_extra_build_requires, script_specification,
update_environment, validate_project_requires_python,
};
use crate::commands::reporters::PythonDownloadReporter;
use crate::commands::{ExitStatus, diagnostics, project};
@ -359,6 +359,8 @@ hint: If you are running a script with `{}` in the shebang, you may need to incl
// Install the script requirements, if necessary. Otherwise, use an isolated environment.
if let Some(spec) = script_specification((&script).into(), &settings.resolver)? {
let script_extra_build_requires =
script_extra_build_requires((&script).into(), &settings.resolver)?;
let environment = ScriptEnvironment::get_or_init(
(&script).into(),
python.as_deref().map(PythonRequest::parse),
@ -407,6 +409,7 @@ hint: If you are running a script with `{}` in the shebang, you may need to incl
spec,
modifications,
build_constraints.unwrap_or_default(),
script_extra_build_requires,
&settings,
&network_settings,
&sync_state,

View File

@ -14,7 +14,7 @@ use uv_client::{BaseClientBuilder, FlatIndexClient, RegistryClientBuilder};
use uv_configuration::{
Concurrency, Constraints, DependencyGroups, DependencyGroupsWithDefaults, DryRun, EditableMode,
ExtrasSpecification, ExtrasSpecificationWithDefaults, HashCheckingMode, InstallOptions,
Preview, PreviewFeatures, TargetTriple,
Preview, PreviewFeatures, TargetTriple, Upgrade,
};
use uv_dispatch::BuildDispatch;
use uv_distribution_types::{
@ -26,11 +26,11 @@ use uv_normalize::{DefaultExtras, DefaultGroups, PackageName};
use uv_pep508::{MarkerTree, VersionOrUrl};
use uv_pypi_types::{ParsedArchiveUrl, ParsedGitUrl, ParsedUrl};
use uv_python::{PythonDownloads, PythonEnvironment, PythonPreference, PythonRequest};
use uv_resolver::{FlatIndex, Installable, Lock};
use uv_scripts::{Pep723ItemRef, Pep723Script};
use uv_resolver::{FlatIndex, ForkStrategy, Installable, Lock, PrereleaseMode, ResolutionMode};
use uv_scripts::Pep723Script;
use uv_settings::PythonInstallMirrors;
use uv_types::{BuildIsolation, HashStrategy};
use uv_warnings::warn_user;
use uv_warnings::{warn_user, warn_user_once};
use uv_workspace::pyproject::Source;
use uv_workspace::{DiscoveryOptions, MemberDiscovery, VirtualProject, Workspace, WorkspaceCache};
@ -43,11 +43,14 @@ use crate::commands::project::lock::{LockMode, LockOperation, LockResult};
use crate::commands::project::lock_target::LockTarget;
use crate::commands::project::{
PlatformState, ProjectEnvironment, ProjectError, ScriptEnvironment, UniversalState,
default_dependency_groups, detect_conflicts, script_specification, update_environment,
default_dependency_groups, detect_conflicts, script_extra_build_requires, script_specification,
update_environment,
};
use crate::commands::{ExitStatus, diagnostics};
use crate::printer::Printer;
use crate::settings::{InstallerSettingsRef, NetworkSettings, ResolverInstallerSettings};
use crate::settings::{
InstallerSettingsRef, NetworkSettings, ResolverInstallerSettings, ResolverSettings,
};
/// Sync the project environment.
#[allow(clippy::fn_params_excessive_bools)]
@ -164,7 +167,7 @@ pub(crate) async fn sync(
),
SyncTarget::Script(script) => SyncEnvironment::Script(
ScriptEnvironment::get_or_init(
Pep723ItemRef::Script(script),
script.into(),
python.as_deref().map(PythonRequest::parse),
&network_settings,
python_preference,
@ -222,8 +225,9 @@ pub(crate) async fn sync(
}
// Parse the requirements from the script.
let spec = script_specification(Pep723ItemRef::Script(script), &settings.resolver)?
.unwrap_or_default();
let spec = script_specification(script.into(), &settings.resolver)?.unwrap_or_default();
let script_extra_build_requires =
script_extra_build_requires(script.into(), &settings.resolver)?;
// Parse the build constraints from the script.
let build_constraints = script
@ -248,6 +252,7 @@ pub(crate) async fn sync(
spec,
modifications,
build_constraints.unwrap_or_default(),
script_extra_build_requires,
&settings,
&network_settings,
&PlatformState::default(),
@ -579,6 +584,7 @@ pub(super) async fn do_sync(
config_settings_package,
no_build_isolation,
no_build_isolation_package,
extra_build_dependencies,
exclude_newer,
link_mode,
compile_bytecode,
@ -587,6 +593,52 @@ pub(super) async fn do_sync(
sources,
} = 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: exclude_newer.clone(),
fork_strategy: ForkStrategy::default(),
index_locations: index_locations.clone(),
index_strategy,
keyring_provider,
link_mode,
no_build_isolation,
no_build_isolation_package: no_build_isolation_package.to_vec(),
extra_build_dependencies: extra_build_dependencies.clone(),
prerelease: PrereleaseMode::default(),
resolution: ResolutionMode::default(),
sources,
upgrade: Upgrade::default(),
};
script_extra_build_requires((*script).into(), &resolver_settings)?
}
};
let client_builder = BaseClientBuilder::new()
.retries_from_env()?
.connectivity(network_settings.connectivity)
@ -715,10 +767,11 @@ pub(super) async fn do_sync(
config_setting,
config_settings_package,
build_isolation,
&extra_build_requires,
link_mode,
build_options,
&build_hasher,
exclude_newer,
exclude_newer.clone(),
sources,
workspace_cache.clone(),
concurrency,

View File

@ -13,7 +13,7 @@ use uv_normalize::DefaultGroups;
use uv_pep508::PackageName;
use uv_python::{PythonDownloads, PythonPreference, PythonRequest, PythonVersion};
use uv_resolver::{PackageMap, TreeDisplay};
use uv_scripts::{Pep723ItemRef, Pep723Script};
use uv_scripts::Pep723Script;
use uv_settings::PythonInstallMirrors;
use uv_workspace::{DiscoveryOptions, Workspace, WorkspaceCache};
@ -86,7 +86,7 @@ pub(crate) async fn tree(
} else {
Some(match target {
LockTarget::Script(script) => ScriptInterpreter::discover(
Pep723ItemRef::Script(script),
script.into(),
python.as_deref().map(PythonRequest::parse),
network_settings,
python_preference,
@ -203,6 +203,7 @@ pub(crate) async fn tree(
config_settings_package: _,
no_build_isolation: _,
no_build_isolation_package: _,
extra_build_dependencies: _,
exclude_newer: _,
link_mode: _,
upgrade: _,

View File

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

View File

@ -19,7 +19,7 @@ use uv_requirements::RequirementsSpecification;
use uv_settings::{Combine, PythonInstallMirrors, ResolverInstallerOptions, ToolOptions};
use uv_tool::InstalledTools;
use uv_warnings::write_error_chain;
use uv_workspace::WorkspaceCache;
use uv_workspace::{WorkspaceCache, pyproject::ExtraBuildDependencies};
use crate::commands::pip::loggers::{
DefaultInstallLogger, SummaryResolveLogger, UpgradeInstallLogger,
@ -337,6 +337,7 @@ async fn upgrade_tool(
spec,
Modifications::Exact,
build_constraints,
uv_distribution::ExtraBuildRequires::from_lowered(ExtraBuildDependencies::default()),
&settings,
network_settings,
&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_virtualenv::OnExisting;
use uv_warnings::warn_user;
use uv_workspace::pyproject::ExtraBuildDependencies;
use uv_workspace::{DiscoveryOptions, VirtualProject, WorkspaceCache, WorkspaceError};
use crate::commands::ExitStatus;
@ -266,7 +267,8 @@ pub(crate) async fn venv(
// Do not allow builds
let build_options = BuildOptions::new(NoBinary::None, NoBuild::All);
let extra_build_requires =
uv_distribution::ExtraBuildRequires::from_lowered(ExtraBuildDependencies::default());
// Prep the build context.
let build_dispatch = BuildDispatch::new(
&client,
@ -281,6 +283,7 @@ pub(crate) async fn venv(
&config_settings,
&config_settings_package,
BuildIsolation::Isolated,
&extra_build_requires,
link_mode,
&build_options,
&build_hasher,

View File

@ -28,7 +28,7 @@ use uv_cli::{
ProjectCommand, PythonCommand, PythonNamespace, SelfCommand, SelfNamespace, ToolCommand,
ToolNamespace, TopLevelArgs, compat::CompatArgs,
};
use uv_configuration::min_stack_size;
use uv_configuration::{PreviewFeatures, min_stack_size};
use uv_fs::{CWD, Simplified};
#[cfg(feature = "self-update")]
use uv_pep440::release_specifiers_to_ranges;
@ -37,7 +37,7 @@ use uv_pypi_types::{ParsedDirectoryUrl, ParsedUrl};
use uv_python::PythonRequest;
use uv_requirements::{GroupsSpecification, RequirementsSource};
use uv_requirements_txt::RequirementsTxtRequirement;
use uv_scripts::{Pep723Error, Pep723Item, Pep723ItemRef, Pep723Metadata, Pep723Script};
use uv_scripts::{Pep723Error, Pep723Item, Pep723Metadata, Pep723Script};
use uv_settings::{Combine, EnvironmentOptions, FilesystemOptions, Options};
use uv_static::EnvVars;
use uv_warnings::{warn_user, warn_user_once};
@ -443,6 +443,16 @@ async fn run(mut cli: Cli) -> Result<ExitStatus> {
// Resolve the settings from the command-line arguments and workspace configuration.
let args = PipCompileSettings::resolve(args, filesystem);
show_settings!(args);
if !args.settings.extra_build_dependencies.is_empty()
&& !globals
.preview
.is_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.
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.no_build_isolation,
args.settings.no_build_isolation_package,
&args.settings.extra_build_dependencies,
args.settings.build_options,
args.settings.python_version,
args.settings.python_platform,
@ -543,6 +554,16 @@ async fn run(mut cli: Cli) -> Result<ExitStatus> {
// Resolve the settings from the command-line arguments and workspace configuration.
let args = PipSyncSettings::resolve(args, filesystem);
show_settings!(args);
if !args.settings.extra_build_dependencies.is_empty()
&& !globals
.preview
.is_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.
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.no_build_isolation,
args.settings.no_build_isolation_package,
&args.settings.extra_build_dependencies,
args.settings.build_options,
args.settings.python_version,
args.settings.python_platform,
@ -621,6 +643,16 @@ async fn run(mut cli: Cli) -> Result<ExitStatus> {
// Resolve the settings from the command-line arguments and workspace configuration.
let mut args = PipInstallSettings::resolve(args, filesystem);
show_settings!(args);
if !args.settings.extra_build_dependencies.is_empty()
&& !globals
.preview
.is_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(
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.no_build_isolation,
args.settings.no_build_isolation_package,
&args.settings.extra_build_dependencies,
args.settings.build_options,
args.modifications,
args.settings.python_version,
@ -1467,7 +1500,7 @@ async fn run(mut cli: Cli) -> Result<ExitStatus> {
if let Some(Pep723Item::Script(script)) = script {
commands::python_find_script(
Pep723ItemRef::Script(&script),
(&script).into(),
args.show_version,
&globals.network_settings,
globals.python_preference,

View File

@ -45,7 +45,7 @@ use uv_settings::{
use uv_static::EnvVars;
use uv_torch::TorchMode;
use uv_warnings::warn_user_once;
use uv_workspace::pyproject::DependencyType;
use uv_workspace::pyproject::{DependencyType, ExtraBuildDependencies};
use uv_workspace::pyproject_mut::AddBoundsKind;
use crate::commands::ToolRunCommand;
@ -2714,6 +2714,7 @@ pub(crate) struct InstallerSettingsRef<'a> {
pub(crate) config_settings_package: &'a PackageConfigSettings,
pub(crate) no_build_isolation: bool,
pub(crate) no_build_isolation_package: &'a [PackageName],
pub(crate) extra_build_dependencies: &'a ExtraBuildDependencies,
pub(crate) exclude_newer: ExcludeNewer,
pub(crate) link_mode: LinkMode,
pub(crate) compile_bytecode: bool,
@ -2740,6 +2741,7 @@ pub(crate) struct ResolverSettings {
pub(crate) link_mode: LinkMode,
pub(crate) no_build_isolation: bool,
pub(crate) no_build_isolation_package: Vec<PackageName>,
pub(crate) extra_build_dependencies: ExtraBuildDependencies,
pub(crate) prerelease: PrereleaseMode,
pub(crate) resolution: ResolutionMode,
pub(crate) sources: SourceStrategy,
@ -2792,6 +2794,7 @@ impl From<ResolverOptions> for ResolverSettings {
config_settings_package: value.config_settings_package.unwrap_or_default(),
no_build_isolation: value.no_build_isolation.unwrap_or_default(),
no_build_isolation_package: value.no_build_isolation_package.unwrap_or_default(),
extra_build_dependencies: value.extra_build_dependencies.unwrap_or_default(),
exclude_newer: value.exclude_newer,
link_mode: value.link_mode.unwrap_or_default(),
sources: SourceStrategy::from_args(value.no_sources.unwrap_or_default()),
@ -2889,6 +2892,7 @@ impl From<ResolverInstallerOptions> for ResolverInstallerSettings {
link_mode: value.link_mode.unwrap_or_default(),
no_build_isolation: value.no_build_isolation.unwrap_or_default(),
no_build_isolation_package: value.no_build_isolation_package.unwrap_or_default(),
extra_build_dependencies: value.extra_build_dependencies.unwrap_or_default(),
prerelease: value.prerelease.unwrap_or_default(),
resolution: value.resolution.unwrap_or_default(),
sources: SourceStrategy::from_args(value.no_sources.unwrap_or_default()),
@ -2931,6 +2935,7 @@ pub(crate) struct PipSettings {
pub(crate) torch_backend: Option<TorchMode>,
pub(crate) no_build_isolation: bool,
pub(crate) no_build_isolation_package: Vec<PackageName>,
pub(crate) extra_build_dependencies: ExtraBuildDependencies,
pub(crate) build_options: BuildOptions,
pub(crate) allow_empty_requirements: bool,
pub(crate) strict: bool,
@ -2998,6 +3003,7 @@ impl PipSettings {
only_binary,
no_build_isolation,
no_build_isolation_package,
extra_build_dependencies,
strict,
extra,
all_extras,
@ -3057,6 +3063,7 @@ impl PipSettings {
config_settings_package: top_level_config_settings_package,
no_build_isolation: top_level_no_build_isolation,
no_build_isolation_package: top_level_no_build_isolation_package,
extra_build_dependencies: top_level_extra_build_dependencies,
exclude_newer: top_level_exclude_newer,
link_mode: top_level_link_mode,
compile_bytecode: top_level_compile_bytecode,
@ -3093,6 +3100,8 @@ impl PipSettings {
let no_build_isolation = no_build_isolation.combine(top_level_no_build_isolation);
let no_build_isolation_package =
no_build_isolation_package.combine(top_level_no_build_isolation_package);
let extra_build_dependencies =
extra_build_dependencies.combine(top_level_extra_build_dependencies);
let exclude_newer = args
.exclude_newer
.combine(exclude_newer)
@ -3196,6 +3205,10 @@ impl PipSettings {
.no_build_isolation_package
.combine(no_build_isolation_package)
.unwrap_or_default(),
extra_build_dependencies: args
.extra_build_dependencies
.combine(extra_build_dependencies)
.unwrap_or_default(),
config_setting: args
.config_settings
.combine(config_settings)
@ -3303,6 +3316,7 @@ impl<'a> From<&'a ResolverInstallerSettings> for InstallerSettingsRef<'a> {
config_settings_package: &settings.resolver.config_settings_package,
no_build_isolation: settings.resolver.no_build_isolation,
no_build_isolation_package: &settings.resolver.no_build_isolation_package,
extra_build_dependencies: &settings.resolver.extra_build_dependencies,
exclude_newer: settings.resolver.exclude_newer.clone(),
link_mode: settings.resolver.link_mode,
compile_bytecode: settings.compile_bytecode,

View File

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

View File

@ -184,6 +184,9 @@ fn resolve_uv_toml() -> anyhow::Result<()> {
torch_backend: None,
no_build_isolation: false,
no_build_isolation_package: [],
extra_build_dependencies: ExtraBuildDependencies(
{},
),
build_options: BuildOptions {
no_binary: None,
no_build: None,
@ -378,6 +381,9 @@ fn resolve_uv_toml() -> anyhow::Result<()> {
torch_backend: None,
no_build_isolation: false,
no_build_isolation_package: [],
extra_build_dependencies: ExtraBuildDependencies(
{},
),
build_options: BuildOptions {
no_binary: None,
no_build: None,
@ -573,6 +579,9 @@ fn resolve_uv_toml() -> anyhow::Result<()> {
torch_backend: None,
no_build_isolation: false,
no_build_isolation_package: [],
extra_build_dependencies: ExtraBuildDependencies(
{},
),
build_options: BuildOptions {
no_binary: None,
no_build: None,
@ -800,6 +809,9 @@ fn resolve_pyproject_toml() -> anyhow::Result<()> {
torch_backend: None,
no_build_isolation: false,
no_build_isolation_package: [],
extra_build_dependencies: ExtraBuildDependencies(
{},
),
build_options: BuildOptions {
no_binary: None,
no_build: None,
@ -962,6 +974,9 @@ fn resolve_pyproject_toml() -> anyhow::Result<()> {
torch_backend: None,
no_build_isolation: false,
no_build_isolation_package: [],
extra_build_dependencies: ExtraBuildDependencies(
{},
),
build_options: BuildOptions {
no_binary: None,
no_build: None,
@ -1168,6 +1183,9 @@ fn resolve_pyproject_toml() -> anyhow::Result<()> {
torch_backend: None,
no_build_isolation: false,
no_build_isolation_package: [],
extra_build_dependencies: ExtraBuildDependencies(
{},
),
build_options: BuildOptions {
no_binary: None,
no_build: None,
@ -1422,6 +1440,9 @@ fn resolve_index_url() -> anyhow::Result<()> {
torch_backend: None,
no_build_isolation: false,
no_build_isolation_package: [],
extra_build_dependencies: ExtraBuildDependencies(
{},
),
build_options: BuildOptions {
no_binary: None,
no_build: None,
@ -1686,6 +1707,9 @@ fn resolve_index_url() -> anyhow::Result<()> {
torch_backend: None,
no_build_isolation: false,
no_build_isolation_package: [],
extra_build_dependencies: ExtraBuildDependencies(
{},
),
build_options: BuildOptions {
no_binary: None,
no_build: None,
@ -1905,6 +1929,9 @@ fn resolve_find_links() -> anyhow::Result<()> {
torch_backend: None,
no_build_isolation: false,
no_build_isolation_package: [],
extra_build_dependencies: ExtraBuildDependencies(
{},
),
build_options: BuildOptions {
no_binary: None,
no_build: None,
@ -2089,6 +2116,9 @@ fn resolve_top_level() -> anyhow::Result<()> {
torch_backend: None,
no_build_isolation: false,
no_build_isolation_package: [],
extra_build_dependencies: ExtraBuildDependencies(
{},
),
build_options: BuildOptions {
no_binary: None,
no_build: None,
@ -2333,6 +2363,9 @@ fn resolve_top_level() -> anyhow::Result<()> {
torch_backend: None,
no_build_isolation: false,
no_build_isolation_package: [],
extra_build_dependencies: ExtraBuildDependencies(
{},
),
build_options: BuildOptions {
no_binary: None,
no_build: None,
@ -2560,6 +2593,9 @@ fn resolve_top_level() -> anyhow::Result<()> {
torch_backend: None,
no_build_isolation: false,
no_build_isolation_package: [],
extra_build_dependencies: ExtraBuildDependencies(
{},
),
build_options: BuildOptions {
no_binary: None,
no_build: None,
@ -2743,6 +2779,9 @@ fn resolve_user_configuration() -> anyhow::Result<()> {
torch_backend: None,
no_build_isolation: false,
no_build_isolation_package: [],
extra_build_dependencies: ExtraBuildDependencies(
{},
),
build_options: BuildOptions {
no_binary: None,
no_build: None,
@ -2910,6 +2949,9 @@ fn resolve_user_configuration() -> anyhow::Result<()> {
torch_backend: None,
no_build_isolation: false,
no_build_isolation_package: [],
extra_build_dependencies: ExtraBuildDependencies(
{},
),
build_options: BuildOptions {
no_binary: None,
no_build: None,
@ -3077,6 +3119,9 @@ fn resolve_user_configuration() -> anyhow::Result<()> {
torch_backend: None,
no_build_isolation: false,
no_build_isolation_package: [],
extra_build_dependencies: ExtraBuildDependencies(
{},
),
build_options: BuildOptions {
no_binary: None,
no_build: None,
@ -3246,6 +3291,9 @@ fn resolve_user_configuration() -> anyhow::Result<()> {
torch_backend: None,
no_build_isolation: false,
no_build_isolation_package: [],
extra_build_dependencies: ExtraBuildDependencies(
{},
),
build_options: BuildOptions {
no_binary: None,
no_build: None,
@ -3407,6 +3455,7 @@ fn resolve_tool() -> anyhow::Result<()> {
config_settings_package: None,
no_build_isolation: None,
no_build_isolation_package: None,
extra_build_dependencies: None,
exclude_newer: None,
exclude_newer_package: None,
link_mode: Some(
@ -3455,6 +3504,9 @@ fn resolve_tool() -> anyhow::Result<()> {
link_mode: Clone,
no_build_isolation: false,
no_build_isolation_package: [],
extra_build_dependencies: ExtraBuildDependencies(
{},
),
prerelease: IfNecessaryOrExplicit,
resolution: LowestDirect,
sources: Enabled,
@ -3613,6 +3665,9 @@ fn resolve_poetry_toml() -> anyhow::Result<()> {
torch_backend: None,
no_build_isolation: false,
no_build_isolation_package: [],
extra_build_dependencies: ExtraBuildDependencies(
{},
),
build_options: BuildOptions {
no_binary: None,
no_build: None,
@ -3848,6 +3903,9 @@ fn resolve_both() -> anyhow::Result<()> {
torch_backend: None,
no_build_isolation: false,
no_build_isolation_package: [],
extra_build_dependencies: ExtraBuildDependencies(
{},
),
build_options: BuildOptions {
no_binary: None,
no_build: None,
@ -4087,6 +4145,9 @@ fn resolve_both_special_fields() -> anyhow::Result<()> {
torch_backend: None,
no_build_isolation: false,
no_build_isolation_package: [],
extra_build_dependencies: ExtraBuildDependencies(
{},
),
build_options: BuildOptions {
no_binary: None,
no_build: None,
@ -4405,6 +4466,9 @@ fn resolve_config_file() -> anyhow::Result<()> {
torch_backend: None,
no_build_isolation: false,
no_build_isolation_package: [],
extra_build_dependencies: ExtraBuildDependencies(
{},
),
build_options: BuildOptions {
no_binary: None,
no_build: None,
@ -4490,7 +4554,7 @@ fn resolve_config_file() -> anyhow::Result<()> {
|
1 | [project]
| ^^^^^^^
unknown field `project`, expected one of `required-version`, `native-tls`, `offline`, `no-cache`, `cache-dir`, `preview`, `python-preference`, `python-downloads`, `concurrent-downloads`, `concurrent-builds`, `concurrent-installs`, `index`, `index-url`, `extra-index-url`, `no-index`, `find-links`, `index-strategy`, `keyring-provider`, `allow-insecure-host`, `resolution`, `prerelease`, `fork-strategy`, `dependency-metadata`, `config-settings`, `config-settings-package`, `no-build-isolation`, `no-build-isolation-package`, `exclude-newer`, `exclude-newer-package`, `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`, `exclude-newer-package`, `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`
"
);
@ -4665,6 +4729,9 @@ fn resolve_skip_empty() -> anyhow::Result<()> {
torch_backend: None,
no_build_isolation: false,
no_build_isolation_package: [],
extra_build_dependencies: ExtraBuildDependencies(
{},
),
build_options: BuildOptions {
no_binary: None,
no_build: None,
@ -4835,6 +4902,9 @@ fn resolve_skip_empty() -> anyhow::Result<()> {
torch_backend: None,
no_build_isolation: false,
no_build_isolation_package: [],
extra_build_dependencies: ExtraBuildDependencies(
{},
),
build_options: BuildOptions {
no_binary: None,
no_build: None,
@ -5024,6 +5094,9 @@ fn allow_insecure_host() -> anyhow::Result<()> {
torch_backend: None,
no_build_isolation: false,
no_build_isolation_package: [],
extra_build_dependencies: ExtraBuildDependencies(
{},
),
build_options: BuildOptions {
no_binary: None,
no_build: None,
@ -5274,6 +5347,9 @@ fn index_priority() -> anyhow::Result<()> {
torch_backend: None,
no_build_isolation: false,
no_build_isolation_package: [],
extra_build_dependencies: ExtraBuildDependencies(
{},
),
build_options: BuildOptions {
no_binary: None,
no_build: None,
@ -5503,6 +5579,9 @@ fn index_priority() -> anyhow::Result<()> {
torch_backend: None,
no_build_isolation: false,
no_build_isolation_package: [],
extra_build_dependencies: ExtraBuildDependencies(
{},
),
build_options: BuildOptions {
no_binary: None,
no_build: None,
@ -5738,6 +5817,9 @@ fn index_priority() -> anyhow::Result<()> {
torch_backend: None,
no_build_isolation: false,
no_build_isolation_package: [],
extra_build_dependencies: ExtraBuildDependencies(
{},
),
build_options: BuildOptions {
no_binary: None,
no_build: None,
@ -5968,6 +6050,9 @@ fn index_priority() -> anyhow::Result<()> {
torch_backend: None,
no_build_isolation: false,
no_build_isolation_package: [],
extra_build_dependencies: ExtraBuildDependencies(
{},
),
build_options: BuildOptions {
no_binary: None,
no_build: None,
@ -6205,6 +6290,9 @@ fn index_priority() -> anyhow::Result<()> {
torch_backend: None,
no_build_isolation: false,
no_build_isolation_package: [],
extra_build_dependencies: ExtraBuildDependencies(
{},
),
build_options: BuildOptions {
no_binary: None,
no_build: None,
@ -6435,6 +6523,9 @@ fn index_priority() -> anyhow::Result<()> {
torch_backend: None,
no_build_isolation: false,
no_build_isolation_package: [],
extra_build_dependencies: ExtraBuildDependencies(
{},
),
build_options: BuildOptions {
no_binary: None,
no_build: None,
@ -6609,6 +6700,9 @@ fn verify_hashes() -> anyhow::Result<()> {
torch_backend: None,
no_build_isolation: false,
no_build_isolation_package: [],
extra_build_dependencies: ExtraBuildDependencies(
{},
),
build_options: BuildOptions {
no_binary: None,
no_build: None,
@ -6769,6 +6863,9 @@ fn verify_hashes() -> anyhow::Result<()> {
torch_backend: None,
no_build_isolation: false,
no_build_isolation_package: [],
extra_build_dependencies: ExtraBuildDependencies(
{},
),
build_options: BuildOptions {
no_binary: None,
no_build: None,
@ -6927,6 +7024,9 @@ fn verify_hashes() -> anyhow::Result<()> {
torch_backend: None,
no_build_isolation: false,
no_build_isolation_package: [],
extra_build_dependencies: ExtraBuildDependencies(
{},
),
build_options: BuildOptions {
no_binary: None,
no_build: None,
@ -7087,6 +7187,9 @@ fn verify_hashes() -> anyhow::Result<()> {
torch_backend: None,
no_build_isolation: false,
no_build_isolation_package: [],
extra_build_dependencies: ExtraBuildDependencies(
{},
),
build_options: BuildOptions {
no_binary: None,
no_build: None,
@ -7245,6 +7348,9 @@ fn verify_hashes() -> anyhow::Result<()> {
torch_backend: None,
no_build_isolation: false,
no_build_isolation_package: [],
extra_build_dependencies: ExtraBuildDependencies(
{},
),
build_options: BuildOptions {
no_binary: None,
no_build: None,
@ -7404,6 +7510,9 @@ fn verify_hashes() -> anyhow::Result<()> {
torch_backend: None,
no_build_isolation: false,
no_build_isolation_package: [],
extra_build_dependencies: ExtraBuildDependencies(
{},
),
build_options: BuildOptions {
no_binary: None,
no_build: None,
@ -7501,7 +7610,7 @@ fn preview_features() {
show_settings: true,
preview: Preview {
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,
@ -7572,6 +7681,9 @@ fn preview_features() {
link_mode: Clone,
no_build_isolation: false,
no_build_isolation_package: [],
extra_build_dependencies: ExtraBuildDependencies(
{},
),
prerelease: IfNecessaryOrExplicit,
resolution: Highest,
sources: Enabled,
@ -7679,6 +7791,9 @@ fn preview_features() {
link_mode: Clone,
no_build_isolation: false,
no_build_isolation_package: [],
extra_build_dependencies: ExtraBuildDependencies(
{},
),
prerelease: IfNecessaryOrExplicit,
resolution: Highest,
sources: Enabled,
@ -7715,7 +7830,7 @@ fn preview_features() {
show_settings: true,
preview: Preview {
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,
@ -7786,6 +7901,9 @@ fn preview_features() {
link_mode: Clone,
no_build_isolation: false,
no_build_isolation_package: [],
extra_build_dependencies: ExtraBuildDependencies(
{},
),
prerelease: IfNecessaryOrExplicit,
resolution: Highest,
sources: Enabled,
@ -7893,6 +8011,9 @@ fn preview_features() {
link_mode: Clone,
no_build_isolation: false,
no_build_isolation_package: [],
extra_build_dependencies: ExtraBuildDependencies(
{},
),
prerelease: IfNecessaryOrExplicit,
resolution: Highest,
sources: Enabled,
@ -8000,6 +8121,9 @@ fn preview_features() {
link_mode: Clone,
no_build_isolation: false,
no_build_isolation_package: [],
extra_build_dependencies: ExtraBuildDependencies(
{},
),
prerelease: IfNecessaryOrExplicit,
resolution: Highest,
sources: Enabled,
@ -8109,6 +8233,9 @@ fn preview_features() {
link_mode: Clone,
no_build_isolation: false,
no_build_isolation_package: [],
extra_build_dependencies: ExtraBuildDependencies(
{},
),
prerelease: IfNecessaryOrExplicit,
resolution: Highest,
sources: Enabled,

View File

@ -1567,6 +1567,401 @@ fn sync_build_isolation_extra() -> Result<()> {
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
/// 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
@ -4198,6 +4593,187 @@ fn no_install_project_no_build() -> Result<()> {
Ok(())
}
#[test]
fn sync_extra_build_dependencies_script() -> Result<()> {
let context = TestContext::new("3.12").with_filtered_counts();
// Write a test package that arbitrarily requires `anyio` at build time
let child = context.temp_dir.child("child");
child.create_dir_all()?;
let child_pyproject_toml = child.child("pyproject.toml");
child_pyproject_toml.write_str(indoc! {r#"
[project]
name = "child"
version = "0.1.0"
requires-python = ">=3.9"
[build-system]
requires = ["hatchling"]
backend-path = ["."]
build-backend = "build_backend"
"#})?;
let build_backend = child.child("build_backend.py");
build_backend.write_str(indoc! {r#"
import sys
from hatchling.build import *
try:
import anyio
except ModuleNotFoundError:
print("Missing `anyio` module", file=sys.stderr)
sys.exit(1)
"#})?;
child.child("src/child/__init__.py").touch()?;
// Create a script that depends on the child package
let script = context.temp_dir.child("script.py");
script.write_str(indoc! {r#"
# /// script
# requires-python = ">=3.12"
# dependencies = ["child"]
#
# [tool.uv.sources]
# child = { path = "child" }
# ///
"#})?;
let filters = context
.filters()
.into_iter()
.chain(vec![(
r"environments-v2/script-[a-z0-9]+",
"environments-v2/script-[HASH]",
)])
.collect::<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]
fn virtual_no_build() -> Result<()> {
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 }
The indexes to use when resolving dependencies.
@ -1127,6 +1149,36 @@ Accepts package-date pairs in a dictionary format.
---
### [`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 URLs of package indexes to use, in addition to `--index-url`.
@ -2616,6 +2668,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 }
<span id="extra-index-url"></span>

View File

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

31
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/ExtraBuildDependencies"
},
{
"type": "null"
}
]
},
"extra-index-url": {
"description": "Extra URLs of package indexes to use, in addition to `--index-url`.\n\nAccepts either a repository compliant with [PEP 503](https://peps.python.org/pep-0503/)\n(the simple repository API), or a local directory laid out in the same format.\n\nAll indexes provided via this flag take priority over the index specified by\n[`index_url`](#index-url) or [`index`](#index) with `default = true`. When multiple indexes\nare provided, earlier values take priority.\n\nTo control uv's resolution strategy when multiple indexes are present, see\n[`index_strategy`](#index-strategy).\n\n(Deprecated: use `index` instead.)",
"type": [
@ -873,6 +884,15 @@
"type": "string",
"pattern": "^\\d{4}-\\d{2}-\\d{2}(T\\d{2}:\\d{2}:\\d{2}(Z|[+-]\\d{2}:\\d{2}))?$"
},
"ExtraBuildDependencies": {
"type": "object",
"additionalProperties": {
"type": "array",
"items": {
"$ref": "#/definitions/Requirement"
}
}
},
"ExtraName": {
"description": "The normalized name of an extra dependency.\n\nConverts the name to lowercase and collapses runs of `-`, `_`, and `.` down to a single `-`.\nFor example, `---`, `.`, and `__` are all converted to a single `-`.\n\nSee:\n- <https://peps.python.org/pep-0685/#specification/>\n- <https://packaging.python.org/en/latest/specifications/name-normalization/>",
"type": "string"
@ -1315,6 +1335,17 @@
"$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.",
"anyOf": [
{
"$ref": "#/definitions/ExtraBuildDependencies"
},
{
"type": "null"
}
]
},
"extra-index-url": {
"description": "Extra URLs of package indexes to use, in addition to `--index-url`.\n\nAccepts either a repository compliant with [PEP 503](https://peps.python.org/pep-0503/)\n(the simple repository API), or a local directory laid out in the same format.\n\nAll indexes provided via this flag take priority over the index specified by\n[`index_url`](#index-url). 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": [