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