Skip to main content

uv_dispatch/
lib.rs

1//! Avoid cyclic crate dependencies between [resolver][`uv_resolver`],
2//! [installer][`uv_installer`] and [build][`uv_build`] through [`BuildDispatch`]
3//! implementing [`BuildContext`].
4
5use 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                // Walk the anyhow error chain to find hint-bearing errors
78                // (e.g., ResolveError wrapped via `with_context`).
79                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
108/// The main implementation of [`BuildContext`], used by the CLI, see [`BuildContext`]
109/// documentation.
110pub 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    /// Set the environment variables to be used when building a source distribution.
194    #[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        // Walk any URL requirements transitively so their sub-URLs (for example, a workspace
288        // member that depends on another workspace member) are known before the resolver runs
289        // its URL allow-list check. This mirrors what the project resolver does in
290        // `uv_requirements::LookaheadResolver` and prevents a `DisallowedUrl` error when one
291        // `build-system.requires` entry pulls in another URL dependency.
292        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            // Conflicting groups only make sense when doing universal resolution.
332            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        // Determine the current environment markers.
384        let tags = self.interpreter.tags()?;
385
386        // Determine the set of installed packages.
387        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        // Nothing to do.
411        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        // Verify that none of the missing distributions are already in the build stack.
417        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        // Download any missing distributions.
427        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        // Remove any unnecessary packages.
455        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        // Install the resolved distributions.
473        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        // Push the current distribution onto the build stack, to prevent cyclic dependencies.
514        if let Some(dist) = dist {
515            build_stack.insert(dist.distribution_id());
516        }
517
518        // Get package-specific config settings if available; otherwise, use global settings.
519        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        // Get package-specific environment variables if available.
530        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        // Only perform the direct build if the backend is uv in a compatible version.
585        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/// Shared state used during resolution and installation.
636///
637/// All elements are `Arc`s, so we can clone freely.
638#[derive(Default, Clone)]
639pub struct SharedState {
640    /// The resolved Git references.
641    git: GitResolver,
642    /// The discovered capabilities for each registry index.
643    capabilities: IndexCapabilities,
644    /// The fetched package versions and metadata.
645    index: InMemoryIndex,
646    /// The downloaded distributions.
647    in_flight: InFlight,
648    /// Build directories for any PEP 517 builds executed during resolution or installation.
649    build_arena: BuildArena<SourceBuild>,
650}
651
652impl SharedState {
653    /// Fork the [`SharedState`], creating a new in-memory index and in-flight cache.
654    ///
655    /// State that is universally applicable (like the Git resolver and index capabilities)
656    /// are retained.
657    #[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    /// Return the [`GitResolver`] used by the [`SharedState`].
668    pub fn git(&self) -> &GitResolver {
669        &self.git
670    }
671
672    /// Return the [`InMemoryIndex`] used by the [`SharedState`].
673    pub fn index(&self) -> &InMemoryIndex {
674        &self.index
675    }
676
677    /// Return the [`InFlight`] used by the [`SharedState`].
678    pub fn in_flight(&self) -> &InFlight {
679        &self.in_flight
680    }
681}