diff --git a/crates/uv-distribution/src/distribution_database.rs b/crates/uv-distribution/src/distribution_database.rs index ccd92ece5..1d7462efb 100644 --- a/crates/uv-distribution/src/distribution_database.rs +++ b/crates/uv-distribution/src/distribution_database.rs @@ -1,12 +1,14 @@ use std::future::Future; use std::io; use std::path::Path; +use std::pin::Pin; use std::rc::Rc; use std::sync::Arc; +use std::task::{Context, Poll}; use futures::{FutureExt, TryStreamExt}; use tempfile::TempDir; -use tokio::io::AsyncSeekExt; +use tokio::io::{AsyncRead, AsyncSeekExt, ReadBuf}; use tokio::sync::Semaphore; use tokio_util::compat::FuturesAsyncReadCompatExt; use tracing::{info_span, instrument, warn, Instrument}; @@ -49,6 +51,7 @@ pub struct DistributionDatabase<'a, Context: BuildContext> { builder: SourceDistributionBuilder<'a, Context>, locks: Rc, client: ManagedClient<'a>, + reporter: Option>, } impl<'a, Context: BuildContext> DistributionDatabase<'a, Context> { @@ -62,6 +65,7 @@ impl<'a, Context: BuildContext> DistributionDatabase<'a, Context> { builder: SourceDistributionBuilder::new(build_context), locks: Rc::new(Locks::default()), client: ManagedClient::new(client, concurrent_downloads), + reporter: None, } } @@ -70,6 +74,7 @@ impl<'a, Context: BuildContext> DistributionDatabase<'a, Context> { pub fn with_reporter(self, reporter: impl Reporter + 'static) -> Self { let reporter = Arc::new(reporter); Self { + reporter: Some(reporter.clone()), builder: self.builder.with_reporter(reporter), ..self } @@ -168,6 +173,7 @@ impl<'a, Context: BuildContext> DistributionDatabase<'a, Context> { NoBinary::All => true, NoBinary::Packages(packages) => packages.contains(dist.name()), }; + if no_binary { return Err(Error::NoBinary); } @@ -188,6 +194,7 @@ impl<'a, Context: BuildContext> DistributionDatabase<'a, Context> { WheelCache::Index(&wheel.index).wheel_dir(wheel.name().as_ref()), wheel.filename.stem(), ); + return self .load_wheel(path, &wheel.filename, cache_entry, dist, hashes) .await; @@ -203,7 +210,14 @@ impl<'a, Context: BuildContext> DistributionDatabase<'a, Context> { // Download and unzip. match self - .stream_wheel(url.clone(), &wheel.filename, &wheel_entry, dist, hashes) + .stream_wheel( + url.clone(), + &wheel.filename, + wheel.file.size, + &wheel_entry, + dist, + hashes, + ) .await { Ok(archive) => Ok(LocalWheel { @@ -220,8 +234,16 @@ impl<'a, Context: BuildContext> DistributionDatabase<'a, Context> { // If the request failed because streaming is unsupported, download the // wheel directly. let archive = self - .download_wheel(url, &wheel.filename, &wheel_entry, dist, hashes) + .download_wheel( + url, + &wheel.filename, + wheel.file.size, + &wheel_entry, + dist, + hashes, + ) .await?; + Ok(LocalWheel { dist: Dist::Built(dist.clone()), archive: self.build_context.cache().archive(&archive.id), @@ -246,6 +268,7 @@ impl<'a, Context: BuildContext> DistributionDatabase<'a, Context> { .stream_wheel( wheel.url.raw().clone(), &wheel.filename, + None, &wheel_entry, dist, hashes, @@ -269,6 +292,7 @@ impl<'a, Context: BuildContext> DistributionDatabase<'a, Context> { .download_wheel( wheel.url.raw().clone(), &wheel.filename, + None, &wheel_entry, dist, hashes, @@ -427,6 +451,7 @@ impl<'a, Context: BuildContext> DistributionDatabase<'a, Context> { &self, url: Url, filename: &WheelFilename, + size: Option, wheel_entry: &CacheEntry, dist: &BuiltDist, hashes: HashPolicy<'_>, @@ -434,8 +459,19 @@ impl<'a, Context: BuildContext> DistributionDatabase<'a, Context> { // Create an entry for the HTTP cache. let http_entry = wheel_entry.with_file(format!("{}.http", filename.stem())); + // Fetch the archive from the cache, or download it if necessary. + let req = self.request(url.clone())?; + + // Extract the size from the `Content-Length` header, if not provided by the registry. + let size = size.or_else(|| content_length(&req)); + let download = |response: reqwest::Response| { async { + let progress = self + .reporter + .as_ref() + .map(|reporter| (reporter, reporter.on_download_start(dist.name(), size))); + let reader = response .bytes_stream() .map_err(|err| self.handle_response_errors(err)) @@ -449,7 +485,16 @@ impl<'a, Context: BuildContext> DistributionDatabase<'a, Context> { // Download and unzip the wheel to a temporary directory. let temp_dir = tempfile::tempdir_in(self.build_context.cache().root()) .map_err(Error::CacheWrite)?; - uv_extract::stream::unzip(&mut hasher, temp_dir.path()).await?; + + match progress { + Some((reporter, progress)) => { + let mut reader = ProgressReader::new(&mut hasher, progress, &**reporter); + uv_extract::stream::unzip(&mut reader, temp_dir.path()).await?; + } + None => { + uv_extract::stream::unzip(&mut hasher, temp_dir.path()).await?; + } + } // If necessary, exhaust the reader to compute the hash. if !hashes.is_none() { @@ -464,6 +509,10 @@ impl<'a, Context: BuildContext> DistributionDatabase<'a, Context> { .await .map_err(Error::CacheRead)?; + if let Some((reporter, progress)) = progress { + reporter.on_download_complete(dist.name(), progress); + } + Ok(Archive::new( id, hashers.into_iter().map(HashDigest::from).collect(), @@ -523,6 +572,7 @@ impl<'a, Context: BuildContext> DistributionDatabase<'a, Context> { &self, url: Url, filename: &WheelFilename, + size: Option, wheel_entry: &CacheEntry, dist: &BuiltDist, hashes: HashPolicy<'_>, @@ -530,8 +580,18 @@ impl<'a, Context: BuildContext> DistributionDatabase<'a, Context> { // Create an entry for the HTTP cache. let http_entry = wheel_entry.with_file(format!("{}.http", filename.stem())); + let req = self.request(url.clone())?; + + // Extract the size from the `Content-Length` header, if not provided by the registry. + let size = size.or_else(|| content_length(&req)); + let download = |response: reqwest::Response| { async { + let progress = self + .reporter + .as_ref() + .map(|reporter| (reporter, reporter.on_download_start(dist.name(), size))); + let reader = response .bytes_stream() .map_err(|err| self.handle_response_errors(err)) @@ -541,9 +601,25 @@ impl<'a, Context: BuildContext> DistributionDatabase<'a, Context> { let temp_file = tempfile::tempfile_in(self.build_context.cache().root()) .map_err(Error::CacheWrite)?; let mut writer = tokio::io::BufWriter::new(tokio::fs::File::from_std(temp_file)); - tokio::io::copy(&mut reader.compat(), &mut writer) - .await - .map_err(Error::CacheWrite)?; + + match progress { + Some((reporter, progress)) => { + // Wrap the reader in a progress reporter. This will report 100% progress + // after the download is complete, even if we still have to unzip and hash + // part of the file. + let mut reader = + ProgressReader::new(reader.compat(), progress, &**reporter); + + tokio::io::copy(&mut reader, &mut writer) + .await + .map_err(Error::CacheWrite)?; + } + None => { + tokio::io::copy(&mut reader.compat(), &mut writer) + .await + .map_err(Error::CacheWrite)?; + } + } // Unzip the wheel to a temporary directory. let temp_dir = tempfile::tempdir_in(self.build_context.cache().root()) @@ -588,6 +664,10 @@ impl<'a, Context: BuildContext> DistributionDatabase<'a, Context> { .await .map_err(Error::CacheRead)?; + if let Some((reporter, progress)) = progress { + reporter.on_download_complete(dist.name(), progress); + } + Ok(Archive::new(id, hashes)) } .instrument(info_span!("wheel", wheel = %dist)) @@ -813,6 +893,50 @@ impl<'a> ManagedClient<'a> { } } +/// Returns the value of the `Content-Length` header from the [`reqwest::Request`], if present. +fn content_length(req: &reqwest::Request) -> Option { + req.headers() + .get(reqwest::header::CONTENT_LENGTH) + .and_then(|val| val.to_str().ok()) + .and_then(|val| val.parse::().ok()) +} + +/// An asynchronous reader that reports progress as bytes are read. +struct ProgressReader<'a, R> { + reader: R, + index: usize, + reporter: &'a dyn Reporter, +} + +impl<'a, R> ProgressReader<'a, R> { + /// Create a new [`ProgressReader`] that wraps another reader. + fn new(reader: R, index: usize, reporter: &'a dyn Reporter) -> Self { + Self { + reader, + index, + reporter, + } + } +} + +impl AsyncRead for ProgressReader<'_, R> +where + R: AsyncRead + Unpin, +{ + fn poll_read( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &mut ReadBuf<'_>, + ) -> Poll> { + Pin::new(&mut self.as_mut().reader) + .poll_read(cx, buf) + .map_ok(|()| { + self.reporter + .on_download_progress(self.index, buf.filled().len() as u64); + }) + } +} + /// A pointer to an archive in the cache, fetched from an HTTP archive. /// /// Encoded with `MsgPack`, and represented on disk by a `.http` file. diff --git a/crates/uv-distribution/src/reporter.rs b/crates/uv-distribution/src/reporter.rs index 2740513ac..d519783bf 100644 --- a/crates/uv-distribution/src/reporter.rs +++ b/crates/uv-distribution/src/reporter.rs @@ -3,6 +3,7 @@ use std::sync::Arc; use url::Url; use distribution_types::BuildableSource; +use pep508_rs::PackageName; pub trait Reporter: Send + Sync { /// Callback to invoke when a source distribution build is kicked off. @@ -15,7 +16,17 @@ pub trait Reporter: Send + Sync { fn on_checkout_start(&self, url: &Url, rev: &str) -> usize; /// Callback to invoke when a repository checkout completes. - fn on_checkout_complete(&self, url: &Url, rev: &str, index: usize); + fn on_checkout_complete(&self, url: &Url, rev: &str, id: usize); + + /// Callback to invoke when a download is kicked off. + fn on_download_start(&self, name: &PackageName, size: Option) -> usize; + + /// Callback to invoke when a download makes progress (i.e. some number of bytes are + /// downloaded). + fn on_download_progress(&self, id: usize, inc: u64); + + /// Callback to invoke when a download is complete. + fn on_download_complete(&self, name: &PackageName, id: usize); } /// A facade for converting from [`Reporter`] to [`uv_git::Reporter`]. @@ -34,7 +45,7 @@ impl uv_git::Reporter for Facade { self.reporter.on_checkout_start(url, rev) } - fn on_checkout_complete(&self, url: &Url, rev: &str, index: usize) { - self.reporter.on_checkout_complete(url, rev, index); + fn on_checkout_complete(&self, url: &Url, rev: &str, id: usize) { + self.reporter.on_checkout_complete(url, rev, id); } } diff --git a/crates/uv-installer/src/downloader.rs b/crates/uv-installer/src/downloader.rs index dbcf6f6ea..0aa84f574 100644 --- a/crates/uv-installer/src/downloader.rs +++ b/crates/uv-installer/src/downloader.rs @@ -3,6 +3,7 @@ use std::path::Path; use std::sync::Arc; use futures::{stream::FuturesUnordered, FutureExt, Stream, StreamExt, TryFutureExt, TryStreamExt}; +use pep508_rs::PackageName; use tokio::task::JoinError; use tracing::instrument; use url::Url; @@ -169,6 +170,7 @@ impl<'a, Context: BuildContext> Downloader<'a, Context> { let id = dist.distribution_id(); if in_flight.downloads.register(id.clone()) { let policy = self.hashes.get(&dist); + let result = self .database .get_or_build_wheel(&dist, self.tags, policy) @@ -223,6 +225,16 @@ pub trait Reporter: Send + Sync { /// Callback to invoke when the operation is complete. fn on_complete(&self); + /// Callback to invoke when a download is kicked off. + fn on_download_start(&self, name: &PackageName, size: Option) -> usize; + + /// Callback to invoke when a download makes progress (i.e. some number of bytes are + /// downloaded). + fn on_download_progress(&self, index: usize, bytes: u64); + + /// Callback to invoke when a download is complete. + fn on_download_complete(&self, name: &PackageName, index: usize); + /// Callback to invoke when a source distribution build is kicked off. fn on_build_start(&self, source: &BuildableSource) -> usize; @@ -269,4 +281,16 @@ impl uv_distribution::Reporter for Facade { fn on_checkout_complete(&self, url: &Url, rev: &str, index: usize) { self.reporter.on_checkout_complete(url, rev, index); } + + fn on_download_start(&self, name: &PackageName, size: Option) -> usize { + self.reporter.on_download_start(name, size) + } + + fn on_download_progress(&self, index: usize, inc: u64) { + self.reporter.on_download_progress(index, inc); + } + + fn on_download_complete(&self, name: &PackageName, index: usize) { + self.reporter.on_download_complete(name, index); + } } diff --git a/crates/uv-resolver/src/resolver/reporter.rs b/crates/uv-resolver/src/resolver/reporter.rs index 2d446cfda..a15e09e46 100644 --- a/crates/uv-resolver/src/resolver/reporter.rs +++ b/crates/uv-resolver/src/resolver/reporter.rs @@ -20,11 +20,21 @@ pub trait Reporter: Send + Sync { /// Callback to invoke when a source distribution build is complete. fn on_build_complete(&self, source: &BuildableSource, id: usize); + /// Callback to invoke when a download is kicked off. + fn on_download_start(&self, name: &PackageName, size: Option) -> usize; + + /// Callback to invoke when a download makes progress (i.e. some number of bytes are + /// downloaded). + fn on_download_progress(&self, id: usize, bytes: u64); + + /// Callback to invoke when a download is complete. + fn on_download_complete(&self, name: &PackageName, id: usize); + /// Callback to invoke when a repository checkout begins. fn on_checkout_start(&self, url: &Url, rev: &str) -> usize; /// Callback to invoke when a repository checkout completes. - fn on_checkout_complete(&self, url: &Url, rev: &str, index: usize); + fn on_checkout_complete(&self, url: &Url, rev: &str, id: usize); } /// A facade for converting from [`Reporter`] to [`uv_distribution::Reporter`]. @@ -45,7 +55,19 @@ impl uv_distribution::Reporter for Facade { self.reporter.on_checkout_start(url, rev) } - fn on_checkout_complete(&self, url: &Url, rev: &str, index: usize) { - self.reporter.on_checkout_complete(url, rev, index); + fn on_checkout_complete(&self, url: &Url, rev: &str, id: usize) { + self.reporter.on_checkout_complete(url, rev, id); + } + + fn on_download_start(&self, name: &PackageName, size: Option) -> usize { + self.reporter.on_download_start(name, size) + } + + fn on_download_progress(&self, id: usize, bytes: u64) { + self.reporter.on_download_progress(id, bytes); + } + + fn on_download_complete(&self, name: &PackageName, id: usize) { + self.reporter.on_download_complete(name, id); } } diff --git a/crates/uv/src/commands/reporters.rs b/crates/uv/src/commands/reporters.rs index a03d41285..cb0bb95ba 100644 --- a/crates/uv/src/commands/reporters.rs +++ b/crates/uv/src/commands/reporters.rs @@ -3,6 +3,7 @@ use std::time::Duration; use indicatif::{MultiProgress, ProgressBar, ProgressStyle}; use owo_colors::OwoColorize; +use rustc_hash::FxHashMap; use url::Url; use distribution_types::{ @@ -14,91 +15,109 @@ use uv_normalize::PackageName; use crate::printer::Printer; #[derive(Debug)] -pub(crate) struct DownloadReporter { +struct ProgressReporter { printer: Printer, + root: ProgressBar, multi_progress: MultiProgress, - progress: ProgressBar, - bars: Arc>>, + state: Arc>, } -impl From for DownloadReporter { - fn from(printer: Printer) -> Self { - let multi_progress = MultiProgress::with_draw_target(printer.target()); +#[derive(Default, Debug)] +struct BarState { + // The number of bars that precede any download bars (i.e. build/checkout status). + headers: usize, + // A list of donwnload bar sizes, in descending order. + sizes: Vec, + // A map of progress bars, by ID. + bars: FxHashMap, + // A monotonic counter for bar IDs. + id: usize, +} - let progress = multi_progress.add(ProgressBar::with_draw_target(None, printer.target())); - progress.set_style( - ProgressStyle::with_template("{bar:20} [{pos}/{len}] {wide_msg:.dim}").unwrap(), - ); - progress.set_message("Fetching packages..."); - - Self { - printer, - multi_progress, - progress, - bars: Arc::new(Mutex::new(Vec::new())), - } +impl BarState { + // Returns a unique ID for a new bar. + fn id(&mut self) -> usize { + self.id += 1; + self.id } } -impl DownloadReporter { - #[must_use] - pub(crate) fn with_length(self, length: u64) -> Self { - self.progress.set_length(length); - self - } -} - -impl DownloadReporter { +impl ProgressReporter { fn on_any_build_start(&self, color_string: &str) -> usize { + let mut state = self.state.lock().unwrap(); + let id = state.id(); + let progress = self.multi_progress.insert_before( - &self.progress, + &self.root, ProgressBar::with_draw_target(None, self.printer.target()), ); progress.set_style(ProgressStyle::with_template("{wide_msg}").unwrap()); progress.set_message(format!("{} {}", "Building".bold().cyan(), color_string)); - let mut bars = self.bars.lock().unwrap(); - bars.push(progress); - bars.len() - 1 + state.headers += 1; + state.bars.insert(id, progress); + id } fn on_any_build_complete(&self, color_string: &str, id: usize) { - let bars = self.bars.lock().unwrap(); - let progress = &bars[id]; + let progress = { + let mut state = self.state.lock().unwrap(); + state.headers -= 1; + state.bars.remove(&id).unwrap() + }; + progress.finish_with_message(format!(" {} {}", "Built".bold().green(), color_string)); } -} -impl uv_installer::DownloadReporter for DownloadReporter { - fn on_progress(&self, dist: &CachedDist) { - self.progress.set_message(format!("{dist}")); - self.progress.inc(1); + fn on_download_start(&self, name: &PackageName, size: Option) -> usize { + let mut state = self.state.lock().unwrap(); + + // Preserve ascending order. + let position = size.map_or(0, |size| state.sizes.partition_point(|&len| len < size)); + state.sizes.insert(position, size.unwrap_or(0)); + + let progress = self.multi_progress.insert( + // Make sure not to reorder the initial "Downloading..." bar, or any previous bars. + position + 1 + state.headers, + ProgressBar::with_draw_target(size, self.printer.target()), + ); + + if size.is_some() { + progress.set_style( + ProgressStyle::with_template( + "{msg:10.dim} {bar:30.green/dim} {decimal_bytes:>7}/{decimal_total_bytes:7}", + ) + .unwrap() + .progress_chars("--"), + ); + progress.set_message(name.to_string()); + } else { + progress.set_style(ProgressStyle::with_template("{wide_msg:.dim} ....").unwrap()); + progress.set_message(name.to_string()); + progress.finish(); + } + + let id = state.id(); + state.bars.insert(id, progress); + id } - fn on_complete(&self) { - self.progress.finish_and_clear(); + fn on_download_progress(&self, id: usize, bytes: u64) { + self.state.lock().unwrap().bars[&id].inc(bytes); } - fn on_build_start(&self, source: &BuildableSource) -> usize { - self.on_any_build_start(&source.to_color_string()) - } - - fn on_build_complete(&self, source: &BuildableSource, index: usize) { - self.on_any_build_complete(&source.to_color_string(), index); - } - - fn on_editable_build_start(&self, dist: &LocalEditable) -> usize { - self.on_any_build_start(&dist.to_color_string()) - } - - fn on_editable_build_complete(&self, dist: &LocalEditable, id: usize) { - self.on_any_build_complete(&dist.to_color_string(), id); + fn on_download_complete(&self, id: usize) { + let progress = self.state.lock().unwrap().bars.remove(&id).unwrap(); + progress.finish_and_clear(); } fn on_checkout_start(&self, url: &Url, rev: &str) -> usize { + let mut state = self.state.lock().unwrap(); + let id = state.id(); + let progress = self.multi_progress.insert_before( - &self.progress, + &self.root, ProgressBar::with_draw_target(None, self.printer.target()), ); @@ -111,14 +130,18 @@ impl uv_installer::DownloadReporter for DownloadReporter { )); progress.finish(); - let mut bars = self.bars.lock().unwrap(); - bars.push(progress); - bars.len() - 1 + state.headers += 1; + state.bars.insert(id, progress); + id } - fn on_checkout_complete(&self, url: &Url, rev: &str, index: usize) { - let bars = self.bars.lock().unwrap(); - let progress = &bars[index]; + fn on_checkout_complete(&self, url: &Url, rev: &str, id: usize) { + let progress = { + let mut state = self.state.lock().unwrap(); + state.headers -= 1; + state.bars.remove(&id).unwrap() + }; + progress.finish_with_message(format!( " {} {} ({})", "Updated".bold().green(), @@ -128,6 +151,205 @@ impl uv_installer::DownloadReporter for DownloadReporter { } } +#[derive(Debug)] +pub(crate) struct DownloadReporter { + reporter: ProgressReporter, +} + +impl From for DownloadReporter { + fn from(printer: Printer) -> Self { + let multi_progress = MultiProgress::with_draw_target(printer.target()); + + let progress = multi_progress.add(ProgressBar::with_draw_target(None, printer.target())); + progress.enable_steady_tick(Duration::from_millis(200)); + progress.set_style( + ProgressStyle::with_template("{spinner:.white} {msg:.dim} ({pos}/{len})") + .unwrap() + .tick_strings(&["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]), + ); + progress.set_message("Downloading packages..."); + + let reporter = ProgressReporter { + printer, + multi_progress, + root: progress, + state: Arc::default(), + }; + + Self { reporter } + } +} + +impl DownloadReporter { + #[must_use] + pub(crate) fn with_length(self, length: u64) -> Self { + self.reporter.root.set_length(length); + self + } +} + +impl uv_installer::DownloadReporter for DownloadReporter { + fn on_progress(&self, _dist: &CachedDist) { + self.reporter.root.inc(1); + } + + fn on_complete(&self) { + self.reporter.root.finish_and_clear(); + } + + fn on_build_start(&self, source: &BuildableSource) -> usize { + self.reporter.on_any_build_start(&source.to_color_string()) + } + + fn on_build_complete(&self, source: &BuildableSource, id: usize) { + self.reporter + .on_any_build_complete(&source.to_color_string(), id); + } + + fn on_editable_build_start(&self, dist: &LocalEditable) -> usize { + self.reporter.on_any_build_start(&dist.to_color_string()) + } + + fn on_editable_build_complete(&self, dist: &LocalEditable, id: usize) { + self.reporter + .on_any_build_complete(&dist.to_color_string(), id); + } + + fn on_download_start(&self, name: &PackageName, size: Option) -> usize { + self.reporter.on_download_start(name, size) + } + + fn on_download_progress(&self, id: usize, bytes: u64) { + self.reporter.on_download_progress(id, bytes); + } + + fn on_download_complete(&self, _name: &PackageName, id: usize) { + self.reporter.on_download_complete(id); + } + + fn on_checkout_start(&self, url: &Url, rev: &str) -> usize { + self.reporter.on_checkout_start(url, rev) + } + + fn on_checkout_complete(&self, url: &Url, rev: &str, id: usize) { + self.reporter.on_checkout_complete(url, rev, id); + } +} + +#[derive(Debug)] +pub(crate) struct ResolverReporter { + reporter: ProgressReporter, +} + +impl ResolverReporter { + #[must_use] + pub(crate) fn with_length(self, length: u64) -> Self { + self.reporter.root.set_length(length); + self + } +} + +impl From for ResolverReporter { + fn from(printer: Printer) -> Self { + let multi_progress = MultiProgress::with_draw_target(printer.target()); + + let root = multi_progress.add(ProgressBar::with_draw_target(None, printer.target())); + root.enable_steady_tick(Duration::from_millis(200)); + root.set_style( + ProgressStyle::with_template("{spinner:.white} {wide_msg:.dim}") + .unwrap() + .tick_strings(&["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]), + ); + root.set_message("Resolving dependencies..."); + + let reporter = ProgressReporter { + root, + printer, + multi_progress, + state: Arc::default(), + }; + + ResolverReporter { reporter } + } +} + +impl uv_resolver::ResolverReporter for ResolverReporter { + fn on_progress(&self, name: &PackageName, version_or_url: &VersionOrUrlRef) { + match version_or_url { + VersionOrUrlRef::Version(version) => { + self.reporter.root.set_message(format!("{name}=={version}")); + } + VersionOrUrlRef::Url(url) => { + self.reporter.root.set_message(format!("{name} @ {url}")); + } + } + } + + fn on_complete(&self) { + self.reporter.root.finish_and_clear(); + } + + fn on_build_start(&self, source: &BuildableSource) -> usize { + self.reporter.on_any_build_start(&source.to_color_string()) + } + + fn on_build_complete(&self, source: &BuildableSource, id: usize) { + self.reporter + .on_any_build_complete(&source.to_color_string(), id); + } + + fn on_checkout_start(&self, url: &Url, rev: &str) -> usize { + self.reporter.on_checkout_start(url, rev) + } + + fn on_checkout_complete(&self, url: &Url, rev: &str, id: usize) { + self.reporter.on_checkout_complete(url, rev, id); + } + + fn on_download_start(&self, name: &PackageName, size: Option) -> usize { + self.reporter.on_download_start(name, size) + } + + fn on_download_progress(&self, id: usize, bytes: u64) { + self.reporter.on_download_progress(id, bytes); + } + + fn on_download_complete(&self, _name: &PackageName, id: usize) { + self.reporter.on_download_complete(id); + } +} + +impl uv_distribution::Reporter for ResolverReporter { + fn on_build_start(&self, source: &BuildableSource) -> usize { + self.reporter.on_any_build_start(&source.to_color_string()) + } + + fn on_build_complete(&self, source: &BuildableSource, id: usize) { + self.reporter + .on_any_build_complete(&source.to_color_string(), id); + } + + fn on_download_start(&self, name: &PackageName, size: Option) -> usize { + self.reporter.on_download_start(name, size) + } + + fn on_download_progress(&self, id: usize, bytes: u64) { + self.reporter.on_download_progress(id, bytes); + } + + fn on_download_complete(&self, _name: &PackageName, id: usize) { + self.reporter.on_download_complete(id); + } + + fn on_checkout_start(&self, url: &Url, rev: &str) -> usize { + self.reporter.on_checkout_start(url, rev) + } + + fn on_checkout_complete(&self, url: &Url, rev: &str, id: usize) { + self.reporter.on_checkout_complete(url, rev, id); + } +} + #[derive(Debug)] pub(crate) struct InstallReporter { progress: ProgressBar, @@ -163,162 +385,6 @@ impl uv_installer::InstallReporter for InstallReporter { } } -#[derive(Debug)] -pub(crate) struct ResolverReporter { - printer: Printer, - multi_progress: MultiProgress, - progress: ProgressBar, - bars: Arc>>, -} - -impl From for ResolverReporter { - fn from(printer: Printer) -> Self { - let multi_progress = MultiProgress::with_draw_target(printer.target()); - - let progress = multi_progress.add(ProgressBar::with_draw_target(None, printer.target())); - progress.enable_steady_tick(Duration::from_millis(200)); - progress.set_style( - ProgressStyle::with_template("{spinner:.white} {wide_msg:.dim}") - .unwrap() - .tick_strings(&["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]), - ); - progress.set_message("Resolving dependencies..."); - - Self { - printer, - multi_progress, - progress, - bars: Arc::new(Mutex::new(Vec::new())), - } - } -} - -impl ResolverReporter { - #[must_use] - pub(crate) fn with_length(self, length: u64) -> Self { - self.progress.set_length(length); - self - } - - fn on_progress(&self, name: &PackageName, version_or_url: &VersionOrUrlRef) { - match version_or_url { - VersionOrUrlRef::Version(version) => { - self.progress.set_message(format!("{name}=={version}")); - } - VersionOrUrlRef::Url(url) => { - self.progress.set_message(format!("{name} @ {url}")); - } - } - } - - fn on_complete(&self) { - self.progress.finish_and_clear(); - } - - fn on_build_start(&self, source: &BuildableSource) -> usize { - let progress = self.multi_progress.insert_before( - &self.progress, - ProgressBar::with_draw_target(None, self.printer.target()), - ); - - progress.set_style(ProgressStyle::with_template("{wide_msg}").unwrap()); - progress.set_message(format!( - "{} {}", - "Building".bold().cyan(), - source.to_color_string(), - )); - - let mut bars = self.bars.lock().unwrap(); - bars.push(progress); - bars.len() - 1 - } - - fn on_build_complete(&self, source: &BuildableSource, index: usize) { - let bars = self.bars.lock().unwrap(); - let progress = &bars[index]; - progress.finish_with_message(format!( - " {} {}", - "Built".bold().green(), - source.to_color_string(), - )); - } - - fn on_checkout_start(&self, url: &Url, rev: &str) -> usize { - let progress = self.multi_progress.insert_before( - &self.progress, - ProgressBar::with_draw_target(None, self.printer.target()), - ); - - progress.set_style(ProgressStyle::with_template("{wide_msg}").unwrap()); - progress.set_message(format!( - "{} {} ({})", - "Updating".bold().cyan(), - url, - rev.dimmed() - )); - progress.finish(); - - let mut bars = self.bars.lock().unwrap(); - bars.push(progress); - bars.len() - 1 - } - - fn on_checkout_complete(&self, url: &Url, rev: &str, index: usize) { - let bars = self.bars.lock().unwrap(); - let progress = &bars[index]; - progress.finish_with_message(format!( - " {} {} ({})", - "Updated".bold().green(), - url, - rev.dimmed() - )); - } -} - -impl uv_resolver::ResolverReporter for ResolverReporter { - fn on_progress(&self, name: &PackageName, version_or_url: &VersionOrUrlRef) { - self.on_progress(name, version_or_url); - } - - fn on_complete(&self) { - self.on_complete(); - } - - fn on_build_start(&self, source: &BuildableSource) -> usize { - self.on_build_start(source) - } - - fn on_build_complete(&self, source: &BuildableSource, index: usize) { - self.on_build_complete(source, index); - } - - fn on_checkout_start(&self, url: &Url, rev: &str) -> usize { - self.on_checkout_start(url, rev) - } - - fn on_checkout_complete(&self, url: &Url, rev: &str, index: usize) { - self.on_checkout_complete(url, rev, index); - } -} - -impl uv_distribution::Reporter for ResolverReporter { - fn on_build_start(&self, source: &BuildableSource) -> usize { - self.on_build_start(source) - } - - fn on_build_complete(&self, source: &BuildableSource, index: usize) { - self.on_build_complete(source, index); - } - - fn on_checkout_start(&self, url: &Url, rev: &str) -> usize { - self.on_checkout_start(url, rev) - } - - fn on_checkout_complete(&self, url: &Url, rev: &str, index: usize) { - self.on_checkout_complete(url, rev, index); - } -} - /// Like [`std::fmt::Display`], but with colors. trait ColorDisplay { fn to_color_string(&self) -> String;