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