1use std::ffi::{OsStr, OsString};
6use std::path::Path;
7
8use anyhow::{Context, Result};
9use futures::FutureExt;
10use itertools::Itertools;
11use rustc_hash::FxHashMap;
12use thiserror::Error;
13use tracing::{debug, instrument, trace};
14
15use uv_build_backend::check_direct_build;
16use uv_build_frontend::{SourceBuild, SourceBuildContext};
17use uv_cache::Cache;
18use uv_client::RegistryClient;
19use uv_configuration::{
20 BuildKind, BuildOptions, Constraints, IndexStrategy, NoSources, Overrides, Reinstall,
21};
22use uv_configuration::{BuildOutput, Concurrency};
23use uv_distribution::DistributionDatabase;
24use uv_distribution_filename::DistFilename;
25use uv_distribution_types::{
26 CachedDist, ConfigSettings, DependencyMetadata, ExtraBuildRequires, ExtraBuildVariables,
27 Identifier, IndexCapabilities, IndexLocations, IsBuildBackendError, Name,
28 PackageConfigSettings, Requirement, Resolution, SourceDist, VersionOrUrlRef,
29};
30use uv_git::GitResolver;
31use uv_installer::{InstallationStrategy, Installer, Plan, Planner, Preparer, SitePackages};
32use uv_preview::Preview;
33use uv_pypi_types::Conflicts;
34use uv_python::{Interpreter, PythonEnvironment};
35use uv_requirements::LookaheadResolver;
36use uv_resolver::{
37 ExcludeNewer, FlatIndex, Flexibility, InMemoryIndex, Manifest, OptionsBuilder,
38 PythonRequirement, Resolver, ResolverEnvironment,
39};
40use uv_types::{
41 AnyErrorBuild, BuildArena, BuildContext, BuildIsolation, BuildStack, EmptyInstalledPackages,
42 HashStrategy, InFlight, ResolvedRequirements, SourceTreeEditablePolicy,
43};
44use uv_workspace::WorkspaceCache;
45
46#[derive(Debug, Error)]
47pub enum BuildDispatchError {
48 #[error(transparent)]
49 BuildFrontend(#[from] AnyErrorBuild),
50
51 #[error(transparent)]
52 Tags(#[from] uv_platform_tags::TagsError),
53
54 #[error(transparent)]
55 Resolve(#[from] uv_resolver::ResolveError),
56
57 #[error(transparent)]
58 Join(#[from] tokio::task::JoinError),
59
60 #[error(transparent)]
61 Anyhow(#[from] anyhow::Error),
62
63 #[error(transparent)]
64 Prepare(#[from] uv_installer::PrepareError),
65
66 #[error(transparent)]
67 Lookahead(#[from] uv_requirements::Error),
68}
69
70impl uv_errors::Hint for BuildDispatchError {
71 fn hints(&self) -> uv_errors::Hints<'_> {
72 match self {
73 Self::BuildFrontend(err) => err.hints(),
74 Self::Resolve(err) => err.hints(),
75 Self::Anyhow(err) => {
76 for cause in err.chain() {
79 if let Some(resolve_err) = cause.downcast_ref::<uv_resolver::ResolveError>() {
80 let hints = resolve_err.hints();
81 if !hints.is_empty() {
82 return hints;
83 }
84 }
85 }
86 uv_errors::Hints::none()
87 }
88 _ => uv_errors::Hints::none(),
89 }
90 }
91}
92
93impl IsBuildBackendError for BuildDispatchError {
94 fn is_build_backend_error(&self) -> bool {
95 match self {
96 Self::Tags(_)
97 | Self::Resolve(_)
98 | Self::Join(_)
99 | Self::Anyhow(_)
100 | Self::Prepare(_)
101 | Self::Lookahead(_) => false,
102 Self::BuildFrontend(err) => err.is_build_backend_error(),
103 }
104 }
105}
106
107pub struct BuildDispatch<'a> {
110 client: &'a RegistryClient,
111 cache: &'a Cache,
112 constraints: &'a Constraints,
113 interpreter: &'a Interpreter,
114 index_locations: &'a IndexLocations,
115 index_strategy: IndexStrategy,
116 flat_index: &'a FlatIndex,
117 shared_state: SharedState,
118 dependency_metadata: &'a DependencyMetadata,
119 build_isolation: BuildIsolation<'a>,
120 extra_build_requires: &'a ExtraBuildRequires,
121 extra_build_variables: &'a ExtraBuildVariables,
122 link_mode: uv_install_wheel::LinkMode,
123 build_options: &'a BuildOptions,
124 config_settings: &'a ConfigSettings,
125 config_settings_package: &'a PackageConfigSettings,
126 hasher: &'a HashStrategy,
127 exclude_newer: ExcludeNewer,
128 source_build_context: SourceBuildContext,
129 build_extra_env_vars: FxHashMap<OsString, OsString>,
130 sources: NoSources,
131 source_tree_editable_policy: SourceTreeEditablePolicy,
132 workspace_cache: WorkspaceCache,
133 concurrency: Concurrency,
134 preview: Preview,
135}
136
137impl<'a> BuildDispatch<'a> {
138 pub fn new(
139 client: &'a RegistryClient,
140 cache: &'a Cache,
141 constraints: &'a Constraints,
142 interpreter: &'a Interpreter,
143 index_locations: &'a IndexLocations,
144 flat_index: &'a FlatIndex,
145 dependency_metadata: &'a DependencyMetadata,
146 shared_state: SharedState,
147 index_strategy: IndexStrategy,
148 config_settings: &'a ConfigSettings,
149 config_settings_package: &'a PackageConfigSettings,
150 build_isolation: BuildIsolation<'a>,
151 extra_build_requires: &'a ExtraBuildRequires,
152 extra_build_variables: &'a ExtraBuildVariables,
153 link_mode: uv_install_wheel::LinkMode,
154 build_options: &'a BuildOptions,
155 hasher: &'a HashStrategy,
156 exclude_newer: ExcludeNewer,
157 sources: NoSources,
158 source_tree_editable_policy: SourceTreeEditablePolicy,
159 workspace_cache: WorkspaceCache,
160 concurrency: Concurrency,
161 preview: Preview,
162 ) -> Self {
163 Self {
164 client,
165 cache,
166 constraints,
167 interpreter,
168 index_locations,
169 flat_index,
170 shared_state,
171 dependency_metadata,
172 index_strategy,
173 config_settings,
174 config_settings_package,
175 build_isolation,
176 extra_build_requires,
177 extra_build_variables,
178 link_mode,
179 build_options,
180 hasher,
181 exclude_newer,
182 source_build_context: SourceBuildContext::new(concurrency.builds_semaphore.clone()),
183 build_extra_env_vars: FxHashMap::default(),
184 sources,
185 source_tree_editable_policy,
186 workspace_cache,
187 concurrency,
188 preview,
189 }
190 }
191
192 #[must_use]
194 pub fn with_build_extra_env_vars<I, K, V>(mut self, sdist_build_env_variables: I) -> Self
195 where
196 I: IntoIterator<Item = (K, V)>,
197 K: AsRef<OsStr>,
198 V: AsRef<OsStr>,
199 {
200 self.build_extra_env_vars = sdist_build_env_variables
201 .into_iter()
202 .map(|(key, value)| (key.as_ref().to_owned(), value.as_ref().to_owned()))
203 .collect();
204 self
205 }
206}
207
208#[allow(refining_impl_trait)]
209impl BuildContext for BuildDispatch<'_> {
210 type SourceDistBuilder = SourceBuild;
211
212 async fn interpreter(&self) -> &Interpreter {
213 self.interpreter
214 }
215
216 fn cache(&self) -> &Cache {
217 self.cache
218 }
219
220 fn git(&self) -> &GitResolver {
221 &self.shared_state.git
222 }
223
224 fn build_arena(&self) -> &BuildArena<SourceBuild> {
225 &self.shared_state.build_arena
226 }
227
228 fn capabilities(&self) -> &IndexCapabilities {
229 &self.shared_state.capabilities
230 }
231
232 fn dependency_metadata(&self) -> &DependencyMetadata {
233 self.dependency_metadata
234 }
235
236 fn build_options(&self) -> &BuildOptions {
237 self.build_options
238 }
239
240 fn build_isolation(&self) -> BuildIsolation<'_> {
241 self.build_isolation
242 }
243
244 fn config_settings(&self) -> &ConfigSettings {
245 self.config_settings
246 }
247
248 fn config_settings_package(&self) -> &PackageConfigSettings {
249 self.config_settings_package
250 }
251
252 fn sources(&self) -> &NoSources {
253 &self.sources
254 }
255
256 fn source_tree_editable_policy(&self) -> SourceTreeEditablePolicy {
257 self.source_tree_editable_policy
258 }
259
260 fn locations(&self) -> &IndexLocations {
261 self.index_locations
262 }
263
264 fn workspace_cache(&self) -> &WorkspaceCache {
265 &self.workspace_cache
266 }
267
268 fn extra_build_requires(&self) -> &ExtraBuildRequires {
269 self.extra_build_requires
270 }
271
272 fn extra_build_variables(&self) -> &ExtraBuildVariables {
273 self.extra_build_variables
274 }
275
276 async fn resolve<'data>(
277 &'data self,
278 requirements: &'data [Requirement],
279 build_stack: &'data BuildStack,
280 ) -> Result<ResolvedRequirements, BuildDispatchError> {
281 let python_requirement = PythonRequirement::from_interpreter(self.interpreter);
282 let marker_env = self.interpreter.resolver_marker_environment();
283 let resolver_env = ResolverEnvironment::specific(marker_env);
284 let tags = self.interpreter.tags()?;
285
286 let hasher = self
292 .hasher
293 .clone()
294 .augment_with_requirements(requirements.iter())
295 .map_err(uv_requirements::Error::from)?;
296 let overrides = Overrides::default();
297 let (lookaheads, hasher) = LookaheadResolver::new(
298 requirements,
299 self.constraints,
300 &overrides,
301 &hasher,
302 &self.shared_state.index,
303 DistributionDatabase::new(
304 self.client,
305 self,
306 self.concurrency.downloads_semaphore.clone(),
307 )
308 .with_build_stack(build_stack),
309 )
310 .resolve(&resolver_env)
311 .await?;
312
313 let manifest = Manifest::simple(requirements.to_vec())
314 .with_constraints(self.constraints.clone())
315 .with_lookaheads(lookaheads);
316
317 let resolver = Resolver::new(
318 manifest,
319 OptionsBuilder::new()
320 .exclude_newer(self.exclude_newer.clone())
321 .index_strategy(self.index_strategy)
322 .build_options(self.build_options.clone())
323 .flexibility(Flexibility::Fixed)
324 .build(),
325 &python_requirement,
326 resolver_env,
327 self.interpreter.markers(),
328 Conflicts::empty(),
330 Some(tags),
331 self.flat_index,
332 &self.shared_state.index,
333 &hasher,
334 self,
335 EmptyInstalledPackages,
336 DistributionDatabase::new(
337 self.client,
338 self,
339 self.concurrency.downloads_semaphore.clone(),
340 )
341 .with_build_stack(build_stack),
342 )?;
343 let resolution = Resolution::from(resolver.resolve().await.with_context(|| {
344 format!(
345 "No solution found when resolving: {}",
346 requirements
347 .iter()
348 .map(|requirement| format!("`{requirement}`"))
349 .join(", ")
350 )
351 })?);
352 Ok(ResolvedRequirements::new(resolution, hasher))
353 }
354
355 #[instrument(
356 skip(self, requirements, venv),
357 fields(
358 resolution = requirements.resolution().distributions().map(ToString::to_string).join(", "),
359 venv = ?venv.root()
360 )
361 )]
362 async fn install<'data>(
363 &'data self,
364 requirements: &'data ResolvedRequirements,
365 venv: &'data PythonEnvironment,
366 build_stack: &'data BuildStack,
367 ) -> Result<Vec<CachedDist>, BuildDispatchError> {
368 let resolution = requirements.resolution();
369 let hasher = requirements.hasher();
370
371 debug!(
372 "Installing in {} in {}",
373 resolution
374 .distributions()
375 .map(ToString::to_string)
376 .join(", "),
377 venv.root().display(),
378 );
379
380 let tags = self.interpreter.tags()?;
382
383 let site_packages = SitePackages::from_environment(venv)?;
385
386 let Plan {
387 cached,
388 remote,
389 reinstalls,
390 extraneous: _,
391 } = Planner::new(resolution).build(
392 site_packages,
393 InstallationStrategy::Permissive,
394 &Reinstall::default(),
395 self.build_options,
396 hasher,
397 self.index_locations,
398 self.config_settings,
399 self.config_settings_package,
400 self.extra_build_requires(),
401 self.extra_build_variables,
402 self.cache(),
403 venv,
404 tags,
405 )?;
406
407 if remote.is_empty() && cached.is_empty() && reinstalls.is_empty() {
409 debug!("No build requirements to install for build");
410 return Ok(vec![]);
411 }
412
413 for dist in &remote {
415 let id = dist.distribution_id();
416 if build_stack.contains(&id) {
417 return Err(BuildDispatchError::BuildFrontend(
418 uv_build_frontend::Error::CyclicBuildDependency(dist.name().clone()).into(),
419 ));
420 }
421 }
422
423 let wheels = if remote.is_empty() {
425 vec![]
426 } else {
427 let preparer = Preparer::new(
428 self.cache,
429 tags,
430 hasher,
431 self.build_options,
432 DistributionDatabase::new(
433 self.client,
434 self,
435 self.concurrency.downloads_semaphore.clone(),
436 )
437 .with_build_stack(build_stack),
438 );
439
440 debug!(
441 "Downloading and building requirement{} for build: {}",
442 if remote.len() == 1 { "" } else { "s" },
443 remote.iter().map(ToString::to_string).join(", ")
444 );
445
446 preparer
447 .prepare(remote, &self.shared_state.in_flight, resolution)
448 .await?
449 };
450
451 if !reinstalls.is_empty() {
453 let layout = venv.interpreter().layout();
454 for dist_info in &reinstalls {
455 let summary = uv_installer::uninstall(dist_info, &layout)
456 .await
457 .context("Failed to uninstall build dependencies")?;
458 debug!(
459 "Uninstalled {} ({} file{}, {} director{})",
460 dist_info.name(),
461 summary.file_count,
462 if summary.file_count == 1 { "" } else { "s" },
463 summary.dir_count,
464 if summary.dir_count == 1 { "y" } else { "ies" },
465 );
466 }
467 }
468
469 let mut wheels = wheels.into_iter().chain(cached).collect::<Vec<_>>();
471 if !wheels.is_empty() {
472 debug!(
473 "Installing build requirement{}: {}",
474 if wheels.len() == 1 { "" } else { "s" },
475 wheels.iter().map(ToString::to_string).join(", ")
476 );
477 wheels = Installer::new(venv, self.preview)
478 .with_link_mode(self.link_mode)
479 .with_cache(self.cache)
480 .install(wheels)
481 .await
482 .context("Failed to install build dependencies")?;
483 }
484
485 Ok(wheels)
486 }
487
488 #[instrument(skip_all, fields(version_id = version_id, subdirectory = ?subdirectory))]
489 async fn setup_build<'data>(
490 &'data self,
491 source: &'data Path,
492 subdirectory: Option<&'data Path>,
493 install_path: &'data Path,
494 version_id: Option<&'data str>,
495 dist: Option<&'data SourceDist>,
496 sources: &'data NoSources,
497 build_kind: BuildKind,
498 build_output: BuildOutput,
499 mut build_stack: BuildStack,
500 ) -> Result<SourceBuild, uv_build_frontend::Error> {
501 let dist_name = dist.map(uv_distribution_types::Name::name);
502 let dist_version = dist
503 .map(uv_distribution_types::DistributionMetadata::version_or_url)
504 .and_then(|version| match version {
505 VersionOrUrlRef::Version(version) => Some(version),
506 VersionOrUrlRef::Url(_) => None,
507 });
508
509 if self
512 .build_options
513 .no_build_requirement(dist_name)
514 && !matches!(build_kind, BuildKind::Editable)
516 {
517 let err = if let Some(dist) = dist {
518 uv_build_frontend::Error::NoSourceDistBuild(dist.name().clone())
519 } else {
520 uv_build_frontend::Error::NoSourceDistBuilds
521 };
522 return Err(err);
523 }
524
525 if let Some(dist) = dist {
527 build_stack.insert(dist.distribution_id());
528 }
529
530 let config_settings = if let Some(name) = dist_name {
532 if let Some(package_settings) = self.config_settings_package.get(name) {
533 package_settings.clone().merge(self.config_settings.clone())
534 } else {
535 self.config_settings.clone()
536 }
537 } else {
538 self.config_settings.clone()
539 };
540
541 let mut environment_variables = self.build_extra_env_vars.clone();
543 if let Some(name) = dist_name {
544 if let Some(package_vars) = self.extra_build_variables.get(name) {
545 environment_variables.extend(
546 package_vars
547 .iter()
548 .map(|(key, value)| (OsString::from(key), OsString::from(value))),
549 );
550 }
551 }
552
553 let builder = SourceBuild::setup(
554 source,
555 subdirectory,
556 install_path,
557 dist_name,
558 dist_version,
559 self.interpreter,
560 self,
561 self.source_build_context.clone(),
562 version_id,
563 self.index_locations,
564 sources.clone(),
565 self.workspace_cache(),
566 config_settings,
567 self.build_isolation,
568 self.extra_build_requires,
569 &build_stack,
570 build_kind,
571 environment_variables,
572 build_output,
573 self.client.credentials_cache(),
574 )
575 .boxed_local()
576 .await?;
577 Ok(builder)
578 }
579
580 async fn direct_build<'data>(
581 &'data self,
582 source: &'data Path,
583 subdirectory: Option<&'data Path>,
584 output_dir: &'data Path,
585 sources: NoSources,
586 build_kind: BuildKind,
587 version_id: Option<&'data str>,
588 ) -> Result<Option<DistFilename>, BuildDispatchError> {
589 let source_tree = if let Some(subdir) = subdirectory {
590 source.join(subdir)
591 } else {
592 source.to_path_buf()
593 };
594
595 let source_tree_str = source_tree.display().to_string();
597 let identifier = version_id.unwrap_or_else(|| &source_tree_str);
598 if let Err(reason) = check_direct_build(&source_tree, uv_version::version()) {
599 trace!("Requirements for direct build not matched because {reason}");
600 return Ok(None);
601 }
602
603 debug!("Performing direct build for {identifier}");
604
605 let output_dir = output_dir.to_path_buf();
606 let filename = tokio::task::spawn_blocking(move || -> Result<_> {
607 let filename = match build_kind {
608 BuildKind::Wheel => {
609 let wheel = uv_build_backend::build_wheel(
610 &source_tree,
611 &output_dir,
612 None,
613 uv_version::version(),
614 sources.is_none(),
615 )?;
616 DistFilename::WheelFilename(wheel)
617 }
618 BuildKind::Sdist => {
619 let source_dist = uv_build_backend::build_source_dist(
620 &source_tree,
621 &output_dir,
622 uv_version::version(),
623 sources.is_none(),
624 )?;
625 DistFilename::SourceDistFilename(source_dist)
626 }
627 BuildKind::Editable => {
628 let wheel = uv_build_backend::build_editable(
629 &source_tree,
630 &output_dir,
631 None,
632 uv_version::version(),
633 sources.is_none(),
634 )?;
635 DistFilename::WheelFilename(wheel)
636 }
637 };
638 Ok(filename)
639 })
640 .await??;
641
642 Ok(Some(filename))
643 }
644}
645
646#[derive(Default, Clone)]
650pub struct SharedState {
651 git: GitResolver,
653 capabilities: IndexCapabilities,
655 index: InMemoryIndex,
657 in_flight: InFlight,
659 build_arena: BuildArena<SourceBuild>,
661}
662
663impl SharedState {
664 #[must_use]
669 pub fn fork(&self) -> Self {
670 Self {
671 git: self.git.clone(),
672 capabilities: self.capabilities.clone(),
673 build_arena: self.build_arena.clone(),
674 ..Default::default()
675 }
676 }
677
678 pub fn git(&self) -> &GitResolver {
680 &self.git
681 }
682
683 pub fn index(&self) -> &InMemoryIndex {
685 &self.index
686 }
687
688 pub fn in_flight(&self) -> &InFlight {
690 &self.in_flight
691 }
692
693 pub fn capabilities(&self) -> &IndexCapabilities {
695 &self.capabilities
696 }
697
698 pub fn build_arena(&self) -> &BuildArena<SourceBuild> {
700 &self.build_arena
701 }
702}