uv_installer/
installer.rs1use 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 name: Option<String>,
23 metadata: bool,
25 preview: Preview,
27}
28
29impl<'a> Installer<'a> {
30 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 #[must_use]
45 pub fn with_link_mode(self, link_mode: LinkMode) -> Self {
46 Self { link_mode, ..self }
47 }
48
49 #[must_use]
51 pub fn with_cache(self, cache: &'a Cache) -> Self {
52 Self {
53 cache: Some(cache),
54 ..self
55 }
56 }
57
58 #[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 #[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 #[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 #[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 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 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 #[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#[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 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 fn on_install_progress(&self, wheel: &CachedDist);
211
212 fn on_install_complete(&self);
214}