Skip to main content

uv_installer/
preparer.rs

1use std::cmp::Reverse;
2use std::sync::Arc;
3
4use futures::{FutureExt, Stream, TryFutureExt, TryStreamExt, stream::FuturesUnordered};
5use tracing::{debug, instrument};
6
7use uv_cache::Cache;
8use uv_configuration::BuildOptions;
9use uv_distribution::{DistributionDatabase, LocalWheel};
10use uv_distribution_types::{
11    BuildableSource, CachedDist, DerivationChain, Dist, DistErrorKind, Hashed, Identifier, Name,
12    RemoteSource, Resolution,
13};
14use uv_normalize::PackageName;
15use uv_platform_tags::Tags;
16use uv_redacted::DisplaySafeUrl;
17use uv_types::{BuildContext, HashStrategy, InFlight};
18
19/// Prepare distributions for installation.
20///
21/// Downloads, builds, and unzips a set of distributions.
22pub struct Preparer<'a, Context: BuildContext> {
23    tags: &'a Tags,
24    cache: &'a Cache,
25    hashes: &'a HashStrategy,
26    build_options: &'a BuildOptions,
27    database: DistributionDatabase<'a, Context>,
28    reporter: Option<Arc<dyn Reporter>>,
29}
30
31impl<'a, Context: BuildContext> Preparer<'a, Context> {
32    pub fn new(
33        cache: &'a Cache,
34        tags: &'a Tags,
35        hashes: &'a HashStrategy,
36        build_options: &'a BuildOptions,
37        database: DistributionDatabase<'a, Context>,
38    ) -> Self {
39        Self {
40            tags,
41            cache,
42            hashes,
43            build_options,
44            database,
45            reporter: None,
46        }
47    }
48
49    /// Set the [`Reporter`] to use for operations.
50    #[must_use]
51    pub fn with_reporter(self, reporter: Arc<dyn Reporter>) -> Self {
52        Self {
53            tags: self.tags,
54            cache: self.cache,
55            hashes: self.hashes,
56            build_options: self.build_options,
57            database: self
58                .database
59                .with_reporter(reporter.clone().into_distribution_reporter()),
60            reporter: Some(reporter),
61        }
62    }
63
64    /// Fetch, build, and unzip the distributions in parallel.
65    fn prepare_stream<'stream>(
66        &'stream self,
67        distributions: Vec<Arc<Dist>>,
68        in_flight: &'stream InFlight,
69        resolution: &'stream Resolution,
70    ) -> impl Stream<Item = Result<CachedDist, Error>> + 'stream {
71        distributions
72            .into_iter()
73            .map(async |dist| {
74                let wheel = self
75                    .get_wheel((*dist).clone(), in_flight, resolution)
76                    .boxed_local()
77                    .await?;
78                if let Some(reporter) = self.reporter.as_ref() {
79                    reporter.on_progress(&wheel);
80                }
81                Ok::<CachedDist, Error>(wheel)
82            })
83            .collect::<FuturesUnordered<_>>()
84    }
85
86    /// Download, build, and unzip a set of distributions.
87    #[instrument(skip_all, fields(total = distributions.len()))]
88    pub async fn prepare(
89        &self,
90        mut distributions: Vec<Arc<Dist>>,
91        in_flight: &InFlight,
92        resolution: &Resolution,
93    ) -> Result<Vec<CachedDist>, Error> {
94        // Sort the distributions by size.
95        distributions
96            .sort_unstable_by_key(|distribution| Reverse(distribution.size().unwrap_or(u64::MAX)));
97
98        let wheels = self
99            .prepare_stream(distributions, in_flight, resolution)
100            .try_collect()
101            .await?;
102
103        if let Some(reporter) = self.reporter.as_ref() {
104            reporter.on_complete();
105        }
106
107        Ok(wheels)
108    }
109    /// Download, build, and unzip a single wheel.
110    #[instrument(skip_all, fields(name = % dist, size = ? dist.size(), url = dist.file().map(| file | file.url.to_string()).unwrap_or_default()))]
111    async fn get_wheel(
112        &self,
113        dist: Dist,
114        in_flight: &InFlight,
115        resolution: &Resolution,
116    ) -> Result<CachedDist, Error> {
117        // Validate that the distribution is compatible with the build options.
118        match dist {
119            Dist::Built(ref dist) => {
120                if self.build_options.no_binary_package(dist.name()) {
121                    return Err(Error::NoBinary(dist.name().clone()));
122                }
123            }
124            Dist::Source(ref dist) => {
125                if self.build_options.no_build_package(dist.name()) {
126                    if dist.is_editable() || dist.is_first_party() {
127                        debug!(
128                            "Allowing build for first-party or editable source distribution: {dist}"
129                        );
130                    } else {
131                        return Err(Error::NoBuild(dist.name().clone()));
132                    }
133                }
134            }
135        }
136
137        let id = dist.distribution_id();
138        if let Some(result) = in_flight.downloads.register_or_wait(&id).await {
139            match result.as_ref() {
140                Ok(cached) => {
141                    // Validate that the wheel is compatible with the distribution.
142                    //
143                    // `get_or_build_wheel` is guaranteed to return a wheel that matches the
144                    // distribution. But there could be multiple requested distributions that share
145                    // a cache entry in `in_flight`, so we need to double-check here.
146                    //
147                    // For example, if two requirements are based on the same local path, but use
148                    // different names, then they'll share an `in_flight` entry, but one of the two
149                    // should be rejected (since at least one of the names will not match the
150                    // package name).
151                    if *dist.name() != cached.filename().name {
152                        let err = uv_distribution::Error::WheelMetadataNameMismatch {
153                            given: dist.name().clone(),
154                            metadata: cached.filename().name.clone(),
155                        };
156                        return Err(Error::from_dist(dist, err, resolution));
157                    }
158                    if let Some(version) = dist.version() {
159                        if *version != cached.filename().version
160                            && *version != cached.filename().version.clone().without_local()
161                        {
162                            let err = uv_distribution::Error::WheelMetadataVersionMismatch {
163                                given: version.clone(),
164                                metadata: cached.filename().version.clone(),
165                            };
166                            return Err(Error::from_dist(dist, err, resolution));
167                        }
168                    }
169                    Ok(cached.clone())
170                }
171                Err(err) => Err(Error::Thread(err.to_owned())),
172            }
173        } else {
174            let policy = self.hashes.get(&dist);
175
176            let result = self
177                .database
178                .get_or_build_wheel(&dist, self.tags, policy)
179                .boxed_local()
180                .map_err(|err| Error::from_dist(dist.clone(), err, resolution))
181                .await
182                .and_then(|wheel: LocalWheel| {
183                    if wheel.satisfies(policy) {
184                        Ok(wheel)
185                    } else {
186                        let err = uv_distribution::Error::hash_mismatch(
187                            dist.to_string(),
188                            policy.digests(),
189                            wheel.hashes(),
190                        );
191                        Err(Error::from_dist(dist, err, resolution))
192                    }
193                })
194                .map(CachedDist::from);
195            match result {
196                Ok(cached) => {
197                    in_flight.downloads.done(id, Ok(cached.clone()));
198                    Ok(cached)
199                }
200                Err(err) => {
201                    in_flight.downloads.done(id, Err(err.to_string()));
202                    Err(err)
203                }
204            }
205        }
206    }
207}
208
209#[derive(thiserror::Error, Debug)]
210pub enum Error {
211    #[error("Building source distributions is disabled, but attempted to build `{0}`")]
212    NoBuild(PackageName),
213    #[error("Using pre-built wheels is disabled, but attempted to use `{0}`")]
214    NoBinary(PackageName),
215    #[error("{0} `{1}`")]
216    Dist(
217        DistErrorKind,
218        Box<Dist>,
219        DerivationChain,
220        #[source] Box<uv_distribution::Error>,
221    ),
222    #[error("Cyclic build dependency detected for `{0}`")]
223    CyclicBuildDependency(PackageName),
224    #[error("Unzip failed in another thread: {0}")]
225    Thread(String),
226}
227
228impl Error {
229    /// Create an [`Error`] from a distribution error.
230    fn from_dist(dist: Dist, err: uv_distribution::Error, resolution: &Resolution) -> Self {
231        let chain =
232            DerivationChain::from_resolution(resolution, (&dist).into()).unwrap_or_default();
233        Self::Dist(
234            DistErrorKind::from_dist(&dist, &err),
235            Box::new(dist),
236            chain,
237            Box::new(err),
238        )
239    }
240}
241
242pub trait Reporter: Send + Sync {
243    /// Callback to invoke when a wheel is unzipped. This implies that the wheel was downloaded and,
244    /// if necessary, built.
245    fn on_progress(&self, dist: &CachedDist);
246
247    /// Callback to invoke when the operation is complete.
248    fn on_complete(&self);
249
250    /// Callback to invoke when a download is kicked off.
251    fn on_download_start(&self, name: &PackageName, size: Option<u64>) -> usize;
252
253    /// Callback to invoke when a download makes progress (i.e. some number of bytes are
254    /// downloaded).
255    fn on_download_progress(&self, index: usize, bytes: u64);
256
257    /// Callback to invoke when a download is complete.
258    fn on_download_complete(&self, name: &PackageName, index: usize);
259
260    /// Callback to invoke when a source distribution build is kicked off.
261    fn on_build_start(&self, source: &BuildableSource) -> usize;
262
263    /// Callback to invoke when a source distribution build is complete.
264    fn on_build_complete(&self, source: &BuildableSource, id: usize);
265
266    /// Callback to invoke when a repository checkout begins.
267    fn on_checkout_start(&self, url: &DisplaySafeUrl, rev: &str) -> usize;
268
269    /// Callback to invoke when a repository checkout completes.
270    fn on_checkout_complete(&self, url: &DisplaySafeUrl, rev: &str, index: usize);
271}
272
273impl dyn Reporter {
274    /// Converts this reporter to a [`uv_distribution::Reporter`].
275    fn into_distribution_reporter(self: Arc<dyn Reporter>) -> Arc<dyn uv_distribution::Reporter> {
276        Arc::new(Facade {
277            reporter: self.clone(),
278        })
279    }
280}
281
282/// A facade for converting from [`Reporter`] to [`uv_distribution::Reporter`].
283struct Facade {
284    reporter: Arc<dyn Reporter>,
285}
286
287impl uv_distribution::Reporter for Facade {
288    fn on_build_start(&self, source: &BuildableSource) -> usize {
289        self.reporter.on_build_start(source)
290    }
291
292    fn on_build_complete(&self, source: &BuildableSource, id: usize) {
293        self.reporter.on_build_complete(source, id);
294    }
295
296    fn on_checkout_start(&self, url: &DisplaySafeUrl, rev: &str) -> usize {
297        self.reporter.on_checkout_start(url, rev)
298    }
299
300    fn on_checkout_complete(&self, url: &DisplaySafeUrl, rev: &str, index: usize) {
301        self.reporter.on_checkout_complete(url, rev, index);
302    }
303
304    fn on_download_start(&self, name: &PackageName, size: Option<u64>) -> usize {
305        self.reporter.on_download_start(name, size)
306    }
307
308    fn on_download_progress(&self, index: usize, inc: u64) {
309        self.reporter.on_download_progress(index, inc);
310    }
311
312    fn on_download_complete(&self, name: &PackageName, index: usize) {
313        self.reporter.on_download_complete(name, index);
314    }
315}