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