Concurrent progress bars (#3252)

## Summary

Implements concurrent progress bars. Resolves
https://github.com/astral-sh/uv/issues/1209.

## Test Plan

b21bdfbb-8817-4873-a65c-16c9e8c7c460
This commit is contained in:
Ibraheem Ahmed 2024-05-26 21:21:07 -04:00 committed by GitHub
parent 70cbc32565
commit 7dc322665c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 477 additions and 230 deletions

View file

@ -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<Locks>,
client: ManagedClient<'a>,
reporter: Option<Arc<dyn Reporter>>,
}
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<u64>,
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)?;
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<u64>,
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));
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<u64> {
req.headers()
.get(reqwest::header::CONTENT_LENGTH)
.and_then(|val| val.to_str().ok())
.and_then(|val| val.parse::<u64>().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<R> AsyncRead for ProgressReader<'_, R>
where
R: AsyncRead + Unpin,
{
fn poll_read(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &mut ReadBuf<'_>,
) -> Poll<io::Result<()>> {
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.

View file

@ -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<u64>) -> 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);
}
}

View file

@ -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<u64>) -> 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<u64>) -> 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);
}
}

View file

@ -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<u64>) -> 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<u64>) -> 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);
}
}

View file

@ -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<Mutex<Vec<ProgressBar>>>,
state: Arc<Mutex<BarState>>,
}
impl From<Printer> 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<u64>,
// A map of progress bars, by ID.
bars: FxHashMap<usize, ProgressBar>,
// 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<u64>) -> 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();
}
fn on_complete(&self) {
self.progress.finish_and_clear();
let id = state.id();
state.bars.insert(id, progress);
id
}
fn on_build_start(&self, source: &BuildableSource) -> usize {
self.on_any_build_start(&source.to_color_string())
fn on_download_progress(&self, id: usize, bytes: u64) {
self.state.lock().unwrap().bars[&id].inc(bytes);
}
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<Printer> 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<u64>) -> 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<Printer> 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<u64>) -> 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<u64>) -> 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<Mutex<Vec<ProgressBar>>>,
}
impl From<Printer> 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;