Skip to main content

uv_installer/
installer.rs

1use std::convert;
2use std::sync::{Arc, LazyLock};
3
4use anyhow::{Context, Error, Result};
5use rayon::iter::{IntoParallelRefIterator, ParallelIterator};
6use tokio::sync::oneshot;
7use tracing::{instrument, warn};
8
9use uv_cache::Cache;
10use uv_configuration::RAYON_INITIALIZE;
11use uv_distribution_types::CachedDist;
12use uv_install_wheel::{Layout, LinkMode};
13use uv_preview::Preview;
14use uv_python::PythonEnvironment;
15
16pub struct Installer<'a> {
17    venv: &'a PythonEnvironment,
18    link_mode: LinkMode,
19    cache: Option<&'a Cache>,
20    reporter: Option<Arc<dyn Reporter>>,
21    /// The name of the [`Installer`].
22    name: Option<String>,
23    /// The metadata associated with the [`Installer`].
24    metadata: bool,
25    /// Preview settings for the installer.
26    preview: Preview,
27}
28
29impl<'a> Installer<'a> {
30    /// Initialize a new installer.
31    pub fn new(venv: &'a PythonEnvironment, preview: Preview) -> Self {
32        Self {
33            venv,
34            link_mode: LinkMode::default(),
35            cache: None,
36            reporter: None,
37            name: Some("uv".to_string()),
38            metadata: true,
39            preview,
40        }
41    }
42
43    /// Set the [`LinkMode`][`uv_install_wheel::LinkMode`] to use for this installer.
44    #[must_use]
45    pub fn with_link_mode(self, link_mode: LinkMode) -> Self {
46        Self { link_mode, ..self }
47    }
48
49    /// Set the [`Cache`] to use for this installer.
50    #[must_use]
51    pub fn with_cache(self, cache: &'a Cache) -> Self {
52        Self {
53            cache: Some(cache),
54            ..self
55        }
56    }
57
58    /// Set the [`Reporter`] to use for this installer.
59    #[must_use]
60    pub fn with_reporter(self, reporter: Arc<dyn Reporter>) -> Self {
61        Self {
62            reporter: Some(reporter),
63            ..self
64        }
65    }
66
67    /// Set the `installer_name` to something other than `"uv"`.
68    #[must_use]
69    pub fn with_installer_name(self, installer_name: Option<String>) -> Self {
70        Self {
71            name: installer_name,
72            ..self
73        }
74    }
75
76    /// Set whether to install uv-specifier files in the dist-info directory.
77    #[must_use]
78    pub fn with_installer_metadata(self, installer_metadata: bool) -> Self {
79        Self {
80            metadata: installer_metadata,
81            ..self
82        }
83    }
84
85    /// Install a set of wheels into a Python virtual environment.
86    #[instrument(skip_all, fields(num_wheels = %wheels.len()))]
87    pub async fn install(self, wheels: Vec<CachedDist>) -> Result<Vec<CachedDist>> {
88        let Self {
89            venv,
90            cache,
91            link_mode,
92            reporter,
93            name: installer_name,
94            metadata: installer_metadata,
95            preview,
96        } = self;
97
98        if cache.is_some_and(Cache::is_temporary) {
99            if link_mode.is_symlink() {
100                return Err(anyhow::anyhow!(
101                    "Symlink-based installation is not supported with `--no-cache`. The created environment will be rendered unusable by the removal of the cache."
102                ));
103            }
104        }
105
106        let (tx, rx) = oneshot::channel();
107
108        let layout = venv.interpreter().layout();
109        let relocatable = venv.relocatable();
110        // Initialize the threadpool with the user settings.
111        LazyLock::force(&RAYON_INITIALIZE);
112        rayon::spawn(move || {
113            let result = install(
114                wheels,
115                &layout,
116                installer_name.as_deref(),
117                link_mode,
118                reporter.as_ref(),
119                relocatable,
120                installer_metadata,
121                preview,
122            );
123
124            // This may fail if the main task was cancelled.
125            let _ = tx.send(result);
126        });
127
128        rx.await
129            .map_err(|_| anyhow::anyhow!("`install_blocking` task panicked"))
130            .and_then(convert::identity)
131    }
132
133    /// Install a set of wheels into a Python virtual environment synchronously.
134    #[instrument(skip_all, fields(num_wheels = %wheels.len()))]
135    pub fn install_blocking(self, wheels: Vec<CachedDist>) -> Result<Vec<CachedDist>> {
136        if self.cache.is_some_and(Cache::is_temporary) {
137            if self.link_mode.is_symlink() {
138                return Err(anyhow::anyhow!(
139                    "Symlink-based installation is not supported with `--no-cache`. The created environment will be rendered unusable by the removal of the cache."
140                ));
141            }
142        }
143
144        install(
145            wheels,
146            &self.venv.interpreter().layout(),
147            self.name.as_deref(),
148            self.link_mode,
149            self.reporter.as_ref(),
150            self.venv.relocatable(),
151            self.metadata,
152            self.preview,
153        )
154    }
155}
156
157/// Install a set of wheels into a Python virtual environment synchronously.
158#[instrument(skip_all, fields(num_wheels = %wheels.len()))]
159fn install(
160    wheels: Vec<CachedDist>,
161    layout: &Layout,
162    installer_name: Option<&str>,
163    link_mode: LinkMode,
164    reporter: Option<&Arc<dyn Reporter>>,
165    relocatable: bool,
166    installer_metadata: bool,
167    preview: Preview,
168) -> Result<Vec<CachedDist>> {
169    // Initialize the threadpool with the user settings.
170    LazyLock::force(&RAYON_INITIALIZE);
171    let locks = uv_install_wheel::Locks::new(preview);
172    wheels.par_iter().try_for_each(|wheel| {
173        uv_install_wheel::install_wheel(
174            layout,
175            relocatable,
176            wheel.path(),
177            wheel.filename(),
178            wheel
179                .parsed_url()
180                .map(uv_pypi_types::DirectUrl::from)
181                .as_ref(),
182            if wheel.cache_info().is_empty() {
183                None
184            } else {
185                Some(wheel.cache_info())
186            },
187            wheel.build_info(),
188            installer_name,
189            installer_metadata,
190            link_mode,
191            &locks,
192        )
193        .with_context(|| format!("Failed to install: {} ({wheel})", wheel.filename()))?;
194
195        if let Some(reporter) = reporter.as_ref() {
196            reporter.on_install_progress(wheel);
197        }
198
199        Ok::<(), Error>(())
200    })?;
201    if let Err(err) = locks.warn_package_conflicts() {
202        warn!("Checking for conflicts between packages failed: {err}");
203    }
204
205    Ok(wheels)
206}
207
208pub trait Reporter: Send + Sync {
209    /// Callback to invoke when a dependency is installed.
210    fn on_install_progress(&self, wheel: &CachedDist);
211
212    /// Callback to invoke when the resolution is complete.
213    fn on_install_complete(&self);
214}