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