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