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