Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion crates/uv-cache/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1162,7 +1162,7 @@ pub enum CacheBucket {
}

impl CacheBucket {
fn to_str(self) -> &'static str {
pub fn to_str(self) -> &'static str {
match self {
// Note that when bumping this, you'll also need to bump it
// in `crates/uv/tests/it/cache_prune.rs`.
Expand Down Expand Up @@ -1309,6 +1309,7 @@ impl CacheBucket {
Self::Archive,
Self::Builds,
Self::Environments,
Self::Python,
Self::Binaries,
]
.iter()
Expand Down
5 changes: 5 additions & 0 deletions crates/uv-cli/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -894,6 +894,11 @@ pub enum CacheCommand {
/// wheels, source distributions, and other cached data. By default, outputs the size in raw
/// bytes; use `--human` for human-readable output.
Size(SizeArgs),
/// Show cache utilization information.
///
/// Displays detailed usage statistics for each cache bucket, similar to `docker system df`.
/// Shows the number of entries and size for each cache type.
Df,
}

#[derive(Args, Debug)]
Expand Down
165 changes: 165 additions & 0 deletions crates/uv/src/commands/cache_df.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
use std::fmt::Write;
use std::path::Path;

use anyhow::Result;
use diskus::DiskUsage;

use crate::commands::{ExitStatus, human_readable_bytes};
use crate::printer::Printer;
use uv_cache::Cache;
use uv_cache::CacheBucket;

fn format_human_readable(bytes: u64) -> (String, &'static str) {
if bytes == 0 {
("0".to_string(), "B")
} else {
let (val, unit) = human_readable_bytes(bytes);
(format!("{val:.1}"), unit)
}
}

pub(crate) fn cache_df(cache: &Cache, printer: Printer) -> Result<ExitStatus> {
// Define all cache buckets with their descriptions
let buckets = [
(
CacheBucket::Wheels,
"Wheels",
"Downloaded and cached wheels from registries and direct URLs",
),
(
CacheBucket::SourceDistributions,
"Source Distributions",
"Source distributions and built wheels",
),
(
CacheBucket::Simple,
"Simple Metadata",
"Package metadata from simple repositories",
),
(
CacheBucket::Git,
"Git Repositories",
"Cloned git repositories",
),
(
CacheBucket::Interpreter,
"Interpreter Info",
"Cached Python interpreter information",
),
(CacheBucket::FlatIndex, "Flat Index", "Flat index responses"),
(
CacheBucket::Archive,
"Archive",
"Shared archive storage for directories",
),
(
CacheBucket::Builds,
"Build Environments",
"Ephemeral environments for builds",
),
(
CacheBucket::Environments,
"Environments",
"Reusable tool environments",
),
(CacheBucket::Python, "Python", "Cached Python downloads"),
(
CacheBucket::Binaries,
"Binaries",
"Downloaded tool binaries",
),
];

// Calculate total size (will be 0 if cache directory doesn't exist)
let total_bytes = if cache.root().exists() {
let disk_usage = DiskUsage::new(vec![cache.root().to_path_buf()]);
disk_usage.count_ignoring_errors()
} else {
0
};

writeln!(printer.stdout_important(), "CACHE UTILIZATION")?;
writeln!(printer.stdout(), "{}", "=".repeat(80))?;

// Display table header
writeln!(
printer.stdout(),
"{:<25} {:>12} {:>12} {:<30}",
"Cache Type",
"Count",
"Size",
"Description"
)?;
writeln!(printer.stdout(), "{}", "-".repeat(80))?;

let mut total_count = 0u64;

for (bucket, name, description) in buckets {
let bucket_path = cache.root().join(bucket.to_str());
let (size, count) = if bucket_path.exists() {
let disk_usage = DiskUsage::new(vec![bucket_path.clone()]);
let size = disk_usage.count_ignoring_errors();
let count = count_files_in_directory(&bucket_path);
(size, count)
} else {
(0, 0)
};

total_count += count;

let (size_val, size_unit) = format_human_readable(size);

writeln!(
printer.stdout(),
"{name:<25} {count:>12} {size_val:>8}{size_unit:<4} {description:<30}"
)?;
}

writeln!(printer.stdout(), "{}", "=".repeat(80))?;

// Display total
let (total_size_val, total_size_unit) = format_human_readable(total_bytes);

writeln!(
printer.stdout(),
"{:<25} {:>12} {:>8}{:<4}",
"TOTAL",
total_count,
total_size_val,
total_size_unit
)?;

writeln!(printer.stdout())?;
writeln!(
printer.stdout(),
"Cache directory: {}",
cache.root().display()
)?;

Ok(ExitStatus::Success)
}

fn count_files_in_directory(dir: &Path) -> u64 {
if !dir.exists() {
return 0;
}

let mut count = 0u64;
let mut stack = vec![dir.to_path_buf()];

while let Some(current) = stack.pop() {
let Ok(entries) = fs_err::read_dir(&current) else {
continue;
};

for entry in entries.filter_map(Result::ok) {
count += 1;
let path = entry.path();
if path.is_dir() {
stack.push(path);
}
}
}

count
}
2 changes: 2 additions & 0 deletions crates/uv/src/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ pub(crate) use auth::logout::logout as auth_logout;
pub(crate) use auth::token::token as auth_token;
pub(crate) use build_frontend::build_frontend;
pub(crate) use cache_clean::cache_clean;
pub(crate) use cache_df::cache_df;
pub(crate) use cache_dir::cache_dir;
pub(crate) use cache_prune::cache_prune;
pub(crate) use cache_size::cache_size;
Expand Down Expand Up @@ -78,6 +79,7 @@ mod auth;
pub(crate) mod build_backend;
mod build_frontend;
mod cache_clean;
mod cache_df;
mod cache_dir;
mod cache_prune;
mod cache_size;
Expand Down
3 changes: 3 additions & 0 deletions crates/uv/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1089,6 +1089,9 @@ async fn run(mut cli: Cli) -> Result<ExitStatus> {
Commands::Cache(CacheNamespace {
command: CacheCommand::Size(args),
}) => commands::cache_size(&cache, args.human, printer, globals.preview),
Commands::Cache(CacheNamespace {
command: CacheCommand::Df,
}) => commands::cache_df(&cache, printer),
Commands::Build(args) => {
// Resolve the settings from the command-line arguments and workspace configuration.
let args = settings::BuildSettings::resolve(args, filesystem, environment);
Expand Down
38 changes: 38 additions & 0 deletions crates/uv/tests/it/cache_df.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
use crate::common::{TestContext, uv_snapshot};
use assert_cmd::assert::OutputAssertExt;

/// `cache df` should succeed and print an empty table for a fresh cache.
#[test]
fn cache_df_empty() {
let context = TestContext::new("3.12").with_collapsed_whitespace();

// Ensure the cache starts empty (no rows should have non-zero values)
context.clean().assert().success();

uv_snapshot!(context.filters(), context.cache_df(), @r"
success: true
exit_code: 0
----- stdout -----
CACHE UTILIZATION
================================================================================
Cache Type Count Size Description
--------------------------------------------------------------------------------
Wheels 0 [SIZE] Downloaded and cached wheels from registries and direct URLs
Source Distributions 0 [SIZE] Source distributions and built wheels
Simple Metadata 0 [SIZE] Package metadata from simple repositories
Git Repositories 0 [SIZE] Cloned git repositories
Interpreter Info 0 [SIZE] Cached Python interpreter information
Flat Index 0 [SIZE] Flat index responses
Archive 0 [SIZE] Shared archive storage for directories
Build Environments 0 [SIZE] Ephemeral environments for builds
Environments 0 [SIZE] Reusable tool environments
Python 0 [SIZE] Cached Python downloads
Binaries 0 [SIZE] Downloaded tool binaries
================================================================================
TOTAL 0 [SIZE]

Cache directory: [CACHE_DIR]/

----- stderr -----
");
}
8 changes: 8 additions & 0 deletions crates/uv/tests/it/common/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1396,6 +1396,14 @@ impl TestContext {
command
}

/// Create a `uv cache df` command.
pub fn cache_df(&self) -> Command {
let mut command = Self::new_command();
command.arg("cache").arg("df");
self.add_shared_options(&mut command, false);
command
}

/// Create a `uv build_backend` command.
///
/// Note that this command is hidden and only invoking it through a build frontend is supported.
Expand Down
3 changes: 3 additions & 0 deletions crates/uv/tests/it/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ mod build_backend;
#[cfg(all(feature = "python", feature = "pypi"))]
mod cache_clean;

#[cfg(all(feature = "python", feature = "pypi"))]
mod cache_df;

#[cfg(all(feature = "python", feature = "pypi"))]
mod cache_prune;

Expand Down
Loading