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