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, Excludes};
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 excludes = Excludes::default();
298 let (lookaheads, hasher) = LookaheadResolver::new(
299 requirements,
300 self.constraints,
301 &overrides,
302 &excludes,
303 &hasher,
304 &self.shared_state.index,
305 DistributionDatabase::new(
306 self.client,
307 self,
308 self.concurrency.downloads_semaphore.clone(),
309 )
310 .with_build_stack(build_stack),
311 )
312 .resolve(&resolver_env)
313 .await?;
314
315 let manifest = Manifest::simple(requirements.to_vec())
316 .with_constraints(self.constraints.clone())
317 .with_lookaheads(lookaheads);
318
319 let resolver = Resolver::new(
320 manifest,
321 OptionsBuilder::new()
322 .exclude_newer(self.exclude_newer.clone())
323 .index_strategy(self.index_strategy)
324 .build_options(self.build_options.clone())
325 .flexibility(Flexibility::Fixed)
326 .build(),
327 &python_requirement,
328 resolver_env,
329 self.interpreter.markers(),
330 Conflicts::empty(),
332 Some(tags),
333 self.flat_index,
334 &self.shared_state.index,
335 &hasher,
336 self,
337 EmptyInstalledPackages,
338 DistributionDatabase::new(
339 self.client,
340 self,
341 self.concurrency.downloads_semaphore.clone(),
342 )
343 .with_build_stack(build_stack),
344 )?;
345 let resolution = Resolution::from(resolver.resolve().await.with_context(|| {
346 format!(
347 "No solution found when resolving: {}",
348 requirements
349 .iter()
350 .map(|requirement| format!("`{requirement}`"))
351 .join(", ")
352 )
353 })?);
354 Ok(ResolvedRequirements::new(resolution, hasher))
355 }
356
357 #[instrument(
358 skip(self, requirements, venv),
359 fields(
360 resolution = requirements.resolution().distributions().map(ToString::to_string).join(", "),
361 venv = ?venv.root()
362 )
363 )]
364 async fn install<'data>(
365 &'data self,
366 requirements: &'data ResolvedRequirements,
367 venv: &'data PythonEnvironment,
368 build_stack: &'data BuildStack,
369 ) -> Result<Vec<CachedDist>, BuildDispatchError> {
370 let resolution = requirements.resolution();
371 let hasher = requirements.hasher();
372
373 debug!(
374 "Installing in {} in {}",
375 resolution
376 .distributions()
377 .map(ToString::to_string)
378 .join(", "),
379 venv.root().display(),
380 );
381
382 let tags = self.interpreter.tags()?;
384
385 let site_packages = SitePackages::from_environment(venv)?;
387
388 let Plan {
389 cached,
390 remote,
391 reinstalls,
392 extraneous: _,
393 } = Planner::new(resolution).build(
394 site_packages,
395 InstallationStrategy::Permissive,
396 &Reinstall::default(),
397 self.build_options,
398 hasher,
399 self.index_locations,
400 self.config_settings,
401 self.config_settings_package,
402 self.extra_build_requires(),
403 self.extra_build_variables,
404 self.cache(),
405 venv,
406 tags,
407 )?;
408
409 if remote.is_empty() && cached.is_empty() && reinstalls.is_empty() {
411 debug!("No build requirements to install for build");
412 return Ok(vec![]);
413 }
414
415 for dist in &remote {
417 let id = dist.distribution_id();
418 if build_stack.contains(&id) {
419 return Err(BuildDispatchError::BuildFrontend(
420 uv_build_frontend::Error::CyclicBuildDependency(dist.name().clone()).into(),
421 ));
422 }
423 }
424
425 let wheels = if remote.is_empty() {
427 vec![]
428 } else {
429 let preparer = Preparer::new(
430 self.cache,
431 tags,
432 hasher,
433 self.build_options,
434 DistributionDatabase::new(
435 self.client,
436 self,
437 self.concurrency.downloads_semaphore.clone(),
438 )
439 .with_build_stack(build_stack),
440 );
441
442 debug!(
443 "Downloading and building requirement{} for build: {}",
444 if remote.len() == 1 { "" } else { "s" },
445 remote.iter().map(ToString::to_string).join(", ")
446 );
447
448 preparer
449 .prepare(remote, &self.shared_state.in_flight, resolution)
450 .await?
451 };
452
453 if !reinstalls.is_empty() {
455 let layout = venv.interpreter().layout();
456 for dist_info in &reinstalls {
457 let summary = uv_installer::uninstall(dist_info, &layout)
458 .await
459 .context("Failed to uninstall build dependencies")?;
460 debug!(
461 "Uninstalled {} ({} file{}, {} director{})",
462 dist_info.name(),
463 summary.file_count,
464 if summary.file_count == 1 { "" } else { "s" },
465 summary.dir_count,
466 if summary.dir_count == 1 { "y" } else { "ies" },
467 );
468 }
469 }
470
471 let mut wheels = wheels.into_iter().chain(cached).collect::<Vec<_>>();
473 if !wheels.is_empty() {
474 debug!(
475 "Installing build requirement{}: {}",
476 if wheels.len() == 1 { "" } else { "s" },
477 wheels.iter().map(ToString::to_string).join(", ")
478 );
479 wheels = Installer::new(venv, self.preview)
480 .with_link_mode(self.link_mode)
481 .with_cache(self.cache)
482 .install(wheels)
483 .await
484 .context("Failed to install build dependencies")?;
485 }
486
487 Ok(wheels)
488 }
489
490 #[instrument(skip_all, fields(version_id = version_id, subdirectory = ?subdirectory))]
491 async fn setup_build<'data>(
492 &'data self,
493 source: &'data Path,
494 subdirectory: Option<&'data Path>,
495 install_path: &'data Path,
496 stop_discovery_at: Option<&'data Path>,
497 version_id: Option<&'data str>,
498 dist: Option<&'data SourceDist>,
499 sources: &'data NoSources,
500 build_kind: BuildKind,
501 build_output: BuildOutput,
502 mut build_stack: BuildStack,
503 ) -> Result<SourceBuild, uv_build_frontend::Error> {
504 let dist_name = dist.map(uv_distribution_types::Name::name);
505 let dist_version = dist
506 .map(uv_distribution_types::DistributionMetadata::version_or_url)
507 .and_then(|version| match version {
508 VersionOrUrlRef::Version(version) => Some(version),
509 VersionOrUrlRef::Url(_) => None,
510 });
511
512 if self
515 .build_options
516 .no_build_requirement(dist_name)
517 && !matches!(build_kind, BuildKind::Editable)
519 {
520 let err = if let Some(dist) = dist {
521 uv_build_frontend::Error::NoSourceDistBuild(dist.name().clone())
522 } else {
523 uv_build_frontend::Error::NoSourceDistBuilds
524 };
525 return Err(err);
526 }
527
528 if let Some(dist) = dist {
530 build_stack.insert(dist.distribution_id());
531 }
532
533 let config_settings = if let Some(name) = dist_name {
535 if let Some(package_settings) = self.config_settings_package.get(name) {
536 package_settings.clone().merge(self.config_settings.clone())
537 } else {
538 self.config_settings.clone()
539 }
540 } else {
541 self.config_settings.clone()
542 };
543
544 let mut environment_variables = self.build_extra_env_vars.clone();
546 if let Some(name) = dist_name {
547 if let Some(package_vars) = self.extra_build_variables.get(name) {
548 environment_variables.extend(
549 package_vars
550 .iter()
551 .map(|(key, value)| (OsString::from(key), OsString::from(value))),
552 );
553 }
554 }
555
556 let builder = SourceBuild::setup(
557 source,
558 subdirectory,
559 install_path,
560 stop_discovery_at,
561 dist_name,
562 dist_version,
563 self.interpreter,
564 self,
565 self.source_build_context.clone(),
566 version_id,
567 self.index_locations,
568 sources.clone(),
569 self.workspace_cache(),
570 config_settings,
571 self.build_isolation,
572 self.extra_build_requires,
573 &build_stack,
574 build_kind,
575 environment_variables,
576 build_output,
577 self.client.credentials_cache(),
578 )
579 .boxed_local()
580 .await?;
581 Ok(builder)
582 }
583
584 async fn direct_build<'data>(
585 &'data self,
586 source: &'data Path,
587 subdirectory: Option<&'data Path>,
588 output_dir: &'data Path,
589 sources: NoSources,
590 build_kind: BuildKind,
591 version_id: Option<&'data str>,
592 ) -> Result<Option<DistFilename>, BuildDispatchError> {
593 let source_tree = if let Some(subdir) = subdirectory {
594 source.join(subdir)
595 } else {
596 source.to_path_buf()
597 };
598
599 let source_tree_str = source_tree.display().to_string();
601 let identifier = version_id.unwrap_or_else(|| &source_tree_str);
602 if let Err(reason) = check_direct_build(&source_tree, uv_version::version()) {
603 trace!("Requirements for direct build not matched because {reason}");
604 return Ok(None);
605 }
606
607 debug!("Performing direct build for {identifier}");
608
609 let output_dir = output_dir.to_path_buf();
610 let filename = tokio::task::spawn_blocking(move || -> Result<_> {
611 let filename = match build_kind {
612 BuildKind::Wheel => {
613 let wheel = uv_build_backend::build_wheel(
614 &source_tree,
615 &output_dir,
616 None,
617 uv_version::version(),
618 sources.is_none(),
619 )?;
620 DistFilename::WheelFilename(wheel)
621 }
622 BuildKind::Sdist => {
623 let source_dist = uv_build_backend::build_source_dist(
624 &source_tree,
625 &output_dir,
626 uv_version::version(),
627 sources.is_none(),
628 )?;
629 DistFilename::SourceDistFilename(source_dist)
630 }
631 BuildKind::Editable => {
632 let wheel = uv_build_backend::build_editable(
633 &source_tree,
634 &output_dir,
635 None,
636 uv_version::version(),
637 sources.is_none(),
638 )?;
639 DistFilename::WheelFilename(wheel)
640 }
641 };
642 Ok(filename)
643 })
644 .await??;
645
646 Ok(Some(filename))
647 }
648}
649
650#[derive(Default, Clone)]
654pub struct SharedState {
655 git: GitResolver,
657 capabilities: IndexCapabilities,
659 index: InMemoryIndex,
661 in_flight: InFlight,
663 build_arena: BuildArena<SourceBuild>,
665}
666
667impl SharedState {
668 #[must_use]
673 pub fn fork(&self) -> Self {
674 Self {
675 git: self.git.clone(),
676 capabilities: self.capabilities.clone(),
677 build_arena: self.build_arena.clone(),
678 ..Default::default()
679 }
680 }
681
682 pub fn git(&self) -> &GitResolver {
684 &self.git
685 }
686
687 pub fn index(&self) -> &InMemoryIndex {
689 &self.index
690 }
691
692 pub fn in_flight(&self) -> &InFlight {
694 &self.in_flight
695 }
696}