Use in cache shards

This commit is contained in:
Zanie Blue 2025-07-23 10:37:15 -05:00
parent 6fb8618410
commit 8b5d9314ef
22 changed files with 323 additions and 139 deletions

1
Cargo.lock generated
View File

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

View File

@ -103,7 +103,7 @@ mod resolver {
ResolverEnvironment, ResolverOutput,
};
use uv_types::{BuildIsolation, EmptyInstalledPackages, HashStrategy};
use uv_workspace::{WorkspaceCache, pyproject::ExtraBuildDependencies};
use uv_workspace::WorkspaceCache;
static MARKERS: LazyLock<MarkerEnvironment> = LazyLock::new(|| {
MarkerEnvironment::try_from(MarkerEnvironmentBuilder {
@ -141,7 +141,9 @@ mod resolver {
universal: bool,
) -> Result<ResolverOutput> {
let build_isolation = BuildIsolation::default();
let extra_build_dependencies = ExtraBuildDependencies::default();
let extra_build_requires = uv_distribution::ExtraBuildRequires {
extra_build_dependencies: uv_workspace::pyproject::ExtraBuildDependencies::default(),
};
let build_options = BuildOptions::default();
let concurrency = Concurrency::default();
let config_settings = ConfigSettings::default();
@ -188,7 +190,7 @@ mod resolver {
&config_settings,
&config_settings_package,
build_isolation,
&extra_build_dependencies,
&extra_build_requires,
LinkMode::default(),
&build_options,
&hashes,

View File

@ -40,7 +40,7 @@ use uv_types::{
HashStrategy, InFlight,
};
use uv_workspace::WorkspaceCache;
use uv_workspace::pyproject::ExtraBuildDependencies;
use uv_distribution::ExtraBuildRequires;
#[derive(Debug, Error)]
pub enum BuildDispatchError {
@ -89,7 +89,7 @@ pub struct BuildDispatch<'a> {
shared_state: SharedState,
dependency_metadata: &'a DependencyMetadata,
build_isolation: BuildIsolation<'a>,
extra_build_dependencies: &'a ExtraBuildDependencies,
extra_build_requires: &'a ExtraBuildRequires,
link_mode: uv_install_wheel::LinkMode,
build_options: &'a BuildOptions,
config_settings: &'a ConfigSettings,
@ -118,7 +118,7 @@ impl<'a> BuildDispatch<'a> {
config_settings: &'a ConfigSettings,
config_settings_package: &'a PackageConfigSettings,
build_isolation: BuildIsolation<'a>,
extra_build_dependencies: &'a ExtraBuildDependencies,
extra_build_requires: &'a ExtraBuildRequires,
link_mode: uv_install_wheel::LinkMode,
build_options: &'a BuildOptions,
hasher: &'a HashStrategy,
@ -141,7 +141,7 @@ impl<'a> BuildDispatch<'a> {
config_settings,
config_settings_package,
build_isolation,
extra_build_dependencies,
extra_build_requires,
link_mode,
build_options,
hasher,
@ -223,6 +223,10 @@ impl BuildContext for BuildDispatch<'_> {
&self.workspace_cache
}
fn extra_build_dependencies(&self) -> &uv_workspace::pyproject::ExtraBuildDependencies {
&self.extra_build_requires.extra_build_dependencies
}
async fn resolve<'data>(
&'data self,
requirements: &'data [Requirement],
@ -456,7 +460,7 @@ impl BuildContext for BuildDispatch<'_> {
self.workspace_cache(),
config_settings,
self.build_isolation,
self.extra_build_dependencies,
&self.extra_build_requires.extra_build_dependencies,
&build_stack,
build_kind,
self.build_extra_env_vars.clone(),

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,91 @@ impl BuildRequires {
})
}
}
/// Lowered extra build dependencies with source resolution applied.
#[derive(Debug, Clone)]
pub struct ExtraBuildRequires {
pub extra_build_dependencies: ExtraBuildDependencies,
}
impl ExtraBuildRequires {
/// Lower extra build dependencies from a workspace, applying source resolution.
pub fn from_workspace(
extra_build_dependencies: ExtraBuildDependencies,
workspace: &Workspace,
index_locations: &IndexLocations,
source_strategy: SourceStrategy,
) -> Result<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

@ -390,6 +390,20 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> {
}
}
/// Determine the extra build dependencies for the given package name.
fn extra_build_dependencies_for(
&self,
name: Option<&PackageName>,
) -> &[uv_pep508::Requirement<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,
@ -423,12 +437,13 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> {
let cache_shard = cache_shard.shard(revision.id());
let source_dist_entry = cache_shard.entry(SOURCE);
// If there are build settings, we need to scope to a cache shard.
// If there are build settings or extra build dependencies, we need to scope to a cache shard.
let config_settings = self.config_settings_for(source.name());
let cache_shard = if config_settings.is_empty() {
let extra_build_deps = self.extra_build_dependencies_for(source.name());
let cache_shard = if config_settings.is_empty() && extra_build_deps.is_empty() {
cache_shard
} else {
cache_shard.shard(cache_digest(&&config_settings))
cache_shard.shard(cache_digest(&(&config_settings, extra_build_deps)))
};
// If the cache contains a compatible wheel, return it.
@ -596,12 +611,13 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> {
}
}
// If there are build settings, we need to scope to a cache shard.
// If there are build settings or extra build dependencies, we need to scope to a cache shard.
let config_settings = self.config_settings_for(source.name());
let cache_shard = if config_settings.is_empty() {
let extra_build_deps = self.extra_build_dependencies_for(source.name());
let cache_shard = if config_settings.is_empty() && extra_build_deps.is_empty() {
cache_shard
} else {
cache_shard.shard(cache_digest(&config_settings))
cache_shard.shard(cache_digest(&(&config_settings, extra_build_deps)))
};
// Otherwise, we either need to build the metadata.
@ -795,12 +811,13 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> {
let cache_shard = cache_shard.shard(revision.id());
let source_entry = cache_shard.entry(SOURCE);
// If there are build settings, we need to scope to a cache shard.
// If there are build settings or extra build dependencies, we need to scope to a cache shard.
let config_settings = self.config_settings_for(source.name());
let cache_shard = if config_settings.is_empty() {
let extra_build_deps = self.extra_build_dependencies_for(source.name());
let cache_shard = if config_settings.is_empty() && extra_build_deps.is_empty() {
cache_shard
} else {
cache_shard.shard(cache_digest(&config_settings))
cache_shard.shard(cache_digest(&(&config_settings, extra_build_deps)))
};
// If the cache contains a compatible wheel, return it.
@ -957,12 +974,13 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> {
});
}
// If there are build settings, we need to scope to a cache shard.
// If there are build settings or extra build dependencies, we need to scope to a cache shard.
let config_settings = self.config_settings_for(source.name());
let cache_shard = if config_settings.is_empty() {
let extra_build_deps = self.extra_build_dependencies_for(source.name());
let cache_shard = if config_settings.is_empty() && extra_build_deps.is_empty() {
cache_shard
} else {
cache_shard.shard(cache_digest(&config_settings))
cache_shard.shard(cache_digest(&(&config_settings, extra_build_deps)))
};
// Otherwise, we need to build a wheel.
@ -1099,12 +1117,13 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> {
// freshness, since entries have to be fresher than the revision itself.
let cache_shard = cache_shard.shard(revision.id());
// If there are build settings, we need to scope to a cache shard.
// If there are build settings or extra build dependencies, we need to scope to a cache shard.
let config_settings = self.config_settings_for(source.name());
let cache_shard = if config_settings.is_empty() {
let extra_build_deps = self.extra_build_dependencies_for(source.name());
let cache_shard = if config_settings.is_empty() && extra_build_deps.is_empty() {
cache_shard
} else {
cache_shard.shard(cache_digest(&config_settings))
cache_shard.shard(cache_digest(&(&config_settings, extra_build_deps)))
};
// If the cache contains a compatible wheel, return it.
@ -1287,12 +1306,13 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> {
));
}
// If there are build settings, we need to scope to a cache shard.
// If there are build settings or extra build dependencies, we need to scope to a cache shard.
let config_settings = self.config_settings_for(source.name());
let cache_shard = if config_settings.is_empty() {
let extra_build_deps = self.extra_build_dependencies_for(source.name());
let cache_shard = if config_settings.is_empty() && extra_build_deps.is_empty() {
cache_shard
} else {
cache_shard.shard(cache_digest(&config_settings))
cache_shard.shard(cache_digest(&(&config_settings, extra_build_deps)))
};
// Otherwise, we need to build a wheel.
@ -1492,12 +1512,13 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> {
// Acquire the advisory lock.
let _lock = cache_shard.lock().await.map_err(Error::CacheWrite)?;
// If there are build settings, we need to scope to a cache shard.
// If there are build settings or extra build dependencies, we need to scope to a cache shard.
let config_settings = self.config_settings_for(source.name());
let cache_shard = if config_settings.is_empty() {
let extra_build_deps = self.extra_build_dependencies_for(source.name());
let cache_shard = if config_settings.is_empty() && extra_build_deps.is_empty() {
cache_shard
} else {
cache_shard.shard(cache_digest(&config_settings))
cache_shard.shard(cache_digest(&(&config_settings, extra_build_deps)))
};
// If the cache contains a compatible wheel, return it.
@ -1795,12 +1816,13 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> {
));
}
// If there are build settings, we need to scope to a cache shard.
// If there are build settings or extra build dependencies, we need to scope to a cache shard.
let config_settings = self.config_settings_for(source.name());
let cache_shard = if config_settings.is_empty() {
let extra_build_deps = self.extra_build_dependencies_for(source.name());
let cache_shard = if config_settings.is_empty() && extra_build_deps.is_empty() {
cache_shard
} else {
cache_shard.shard(cache_digest(&config_settings))
cache_shard.shard(cache_digest(&(&config_settings, extra_build_deps)))
};
// Otherwise, we need to build a wheel.

View File

@ -114,6 +114,24 @@ impl Operator {
pub fn is_star(self) -> bool {
matches!(self, Self::EqualStar | Self::NotEqualStar)
}
/// Returns the string representation of this operator.
pub fn as_str(self) -> &'static str {
match self {
Self::Equal => "==",
// Beware, this doesn't print the star
Self::EqualStar => "==",
#[allow(deprecated)]
Self::ExactEqual => "===",
Self::NotEqual => "!=",
Self::NotEqualStar => "!=",
Self::TildeEqual => "~=",
Self::LessThan => "<",
Self::LessThanEqual => "<=",
Self::GreaterThan => ">",
Self::GreaterThanEqual => ">=",
}
}
}
impl FromStr for Operator {
@ -150,21 +168,7 @@ impl FromStr for Operator {
impl std::fmt::Display for Operator {
/// Note the `EqualStar` is also `==`.
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let operator = match self {
Self::Equal => "==",
// Beware, this doesn't print the star
Self::EqualStar => "==",
#[allow(deprecated)]
Self::ExactEqual => "===",
Self::NotEqual => "!=",
Self::NotEqualStar => "!=",
Self::TildeEqual => "~=",
Self::LessThan => "<",
Self::LessThanEqual => "<=",
Self::GreaterThan => ">",
Self::GreaterThanEqual => ">=",
};
let operator = self.as_str();
write!(f, "{operator}")
}
}

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().to_string().cache_key(state);
}
}
VersionOrUrl::Url(url) => {
1u8.cache_key(state);
url.to_string().cache_key(state);
}
}
} else {
0u8.cache_key(state);
}
if let Some(marker) = self.marker.contents() {
1u8.cache_key(state);
marker.to_string().cache_key(state);
} else {
0u8.cache_key(state);
}
// `origin` is intentionally omitted
}
}
impl<T: Pep508Url> Requirement<T> {
/// Returns whether the markers apply for the given environment
pub fn evaluate_markers(&self, env: &MarkerEnvironment, extras: &[ExtraName]) -> bool {

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

@ -564,6 +564,9 @@ async fn build_package(
let workspace_cache = WorkspaceCache::default();
// Create a build dispatch.
let extra_build_requires = uv_distribution::ExtraBuildRequires::from_lowered(
extra_build_dependencies.clone(),
);
let build_dispatch = BuildDispatch::new(
&client,
cache,
@ -577,7 +580,7 @@ async fn build_package(
config_setting,
config_settings_package,
build_isolation,
extra_build_dependencies,
&extra_build_requires,
link_mode,
build_options,
&hasher,

View File

@ -476,6 +476,9 @@ pub(crate) async fn pip_compile(
.map(|constraint| constraint.requirement.clone()),
);
let extra_build_requires = uv_distribution::ExtraBuildRequires::from_lowered(
extra_build_dependencies.clone(),
);
let build_dispatch = BuildDispatch::new(
&client,
&cache,
@ -489,7 +492,7 @@ pub(crate) async fn pip_compile(
&config_settings,
&config_settings_package,
build_isolation,
extra_build_dependencies,
&extra_build_requires,
link_mode,
&build_options,
&build_hashes,

View File

@ -421,6 +421,9 @@ pub(crate) async fn pip_install(
let state = SharedState::default();
// Create a build dispatch.
let extra_build_requires = uv_distribution::ExtraBuildRequires::from_lowered(
extra_build_dependencies.clone(),
);
let build_dispatch = BuildDispatch::new(
&client,
&cache,
@ -434,7 +437,7 @@ pub(crate) async fn pip_install(
config_settings,
config_settings_package,
build_isolation,
extra_build_dependencies,
&extra_build_requires,
link_mode,
&build_options,
&build_hasher,

View File

@ -354,6 +354,9 @@ pub(crate) async fn pip_sync(
let state = SharedState::default();
// Create a build dispatch.
let extra_build_requires = uv_distribution::ExtraBuildRequires::from_lowered(
extra_build_dependencies.clone(),
);
let build_dispatch = BuildDispatch::new(
&client,
&cache,
@ -367,7 +370,7 @@ pub(crate) async fn pip_sync(
config_settings,
config_settings_package,
build_isolation,
extra_build_dependencies,
&extra_build_requires,
link_mode,
&build_options,
&build_hasher,

View File

@ -431,6 +431,18 @@ pub(crate) async fn add(
};
// Create a build dispatch.
let extra_build_requires = if let AddTarget::Project(project, _) = &target {
uv_distribution::ExtraBuildRequires::from_workspace(
settings.resolver.extra_build_dependencies.clone(),
project.workspace(),
&settings.resolver.index_locations,
settings.resolver.sources,
)?
} else {
uv_distribution::ExtraBuildRequires::from_lowered(
settings.resolver.extra_build_dependencies.clone(),
)
};
let build_dispatch = BuildDispatch::new(
&client,
cache,
@ -444,7 +456,7 @@ pub(crate) async fn add(
&settings.resolver.config_setting,
&settings.resolver.config_settings_package,
build_isolation,
&settings.resolver.extra_build_dependencies,
&extra_build_requires,
settings.resolver.link_mode,
&settings.resolver.build_options,
&build_hasher,

View File

@ -671,6 +671,17 @@ async fn do_lock(
};
// Create a build dispatch.
let extra_build_requires = match &target {
LockTarget::Workspace(workspace) => uv_distribution::ExtraBuildRequires::from_workspace(
extra_build_dependencies.clone(),
workspace,
index_locations,
*sources,
)?,
LockTarget::Script(_) => uv_distribution::ExtraBuildRequires::from_lowered(
extra_build_dependencies.clone(),
),
};
let build_dispatch = BuildDispatch::new(
&client,
cache,
@ -684,7 +695,7 @@ async fn do_lock(
config_setting,
config_settings_package,
build_isolation,
extra_build_dependencies,
&extra_build_requires,
*link_mode,
build_options,
&build_hasher,

View File

@ -1733,6 +1733,9 @@ pub(crate) async fn resolve_names(
let build_hasher = HashStrategy::default();
// Create a build dispatch.
let extra_build_requires = uv_distribution::ExtraBuildRequires::from_lowered(
extra_build_dependencies.clone(),
);
let build_dispatch = BuildDispatch::new(
&client,
cache,
@ -1746,7 +1749,7 @@ pub(crate) async fn resolve_names(
config_setting,
config_settings_package,
build_isolation,
extra_build_dependencies,
&extra_build_requires,
*link_mode,
build_options,
&build_hasher,
@ -1943,6 +1946,9 @@ pub(crate) async fn resolve_environment(
let workspace_cache = WorkspaceCache::default();
// Create a build dispatch.
let extra_build_requires = uv_distribution::ExtraBuildRequires::from_lowered(
extra_build_dependencies.clone(),
);
let resolve_dispatch = BuildDispatch::new(
&client,
cache,
@ -1956,7 +1962,7 @@ pub(crate) async fn resolve_environment(
config_setting,
config_settings_package,
build_isolation,
extra_build_dependencies,
&extra_build_requires,
*link_mode,
build_options,
&build_hasher,
@ -2083,6 +2089,9 @@ pub(crate) async fn sync_environment(
};
// Create a build dispatch.
let extra_build_requires = uv_distribution::ExtraBuildRequires::from_lowered(
extra_build_dependencies.clone(),
);
let build_dispatch = BuildDispatch::new(
&client,
cache,
@ -2096,7 +2105,7 @@ pub(crate) async fn sync_environment(
config_setting,
config_settings_package,
build_isolation,
extra_build_dependencies,
&extra_build_requires,
link_mode,
build_options,
&build_hasher,
@ -2309,6 +2318,9 @@ pub(crate) async fn update_environment(
};
// Create a build dispatch.
let extra_build_requires = uv_distribution::ExtraBuildRequires::from_lowered(
extra_build_dependencies.clone(),
);
let build_dispatch = BuildDispatch::new(
&client,
cache,
@ -2322,7 +2334,7 @@ pub(crate) async fn update_environment(
config_setting,
config_settings_package,
build_isolation,
extra_build_dependencies,
&extra_build_requires,
*link_mode,
build_options,
&build_hasher,

View File

@ -18,24 +18,23 @@ use uv_configuration::{
};
use uv_dispatch::BuildDispatch;
use uv_distribution_types::{
DirectorySourceDist, Dist, Index, IndexLocations, Requirement, Resolution, ResolvedDist,
DirectorySourceDist, Dist, Index, Requirement, Resolution, ResolvedDist,
SourceDist,
};
use uv_fs::{PortablePathBuf, Simplified};
use uv_installer::SitePackages;
use uv_normalize::{DefaultExtras, DefaultGroups, PackageName};
use uv_pep508::{MarkerTree, VersionOrUrl};
use uv_pypi_types::{ParsedArchiveUrl, ParsedGitUrl, ParsedUrl, VerbatimParsedUrl};
use uv_pypi_types::{ParsedArchiveUrl, ParsedGitUrl, ParsedUrl};
use uv_python::{PythonDownloads, PythonEnvironment, PythonPreference, PythonRequest};
use uv_resolver::{FlatIndex, Installable, Lock};
use uv_scripts::{Pep723ItemRef, Pep723Script};
use uv_settings::PythonInstallMirrors;
use uv_types::{BuildIsolation, HashStrategy};
use uv_warnings::{warn_user, warn_user_once};
use uv_workspace::pyproject::Source;
use uv_workspace::pyproject::{ExtraBuildDependencies, Source};
use uv_workspace::{
DiscoveryOptions, MemberDiscovery, VirtualProject, Workspace, WorkspaceCache,
pyproject::ExtraBuildDependencies,
};
use crate::commands::pip::loggers::{DefaultInstallLogger, DefaultResolveLogger, InstallLogger};
@ -547,60 +546,6 @@ impl Deref for SyncEnvironment {
}
}
/// Lower extra build dependencies using workspace sources.
///
/// This ensures that extra build dependencies respect source configurations
/// from the project's `tool.uv.sources` table.
#[allow(clippy::result_large_err)]
fn lower_extra_build_dependencies(
extra_build_dependencies: &ExtraBuildDependencies,
workspace: &Workspace,
index_locations: &IndexLocations,
) -> Result<ExtraBuildDependencies, ProjectError> {
use std::collections::BTreeMap;
use uv_configuration::SourceStrategy;
let mut lowered_dependencies = BTreeMap::new();
for (package_name, requirements) in extra_build_dependencies {
// Use BuildRequires to lower the requirements
let metadata = uv_distribution::BuildRequires::from_workspace(
uv_pypi_types::BuildRequires {
name: Some(package_name.clone()),
requires_dist: requirements.clone(),
},
workspace,
index_locations,
SourceStrategy::Enabled,
)?;
// Extract the lowered requirements and convert them
let lowered_requirements: Vec<uv_pep508::Requirement<VerbatimParsedUrl>> =
metadata.requires_dist.into_iter().map(Into::into).collect();
lowered_dependencies.insert(package_name.clone(), lowered_requirements);
}
Ok(ExtraBuildDependencies::from(lowered_dependencies))
}
/// Lower extra build dependencies using script sources.
///
/// This ensures that extra build dependencies respect source configurations
/// from the script's metadata.
fn lower_extra_build_dependencies_for_script(
extra_build_dependencies: &ExtraBuildDependencies,
_script: &Pep723Script,
_index_locations: &IndexLocations,
) -> ExtraBuildDependencies {
// Scripts don't have extra build dependencies per se, but we still need to handle
// the case for consistency. Since scripts don't define extra build dependencies
// for other packages, we just return the dependencies as-is.
//
// If in the future scripts support defining extra build dependencies for packages
// they depend on, we would need to implement proper lowering here using the
// script's sources.
extra_build_dependencies.clone()
}
/// Sync a lockfile with an environment.
#[allow(clippy::fn_params_excessive_bools)]
@ -650,20 +595,24 @@ pub(super) async fn do_sync(
);
}
// Lower extra build dependencies to apply source configurations
let extra_build_dependencies = match &target {
// Lower the extra build dependencies with source resolution
let extra_build_requires = match &target {
InstallTarget::Workspace { workspace, .. }
| InstallTarget::Project { workspace, .. }
| InstallTarget::NonProjectWorkspace { workspace, .. } => {
lower_extra_build_dependencies(extra_build_dependencies, workspace, index_locations)?
uv_distribution::ExtraBuildRequires::from_workspace(
extra_build_dependencies.clone(),
workspace,
index_locations,
sources,
)?
}
InstallTarget::Script { script, .. } => lower_extra_build_dependencies_for_script(
extra_build_dependencies,
script,
index_locations,
),
InstallTarget::Script { .. } => uv_distribution::ExtraBuildRequires {
extra_build_dependencies: ExtraBuildDependencies::default(),
},
};
let client_builder = BaseClientBuilder::new()
.retries_from_env()?
.connectivity(network_settings.connectivity)
@ -792,7 +741,7 @@ pub(super) async fn do_sync(
config_setting,
config_settings_package,
build_isolation,
&extra_build_dependencies,
&extra_build_requires,
link_mode,
build_options,
&build_hasher,

View File

@ -275,7 +275,9 @@ pub(crate) async fn venv(
// Do not allow builds
let build_options = BuildOptions::new(NoBinary::None, NoBuild::All);
let extra_build_dependencies = ExtraBuildDependencies::default();
let extra_build_requires = uv_distribution::ExtraBuildRequires::from_lowered(
ExtraBuildDependencies::default(),
);
// Prep the build context.
let build_dispatch = BuildDispatch::new(
&client,
@ -290,7 +292,7 @@ pub(crate) async fn venv(
&config_settings,
&config_settings_package,
BuildIsolation::Isolated,
&extra_build_dependencies,
&extra_build_requires,
link_mode,
&build_options,
&build_hasher,

View File

@ -1573,8 +1573,9 @@ fn sync_extra_build_dependencies() -> Result<()> {
child = { path = "child" }
"#})?;
context.venv().arg("--clear").assert().success();
// Running `uv sync` should fail due to missing build-dependencies
uv_snapshot!(context.filters(), context.sync().arg("--reinstall").arg("--refresh"), @r"
uv_snapshot!(context.filters(), context.sync(), @r"
success: false
exit_code: 1
----- stdout -----
@ -1607,7 +1608,8 @@ fn sync_extra_build_dependencies() -> Result<()> {
child = ["anyio"]
"#})?;
uv_snapshot!(context.filters(), context.sync().arg("--reinstall").arg("--refresh"), @r"
context.venv().arg("--clear").assert().success();
uv_snapshot!(context.filters(), context.sync(), @r"
success: true
exit_code: 0
----- stdout -----
@ -1620,7 +1622,8 @@ fn sync_extra_build_dependencies() -> Result<()> {
+ child==0.1.0 (from file://[TEMP_DIR]/child)
");
// Adding `extra-build-dependencies` with the wrong name should not
// Adding `extra-build-dependencies` with the wrong name should fail the build
// (the cache is invalidated when extra build dependencies change)
pyproject_toml.write_str(indoc! {r#"
[project]
name = "parent"
@ -1635,7 +1638,8 @@ fn sync_extra_build_dependencies() -> Result<()> {
wrong_name = ["anyio"]
"#})?;
uv_snapshot!(context.filters(), context.sync().arg("--reinstall").arg("--refresh"), @r"
context.venv().arg("--clear").assert().success();
uv_snapshot!(context.filters(), context.sync(), @r"
success: false
exit_code: 1
----- stdout -----
@ -1703,7 +1707,8 @@ fn sync_extra_build_dependencies() -> Result<()> {
"#})?;
// Confirm that `bad_child` fails if anyio is provided
uv_snapshot!(context.filters(), context.sync().arg("--reinstall").arg("--refresh"), @r"
context.venv().arg("--clear").assert().success();
uv_snapshot!(context.filters(), context.sync(), @r"
success: false
exit_code: 1
----- stdout -----
@ -1738,7 +1743,8 @@ fn sync_extra_build_dependencies() -> Result<()> {
child = ["anyio"]
"#})?;
uv_snapshot!(context.filters(), context.sync().arg("--reinstall").arg("--refresh"), @r"
context.venv().arg("--clear").assert().success();
uv_snapshot!(context.filters(), context.sync(), @r"
success: true
exit_code: 0
----- stdout -----
@ -1747,10 +1753,9 @@ fn sync_extra_build_dependencies() -> Result<()> {
warning: The `extra-build-dependencies` option is experimental and may change without warning. Pass `--preview` to disable this warning.
Resolved [N] packages in [TIME]
Prepared [N] packages in [TIME]
Uninstalled [N] packages in [TIME]
Installed [N] packages in [TIME]
+ bad-child==0.1.0 (from file://[TEMP_DIR]/bad_child)
~ child==0.1.0 (from file://[TEMP_DIR]/child)
+ child==0.1.0 (from file://[TEMP_DIR]/child)
");
Ok(())
@ -1815,7 +1820,7 @@ fn sync_extra_build_dependencies_sources() -> Result<()> {
})?;
// Running `uv sync` should succeed, as `anyio` is provided as a source
uv_snapshot!(context.filters(), context.sync().arg("--reinstall").arg("--refresh"), @r"
uv_snapshot!(context.filters(), context.sync(), @r"
success: true
exit_code: 0
----- stdout -----
@ -1828,6 +1833,9 @@ fn sync_extra_build_dependencies_sources() -> Result<()> {
+ child==0.1.0 (from file://[TEMP_DIR]/child)
");
// TODO(zanieb): We want to test with `--no-sources` too but unfortunately that's not easy
// because it'll disable the `child` path source too!
Ok(())
}