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)
This commit is contained in:
konsti 2025-02-06 00:38:02 +01:00 committed by GitHub
parent b83e25a911
commit 59b46bc216
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 95 additions and 21 deletions

View file

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

View file

@ -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";

View file

@ -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<bool> =
LazyLock::new(|| env::var(EnvVars::UV_TEST_NO_CLI_PROGRESS).is_ok());
#[derive(Debug)]
struct ProgressReporter {
@ -44,6 +51,8 @@ struct BarState {
sizes: Vec<u64>,
/// A map of progress bars, by ID.
bars: FxHashMap<usize, ProgressBar>,
/// The download size, if known, by ID.
download_size: FxHashMap<usize, Option<u64>>,
/// 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<u64>) -> 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);
}
}

View file

@ -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());