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