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
19pub 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 #[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 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 #[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 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 #[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 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 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 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 fn on_progress(&self, dist: &CachedDist);
246
247 fn on_complete(&self);
249
250 fn on_download_start(&self, name: &PackageName, size: Option<u64>) -> usize;
252
253 fn on_download_progress(&self, index: usize, bytes: u64);
256
257 fn on_download_complete(&self, name: &PackageName, index: usize);
259
260 fn on_build_start(&self, source: &BuildableSource) -> usize;
262
263 fn on_build_complete(&self, source: &BuildableSource, id: usize);
265
266 fn on_checkout_start(&self, url: &DisplaySafeUrl, rev: &str) -> usize;
268
269 fn on_checkout_complete(&self, url: &DisplaySafeUrl, rev: &str, index: usize);
271}
272
273impl dyn Reporter {
274 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
282struct 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}