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