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, Excludes};
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 excludes = Excludes::default();
298        let (lookaheads, hasher) = LookaheadResolver::new(
299            requirements,
300            self.constraints,
301            &overrides,
302            &excludes,
303            &hasher,
304            &self.shared_state.index,
305            DistributionDatabase::new(
306                self.client,
307                self,
308                self.concurrency.downloads_semaphore.clone(),
309            )
310            .with_build_stack(build_stack),
311        )
312        .resolve(&resolver_env)
313        .await?;
314
315        let manifest = Manifest::simple(requirements.to_vec())
316            .with_constraints(self.constraints.clone())
317            .with_lookaheads(lookaheads);
318
319        let resolver = Resolver::new(
320            manifest,
321            OptionsBuilder::new()
322                .exclude_newer(self.exclude_newer.clone())
323                .index_strategy(self.index_strategy)
324                .build_options(self.build_options.clone())
325                .flexibility(Flexibility::Fixed)
326                .build(),
327            &python_requirement,
328            resolver_env,
329            self.interpreter.markers(),
330            // Conflicting groups only make sense when doing universal resolution.
331            Conflicts::empty(),
332            Some(tags),
333            self.flat_index,
334            &self.shared_state.index,
335            &hasher,
336            self,
337            EmptyInstalledPackages,
338            DistributionDatabase::new(
339                self.client,
340                self,
341                self.concurrency.downloads_semaphore.clone(),
342            )
343            .with_build_stack(build_stack),
344        )?;
345        let resolution = Resolution::from(resolver.resolve().await.with_context(|| {
346            format!(
347                "No solution found when resolving: {}",
348                requirements
349                    .iter()
350                    .map(|requirement| format!("`{requirement}`"))
351                    .join(", ")
352            )
353        })?);
354        Ok(ResolvedRequirements::new(resolution, hasher))
355    }
356
357    #[instrument(
358        skip(self, requirements, venv),
359        fields(
360            resolution = requirements.resolution().distributions().map(ToString::to_string).join(", "),
361            venv = ?venv.root()
362        )
363    )]
364    async fn install<'data>(
365        &'data self,
366        requirements: &'data ResolvedRequirements,
367        venv: &'data PythonEnvironment,
368        build_stack: &'data BuildStack,
369    ) -> Result<Vec<CachedDist>, BuildDispatchError> {
370        let resolution = requirements.resolution();
371        let hasher = requirements.hasher();
372
373        debug!(
374            "Installing in {} in {}",
375            resolution
376                .distributions()
377                .map(ToString::to_string)
378                .join(", "),
379            venv.root().display(),
380        );
381
382        // Determine the current environment markers.
383        let tags = self.interpreter.tags()?;
384
385        // Determine the set of installed packages.
386        let site_packages = SitePackages::from_environment(venv)?;
387
388        let Plan {
389            cached,
390            remote,
391            reinstalls,
392            extraneous: _,
393        } = Planner::new(resolution).build(
394            site_packages,
395            InstallationStrategy::Permissive,
396            &Reinstall::default(),
397            self.build_options,
398            hasher,
399            self.index_locations,
400            self.config_settings,
401            self.config_settings_package,
402            self.extra_build_requires(),
403            self.extra_build_variables,
404            self.cache(),
405            venv,
406            tags,
407        )?;
408
409        // Nothing to do.
410        if remote.is_empty() && cached.is_empty() && reinstalls.is_empty() {
411            debug!("No build requirements to install for build");
412            return Ok(vec![]);
413        }
414
415        // Verify that none of the missing distributions are already in the build stack.
416        for dist in &remote {
417            let id = dist.distribution_id();
418            if build_stack.contains(&id) {
419                return Err(BuildDispatchError::BuildFrontend(
420                    uv_build_frontend::Error::CyclicBuildDependency(dist.name().clone()).into(),
421                ));
422            }
423        }
424
425        // Download any missing distributions.
426        let wheels = if remote.is_empty() {
427            vec![]
428        } else {
429            let preparer = Preparer::new(
430                self.cache,
431                tags,
432                hasher,
433                self.build_options,
434                DistributionDatabase::new(
435                    self.client,
436                    self,
437                    self.concurrency.downloads_semaphore.clone(),
438                )
439                .with_build_stack(build_stack),
440            );
441
442            debug!(
443                "Downloading and building requirement{} for build: {}",
444                if remote.len() == 1 { "" } else { "s" },
445                remote.iter().map(ToString::to_string).join(", ")
446            );
447
448            preparer
449                .prepare(remote, &self.shared_state.in_flight, resolution)
450                .await?
451        };
452
453        // Remove any unnecessary packages.
454        if !reinstalls.is_empty() {
455            let layout = venv.interpreter().layout();
456            for dist_info in &reinstalls {
457                let summary = uv_installer::uninstall(dist_info, &layout)
458                    .await
459                    .context("Failed to uninstall build dependencies")?;
460                debug!(
461                    "Uninstalled {} ({} file{}, {} director{})",
462                    dist_info.name(),
463                    summary.file_count,
464                    if summary.file_count == 1 { "" } else { "s" },
465                    summary.dir_count,
466                    if summary.dir_count == 1 { "y" } else { "ies" },
467                );
468            }
469        }
470
471        // Install the resolved distributions.
472        let mut wheels = wheels.into_iter().chain(cached).collect::<Vec<_>>();
473        if !wheels.is_empty() {
474            debug!(
475                "Installing build requirement{}: {}",
476                if wheels.len() == 1 { "" } else { "s" },
477                wheels.iter().map(ToString::to_string).join(", ")
478            );
479            wheels = Installer::new(venv, self.preview)
480                .with_link_mode(self.link_mode)
481                .with_cache(self.cache)
482                .install(wheels)
483                .await
484                .context("Failed to install build dependencies")?;
485        }
486
487        Ok(wheels)
488    }
489
490    #[instrument(skip_all, fields(version_id = version_id, subdirectory = ?subdirectory))]
491    async fn setup_build<'data>(
492        &'data self,
493        source: &'data Path,
494        subdirectory: Option<&'data Path>,
495        install_path: &'data Path,
496        stop_discovery_at: Option<&'data Path>,
497        version_id: Option<&'data str>,
498        dist: Option<&'data SourceDist>,
499        sources: &'data NoSources,
500        build_kind: BuildKind,
501        build_output: BuildOutput,
502        mut build_stack: BuildStack,
503    ) -> Result<SourceBuild, uv_build_frontend::Error> {
504        let dist_name = dist.map(uv_distribution_types::Name::name);
505        let dist_version = dist
506            .map(uv_distribution_types::DistributionMetadata::version_or_url)
507            .and_then(|version| match version {
508                VersionOrUrlRef::Version(version) => Some(version),
509                VersionOrUrlRef::Url(_) => None,
510            });
511
512        // Note we can only prevent builds by name for packages with names
513        // unless all builds are disabled.
514        if self
515            .build_options
516            .no_build_requirement(dist_name)
517            // We always allow editable builds
518            && !matches!(build_kind, BuildKind::Editable)
519        {
520            let err = if let Some(dist) = dist {
521                uv_build_frontend::Error::NoSourceDistBuild(dist.name().clone())
522            } else {
523                uv_build_frontend::Error::NoSourceDistBuilds
524            };
525            return Err(err);
526        }
527
528        // Push the current distribution onto the build stack, to prevent cyclic dependencies.
529        if let Some(dist) = dist {
530            build_stack.insert(dist.distribution_id());
531        }
532
533        // Get package-specific config settings if available; otherwise, use global settings.
534        let config_settings = if let Some(name) = dist_name {
535            if let Some(package_settings) = self.config_settings_package.get(name) {
536                package_settings.clone().merge(self.config_settings.clone())
537            } else {
538                self.config_settings.clone()
539            }
540        } else {
541            self.config_settings.clone()
542        };
543
544        // Get package-specific environment variables if available.
545        let mut environment_variables = self.build_extra_env_vars.clone();
546        if let Some(name) = dist_name {
547            if let Some(package_vars) = self.extra_build_variables.get(name) {
548                environment_variables.extend(
549                    package_vars
550                        .iter()
551                        .map(|(key, value)| (OsString::from(key), OsString::from(value))),
552                );
553            }
554        }
555
556        let builder = SourceBuild::setup(
557            source,
558            subdirectory,
559            install_path,
560            stop_discovery_at,
561            dist_name,
562            dist_version,
563            self.interpreter,
564            self,
565            self.source_build_context.clone(),
566            version_id,
567            self.index_locations,
568            sources.clone(),
569            self.workspace_cache(),
570            config_settings,
571            self.build_isolation,
572            self.extra_build_requires,
573            &build_stack,
574            build_kind,
575            environment_variables,
576            build_output,
577            self.client.credentials_cache(),
578        )
579        .boxed_local()
580        .await?;
581        Ok(builder)
582    }
583
584    async fn direct_build<'data>(
585        &'data self,
586        source: &'data Path,
587        subdirectory: Option<&'data Path>,
588        output_dir: &'data Path,
589        sources: NoSources,
590        build_kind: BuildKind,
591        version_id: Option<&'data str>,
592    ) -> Result<Option<DistFilename>, BuildDispatchError> {
593        let source_tree = if let Some(subdir) = subdirectory {
594            source.join(subdir)
595        } else {
596            source.to_path_buf()
597        };
598
599        // Only perform the direct build if the backend is uv in a compatible version.
600        let source_tree_str = source_tree.display().to_string();
601        let identifier = version_id.unwrap_or_else(|| &source_tree_str);
602        if let Err(reason) = check_direct_build(&source_tree, uv_version::version()) {
603            trace!("Requirements for direct build not matched because {reason}");
604            return Ok(None);
605        }
606
607        debug!("Performing direct build for {identifier}");
608
609        let output_dir = output_dir.to_path_buf();
610        let filename = tokio::task::spawn_blocking(move || -> Result<_> {
611            let filename = match build_kind {
612                BuildKind::Wheel => {
613                    let wheel = uv_build_backend::build_wheel(
614                        &source_tree,
615                        &output_dir,
616                        None,
617                        uv_version::version(),
618                        sources.is_none(),
619                    )?;
620                    DistFilename::WheelFilename(wheel)
621                }
622                BuildKind::Sdist => {
623                    let source_dist = uv_build_backend::build_source_dist(
624                        &source_tree,
625                        &output_dir,
626                        uv_version::version(),
627                        sources.is_none(),
628                    )?;
629                    DistFilename::SourceDistFilename(source_dist)
630                }
631                BuildKind::Editable => {
632                    let wheel = uv_build_backend::build_editable(
633                        &source_tree,
634                        &output_dir,
635                        None,
636                        uv_version::version(),
637                        sources.is_none(),
638                    )?;
639                    DistFilename::WheelFilename(wheel)
640                }
641            };
642            Ok(filename)
643        })
644        .await??;
645
646        Ok(Some(filename))
647    }
648}
649
650/// Shared state used during resolution and installation.
651///
652/// All elements are `Arc`s, so we can clone freely.
653#[derive(Default, Clone)]
654pub struct SharedState {
655    /// The resolved Git references.
656    git: GitResolver,
657    /// The discovered capabilities for each registry index.
658    capabilities: IndexCapabilities,
659    /// The fetched package versions and metadata.
660    index: InMemoryIndex,
661    /// The downloaded distributions.
662    in_flight: InFlight,
663    /// Build directories for any PEP 517 builds executed during resolution or installation.
664    build_arena: BuildArena<SourceBuild>,
665}
666
667impl SharedState {
668    /// Fork the [`SharedState`], creating a new in-memory index and in-flight cache.
669    ///
670    /// State that is universally applicable (like the Git resolver and index capabilities)
671    /// are retained.
672    #[must_use]
673    pub fn fork(&self) -> Self {
674        Self {
675            git: self.git.clone(),
676            capabilities: self.capabilities.clone(),
677            build_arena: self.build_arena.clone(),
678            ..Default::default()
679        }
680    }
681
682    /// Return the [`GitResolver`] used by the [`SharedState`].
683    pub fn git(&self) -> &GitResolver {
684        &self.git
685    }
686
687    /// Return the [`InMemoryIndex`] used by the [`SharedState`].
688    pub fn index(&self) -> &InMemoryIndex {
689        &self.index
690    }
691
692    /// Return the [`InFlight`] used by the [`SharedState`].
693    pub fn in_flight(&self) -> &InFlight {
694        &self.in_flight
695    }
696}