From 59b46bc21691784ff842efa8f9d1c68b0d0c5cbf Mon Sep 17 00:00:00 2001 From: konsti Date: Thu, 6 Feb 2025 00:38:02 +0100 Subject: [PATCH] Show messages for builds and large downloads in non-interactive mode (#11165) When stderr is not a tty, we currently don't show any messages for build or large downloads, since indicatif is hidden. We can improve this by showing a message for: * Starting and finishing a large download (>1MB) * Starting and finishing a build Downloads are limited to 1MB or unknown size to keep the logs concise and not scroll the entire terminal away for a download that finishes almost immediately. These messages are not captured in the tests since their order is non-deterministic (downloads and builds race to finish). There are no "tick" messages for large downloads yet, we could e.g. show an update on runnning downloads every n seconds. Part of #11121 **Test Plan** ``` $ uv venv && FORCE_COLOR=1 cargo run -q pip install numpy --no-binary :all: --no-cache 2>&1 | tee a.txt Using CPython 3.13.0 Creating virtual environment at: .venv Activate with: source .venv/bin/activate Resolved 1 package in 221ms Building numpy==2.2.2 Built numpy==2.2.2 Prepared 1 package in 2m 34s Installed 1 package in 6ms + numpy==2.2.2 ``` ![image](https://github.com/user-attachments/assets/f4b64313-afa7-449f-9e5b-2b1b7026bef3) ``` $ uv venv && FORCE_COLOR=1 cargo run -q pip install torch --no-cache 2>&1 | tee b.txt Using CPython 3.13.0 Creating virtual environment at: .venv Activate with: source .venv/bin/activate Resolved 24 packages in 648ms Downloading setuptools (1.2MiB) Downloading nvidia-cuda-cupti-cu12 (13.2MiB) Downloading torch (731.1MiB) Downloading nvidia-nvjitlink-cu12 (20.1MiB) Downloading nvidia-cufft-cu12 (201.7MiB) Downloading nvidia-cuda-nvrtc-cu12 (23.5MiB) Downloading nvidia-curand-cu12 (53.7MiB) Downloading nvidia-nccl-cu12 (179.9MiB) Downloading nvidia-cudnn-cu12 (634.0MiB) Downloading nvidia-cublas-cu12 (346.6MiB) Downloading sympy (5.9MiB) Downloading nvidia-cusparse-cu12 (197.8MiB) Downloading nvidia-cusparselt-cu12 (143.1MiB) Downloading networkx (1.6MiB) Downloading nvidia-cusolver-cu12 (122.0MiB) Downloading triton (241.4MiB) Downloaded setuptools Downloaded networkx Downloaded sympy Downloaded nvidia-cuda-cupti-cu12 Downloaded nvidia-nvjitlink-cu12 Downloaded nvidia-cuda-nvrtc-cu12 Downloaded nvidia-curand-cu12 [...] ``` ![image](https://github.com/user-attachments/assets/71918d94-c5c0-44ce-bea8-aaba6cd80ef7) --- crates/uv-git/src/source.rs | 2 +- crates/uv-static/src/env_vars.rs | 4 ++ crates/uv/src/commands/reporters.rs | 107 ++++++++++++++++++++++------ crates/uv/tests/it/common/mod.rs | 3 + 4 files changed, 95 insertions(+), 21 deletions(-) diff --git a/crates/uv-git/src/source.rs b/crates/uv-git/src/source.rs index 58599f6512a8..9cbefc60a08d 100644 --- a/crates/uv-git/src/source.rs +++ b/crates/uv-git/src/source.rs @@ -133,7 +133,7 @@ impl GitSource { // Report the checkout operation to the reporter. if let Some(task) = task { if let Some(reporter) = self.reporter.as_ref() { - reporter.on_checkout_complete(remote.url(), short_id.as_str(), task); + reporter.on_checkout_complete(remote.url(), actual_rev.as_str(), task); } } diff --git a/crates/uv-static/src/env_vars.rs b/crates/uv-static/src/env_vars.rs index 6a5c90c3e94a..aa35eb162ce3 100644 --- a/crates/uv-static/src/env_vars.rs +++ b/crates/uv-static/src/env_vars.rs @@ -579,6 +579,10 @@ impl EnvVars { #[attr_hidden] pub const UV_TEST_INDEX_URL: &'static str = "UV_TEST_INDEX_URL"; + /// Hide progress messages with non-deterministic order in tests. + #[attr_hidden] + pub const UV_TEST_NO_CLI_PROGRESS: &'static str = "UV_TEST_NO_CLI_PROGRESS"; + /// `.env` files from which to load environment variables when executing `uv run` commands. pub const UV_ENV_FILE: &'static str = "UV_ENV_FILE"; diff --git a/crates/uv/src/commands/reporters.rs b/crates/uv/src/commands/reporters.rs index 1e3fdb1e50cd..b3a022e5bdba 100644 --- a/crates/uv/src/commands/reporters.rs +++ b/crates/uv/src/commands/reporters.rs @@ -1,4 +1,6 @@ use std::env; +use std::fmt::Write; +use std::sync::LazyLock; use std::sync::{Arc, Mutex}; use std::time::Duration; @@ -7,6 +9,8 @@ use owo_colors::OwoColorize; use rustc_hash::FxHashMap; use url::Url; +use crate::commands::human_readable_bytes; +use crate::printer::Printer; use uv_cache::Removal; use uv_distribution_types::{ BuildableSource, CachedDist, DistributionMetadata, Name, SourceDist, VersionOrUrlRef, @@ -16,7 +20,10 @@ use uv_pep440::Version; use uv_python::PythonInstallationKey; use uv_static::EnvVars; -use crate::printer::Printer; +/// Since downloads, fetches and builds run in parallel, their message output order is +/// non-deterministic, so can't capture them in test output. +static HAS_UV_TEST_NO_CLI_PROGRESS: LazyLock = + LazyLock::new(|| env::var(EnvVars::UV_TEST_NO_CLI_PROGRESS).is_ok()); #[derive(Debug)] struct ProgressReporter { @@ -44,6 +51,8 @@ struct BarState { sizes: Vec, /// A map of progress bars, by ID. bars: FxHashMap, + /// The download size, if known, by ID. + download_size: FxHashMap>, /// A monotonic counter for bar IDs. id: usize, } @@ -95,11 +104,15 @@ impl ProgressReporter { ); progress.set_style(ProgressStyle::with_template("{wide_msg}").unwrap()); - progress.set_message(format!( - "{} {}", + let message = format!( + " {} {}", "Building".bold().cyan(), source.to_color_string() - )); + ); + if multi_progress.is_hidden() && !*HAS_UV_TEST_NO_CLI_PROGRESS { + let _ = writeln!(self.printer.stderr(), "{message}"); + } + progress.set_message(message); state.headers += 1; state.bars.insert(id, progress); @@ -107,7 +120,11 @@ impl ProgressReporter { } fn on_build_complete(&self, source: &BuildableSource, id: usize) { - let ProgressMode::Multi { state, .. } = &self.mode else { + let ProgressMode::Multi { + state, + multi_progress, + } = &self.mode + else { return; }; @@ -117,11 +134,15 @@ impl ProgressReporter { state.bars.remove(&id).unwrap() }; - progress.finish_with_message(format!( - " {} {}", + let message = format!( + " {} {}", "Built".bold().green(), source.to_color_string() - )); + ); + if multi_progress.is_hidden() && !*HAS_UV_TEST_NO_CLI_PROGRESS { + let _ = writeln!(self.printer.stderr(), "{message}"); + } + progress.finish_with_message(message); } fn on_download_start(&self, name: String, size: Option) -> usize { @@ -145,7 +166,7 @@ impl ProgressReporter { ProgressBar::with_draw_target(size, self.printer.target()), ); - if size.is_some() { + if let Some(size) = size { // We're using binary bytes to match `human_readable_bytes`. progress.set_style( ProgressStyle::with_template( @@ -154,15 +175,36 @@ impl ProgressReporter { .unwrap() .progress_chars("--"), ); + // If the download is larger than 1MB, show a message to indicate that this may take + // a while keeping the log concise. + if multi_progress.is_hidden() && !*HAS_UV_TEST_NO_CLI_PROGRESS && size > 1024 * 1024 { + let (bytes, unit) = human_readable_bytes(size); + let _ = writeln!( + self.printer.stderr(), + "{} {} {}", + "Downloading".bold().cyan(), + name, + format!("({bytes:.1}{unit})").dimmed() + ); + } progress.set_message(name); } else { progress.set_style(ProgressStyle::with_template("{wide_msg:.dim} ....").unwrap()); + if multi_progress.is_hidden() && !*HAS_UV_TEST_NO_CLI_PROGRESS { + let _ = writeln!( + self.printer.stderr(), + "{} {}", + "Downloading".bold().cyan(), + name + ); + } progress.set_message(name); progress.finish(); } let id = state.id(); state.bars.insert(id, progress); + state.download_size.insert(id, size); id } @@ -175,11 +217,29 @@ impl ProgressReporter { } fn on_download_complete(&self, id: usize) { - let ProgressMode::Multi { state, .. } = &self.mode else { + let ProgressMode::Multi { + state, + multi_progress, + } = &self.mode + else { return; }; let progress = state.lock().unwrap().bars.remove(&id).unwrap(); + + let size = state.lock().unwrap().download_size[&id]; + if multi_progress.is_hidden() + && !*HAS_UV_TEST_NO_CLI_PROGRESS + && size.is_none_or(|size| size > 1024 * 1024) + { + let _ = writeln!( + self.printer.stderr(), + " {} {}", + "Downloaded".bold().green(), + progress.message() + ); + } + progress.finish_and_clear(); } @@ -201,12 +261,11 @@ impl ProgressReporter { ); progress.set_style(ProgressStyle::with_template("{wide_msg}").unwrap()); - progress.set_message(format!( - "{} {} ({})", - "Updating".bold().cyan(), - url, - rev.dimmed() - )); + let message = format!(" {} {} ({})", "Updating".bold().cyan(), url, rev.dimmed()); + if multi_progress.is_hidden() && !*HAS_UV_TEST_NO_CLI_PROGRESS { + let _ = writeln!(self.printer.stderr(), "{message}"); + } + progress.set_message(message); progress.finish(); state.headers += 1; @@ -215,7 +274,11 @@ impl ProgressReporter { } fn on_checkout_complete(&self, url: &Url, rev: &str, id: usize) { - let ProgressMode::Multi { state, .. } = &self.mode else { + let ProgressMode::Multi { + state, + multi_progress, + } = &self.mode + else { return; }; @@ -225,12 +288,16 @@ impl ProgressReporter { state.bars.remove(&id).unwrap() }; - progress.finish_with_message(format!( - " {} {} ({})", + let message = format!( + " {} {} ({})", "Updated".bold().green(), url, rev.dimmed() - )); + ); + if multi_progress.is_hidden() && !*HAS_UV_TEST_NO_CLI_PROGRESS { + let _ = writeln!(self.printer.stderr(), "{message}"); + } + progress.finish_with_message(message); } } diff --git a/crates/uv/tests/it/common/mod.rs b/crates/uv/tests/it/common/mod.rs index da96add6af92..a4dd1f187804 100644 --- a/crates/uv/tests/it/common/mod.rs +++ b/crates/uv/tests/it/common/mod.rs @@ -555,6 +555,9 @@ impl TestContext { .env(EnvVars::UV_PYTHON_DOWNLOADS, "never") .env(EnvVars::UV_TEST_PYTHON_PATH, self.python_path()) .env(EnvVars::UV_EXCLUDE_NEWER, EXCLUDE_NEWER) + // Since downloads, fetches and builds run in parallel, their message output order is + // non-deterministic, so can't capture them in test output. + .env(EnvVars::UV_TEST_NO_CLI_PROGRESS, "1") .env_remove(EnvVars::UV_CACHE_DIR) .env_remove(EnvVars::UV_TOOL_BIN_DIR) .current_dir(self.temp_dir.path());