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        stop_discovery_at: Option<&'data Path>,
495        version_id: Option<&'data str>,
496        dist: Option<&'data SourceDist>,
497        sources: &'data NoSources,
498        build_kind: BuildKind,
499        build_output: BuildOutput,
500        mut build_stack: BuildStack,
501    ) -> Result<SourceBuild, uv_build_frontend::Error> {
502        let dist_name = dist.map(uv_distribution_types::Name::name);
503        let dist_version = dist
504            .map(uv_distribution_types::DistributionMetadata::version_or_url)
505            .and_then(|version| match version {
506                VersionOrUrlRef::Version(version) => Some(version),
507                VersionOrUrlRef::Url(_) => None,
508            });
509
510        // Note we can only prevent builds by name for packages with names
511        // unless all builds are disabled.
512        if self
513            .build_options
514            .no_build_requirement(dist_name)
515            // We always allow editable builds
516            && !matches!(build_kind, BuildKind::Editable)
517        {
518            let err = if let Some(dist) = dist {
519                uv_build_frontend::Error::NoSourceDistBuild(dist.name().clone())
520            } else {
521                uv_build_frontend::Error::NoSourceDistBuilds
522            };
523            return Err(err);
524        }
525
526        // Push the current distribution onto the build stack, to prevent cyclic dependencies.
527        if let Some(dist) = dist {
528            build_stack.insert(dist.distribution_id());
529        }
530
531        // Get package-specific config settings if available; otherwise, use global settings.
532        let config_settings = if let Some(name) = dist_name {
533            if let Some(package_settings) = self.config_settings_package.get(name) {
534                package_settings.clone().merge(self.config_settings.clone())
535            } else {
536                self.config_settings.clone()
537            }
538        } else {
539            self.config_settings.clone()
540        };
541
542        // Get package-specific environment variables if available.
543        let mut environment_variables = self.build_extra_env_vars.clone();
544        if let Some(name) = dist_name {
545            if let Some(package_vars) = self.extra_build_variables.get(name) {
546                environment_variables.extend(
547                    package_vars
548                        .iter()
549                        .map(|(key, value)| (OsString::from(key), OsString::from(value))),
550                );
551            }
552        }
553
554        let builder = SourceBuild::setup(
555            source,
556            subdirectory,
557            install_path,
558            stop_discovery_at,
559            dist_name,
560            dist_version,
561            self.interpreter,
562            self,
563            self.source_build_context.clone(),
564            version_id,
565            self.index_locations,
566            sources.clone(),
567            self.workspace_cache(),
568            config_settings,
569            self.build_isolation,
570            self.extra_build_requires,
571            &build_stack,
572            build_kind,
573            environment_variables,
574            build_output,
575            self.client.credentials_cache(),
576        )
577        .boxed_local()
578        .await?;
579        Ok(builder)
580    }
581
582    async fn direct_build<'data>(
583        &'data self,
584        source: &'data Path,
585        subdirectory: Option<&'data Path>,
586        output_dir: &'data Path,
587        sources: NoSources,
588        build_kind: BuildKind,
589        version_id: Option<&'data str>,
590    ) -> Result<Option<DistFilename>, BuildDispatchError> {
591        let source_tree = if let Some(subdir) = subdirectory {
592            source.join(subdir)
593        } else {
594            source.to_path_buf()
595        };
596
597        // Only perform the direct build if the backend is uv in a compatible version.
598        let source_tree_str = source_tree.display().to_string();
599        let identifier = version_id.unwrap_or_else(|| &source_tree_str);
600        if let Err(reason) = check_direct_build(&source_tree, uv_version::version()) {
601            trace!("Requirements for direct build not matched because {reason}");
602            return Ok(None);
603        }
604
605        debug!("Performing direct build for {identifier}");
606
607        let output_dir = output_dir.to_path_buf();
608        let filename = tokio::task::spawn_blocking(move || -> Result<_> {
609            let filename = match build_kind {
610                BuildKind::Wheel => {
611                    let wheel = uv_build_backend::build_wheel(
612                        &source_tree,
613                        &output_dir,
614                        None,
615                        uv_version::version(),
616                        sources.is_none(),
617                    )?;
618                    DistFilename::WheelFilename(wheel)
619                }
620                BuildKind::Sdist => {
621                    let source_dist = uv_build_backend::build_source_dist(
622                        &source_tree,
623                        &output_dir,
624                        uv_version::version(),
625                        sources.is_none(),
626                    )?;
627                    DistFilename::SourceDistFilename(source_dist)
628                }
629                BuildKind::Editable => {
630                    let wheel = uv_build_backend::build_editable(
631                        &source_tree,
632                        &output_dir,
633                        None,
634                        uv_version::version(),
635                        sources.is_none(),
636                    )?;
637                    DistFilename::WheelFilename(wheel)
638                }
639            };
640            Ok(filename)
641        })
642        .await??;
643
644        Ok(Some(filename))
645    }
646}
647
648/// Shared state used during resolution and installation.
649///
650/// All elements are `Arc`s, so we can clone freely.
651#[derive(Default, Clone)]
652pub struct SharedState {
653    /// The resolved Git references.
654    git: GitResolver,
655    /// The discovered capabilities for each registry index.
656    capabilities: IndexCapabilities,
657    /// The fetched package versions and metadata.
658    index: InMemoryIndex,
659    /// The downloaded distributions.
660    in_flight: InFlight,
661    /// Build directories for any PEP 517 builds executed during resolution or installation.
662    build_arena: BuildArena<SourceBuild>,
663}
664
665impl SharedState {
666    /// Fork the [`SharedState`], creating a new in-memory index and in-flight cache.
667    ///
668    /// State that is universally applicable (like the Git resolver and index capabilities)
669    /// are retained.
670    #[must_use]
671    pub fn fork(&self) -> Self {
672        Self {
673            git: self.git.clone(),
674            capabilities: self.capabilities.clone(),
675            build_arena: self.build_arena.clone(),
676            ..Default::default()
677        }
678    }
679
680    /// Return the [`GitResolver`] used by the [`SharedState`].
681    pub fn git(&self) -> &GitResolver {
682        &self.git
683    }
684
685    /// Return the [`InMemoryIndex`] used by the [`SharedState`].
686    pub fn index(&self) -> &InMemoryIndex {
687        &self.index
688    }
689
690    /// Return the [`InFlight`] used by the [`SharedState`].
691    pub fn in_flight(&self) -> &InFlight {
692        &self.in_flight
693    }
694}