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