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