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() {
127 debug!("Allowing build for editable source distribution: {dist}");
128 } else {
129 return Err(Error::NoBuild(dist.name().clone()));
130 }
131 }
132 }
133 }
134
135 let id = dist.distribution_id();
136 if let Some(result) = in_flight.downloads.register_or_wait(&id).await {
137 match result.as_ref() {
138 Ok(cached) => {
139 if *dist.name() != cached.filename().name {
150 let err = uv_distribution::Error::WheelMetadataNameMismatch {
151 given: dist.name().clone(),
152 metadata: cached.filename().name.clone(),
153 };
154 return Err(Error::from_dist(dist, err, resolution));
155 }
156 if let Some(version) = dist.version() {
157 if *version != cached.filename().version
158 && *version != cached.filename().version.clone().without_local()
159 {
160 let err = uv_distribution::Error::WheelMetadataVersionMismatch {
161 given: version.clone(),
162 metadata: cached.filename().version.clone(),
163 };
164 return Err(Error::from_dist(dist, err, resolution));
165 }
166 }
167 Ok(cached.clone())
168 }
169 Err(err) => Err(Error::Thread(err.to_owned())),
170 }
171 } else {
172 let policy = self.hashes.get(&dist);
173
174 let result = self
175 .database
176 .get_or_build_wheel(&dist, self.tags, policy)
177 .boxed_local()
178 .map_err(|err| Error::from_dist(dist.clone(), err, resolution))
179 .await
180 .and_then(|wheel: LocalWheel| {
181 if wheel.satisfies(policy) {
182 Ok(wheel)
183 } else {
184 let err = uv_distribution::Error::hash_mismatch(
185 dist.to_string(),
186 policy.digests(),
187 wheel.hashes(),
188 );
189 Err(Error::from_dist(dist, err, resolution))
190 }
191 })
192 .map(CachedDist::from);
193 match result {
194 Ok(cached) => {
195 in_flight.downloads.done(id, Ok(cached.clone()));
196 Ok(cached)
197 }
198 Err(err) => {
199 in_flight.downloads.done(id, Err(err.to_string()));
200 Err(err)
201 }
202 }
203 }
204 }
205}
206
207#[derive(thiserror::Error, Debug)]
208pub enum Error {
209 #[error("Building source distributions is disabled, but attempted to build `{0}`")]
210 NoBuild(PackageName),
211 #[error("Using pre-built wheels is disabled, but attempted to use `{0}`")]
212 NoBinary(PackageName),
213 #[error("{0} `{1}`")]
214 Dist(
215 DistErrorKind,
216 Box<Dist>,
217 DerivationChain,
218 #[source] Box<uv_distribution::Error>,
219 ),
220 #[error("Cyclic build dependency detected for `{0}`")]
221 CyclicBuildDependency(PackageName),
222 #[error("Unzip failed in another thread: {0}")]
223 Thread(String),
224}
225
226impl Error {
227 fn from_dist(dist: Dist, err: uv_distribution::Error, resolution: &Resolution) -> Self {
229 let chain =
230 DerivationChain::from_resolution(resolution, (&dist).into()).unwrap_or_default();
231 Self::Dist(
232 DistErrorKind::from_dist(&dist, &err),
233 Box::new(dist),
234 chain,
235 Box::new(err),
236 )
237 }
238}
239
240pub trait Reporter: Send + Sync {
241 fn on_progress(&self, dist: &CachedDist);
244
245 fn on_complete(&self);
247
248 fn on_download_start(&self, name: &PackageName, size: Option<u64>) -> usize;
250
251 fn on_download_progress(&self, index: usize, bytes: u64);
254
255 fn on_download_complete(&self, name: &PackageName, index: usize);
257
258 fn on_build_start(&self, source: &BuildableSource) -> usize;
260
261 fn on_build_complete(&self, source: &BuildableSource, id: usize);
263
264 fn on_checkout_start(&self, url: &DisplaySafeUrl, rev: &str) -> usize;
266
267 fn on_checkout_complete(&self, url: &DisplaySafeUrl, rev: &str, index: usize);
269}
270
271impl dyn Reporter {
272 fn into_distribution_reporter(self: Arc<dyn Reporter>) -> Arc<dyn uv_distribution::Reporter> {
274 Arc::new(Facade {
275 reporter: self.clone(),
276 })
277 }
278}
279
280struct Facade {
282 reporter: Arc<dyn Reporter>,
283}
284
285impl uv_distribution::Reporter for Facade {
286 fn on_build_start(&self, source: &BuildableSource) -> usize {
287 self.reporter.on_build_start(source)
288 }
289
290 fn on_build_complete(&self, source: &BuildableSource, id: usize) {
291 self.reporter.on_build_complete(source, id);
292 }
293
294 fn on_checkout_start(&self, url: &DisplaySafeUrl, rev: &str) -> usize {
295 self.reporter.on_checkout_start(url, rev)
296 }
297
298 fn on_checkout_complete(&self, url: &DisplaySafeUrl, rev: &str, index: usize) {
299 self.reporter.on_checkout_complete(url, rev, index);
300 }
301
302 fn on_download_start(&self, name: &PackageName, size: Option<u64>) -> usize {
303 self.reporter.on_download_start(name, size)
304 }
305
306 fn on_download_progress(&self, index: usize, inc: u64) {
307 self.reporter.on_download_progress(index, inc);
308 }
309
310 fn on_download_complete(&self, name: &PackageName, index: usize) {
311 self.reporter.on_download_complete(name, index);
312 }
313}