diff --git a/crates/uv/src/commands/mod.rs b/crates/uv/src/commands/mod.rs index 37f1814f3..f0a054190 100644 --- a/crates/uv/src/commands/mod.rs +++ b/crates/uv/src/commands/mod.rs @@ -1,5 +1,5 @@ -use std::process::ExitCode; use std::time::Duration; +use std::{fmt::Display, process::ExitCode}; pub(crate) use clean::clean; use distribution_types::InstalledMetadata; @@ -8,6 +8,7 @@ pub(crate) use pip_compile::{extra_name_with_clap_error, pip_compile, Upgrade}; pub(crate) use pip_install::pip_install; pub(crate) use pip_sync::pip_sync; pub(crate) use pip_uninstall::pip_uninstall; +use uv_normalize::PackageName; pub(crate) use venv::venv; mod clean; @@ -70,3 +71,10 @@ pub(super) struct ChangeEvent { dist: T, kind: ChangeEventKind, } + +#[derive(Debug)] +pub(super) struct DryRunEvent { + name: PackageName, + version: T, + kind: ChangeEventKind, +} diff --git a/crates/uv/src/commands/pip_install.rs b/crates/uv/src/commands/pip_install.rs index 8e8add06a..2a5b4b9cc 100644 --- a/crates/uv/src/commands/pip_install.rs +++ b/crates/uv/src/commands/pip_install.rs @@ -2,6 +2,7 @@ use std::collections::HashSet; use std::fmt::Write; use std::path::Path; +use std::time::Instant; use anstream::eprint; use anyhow::{anyhow, Context, Result}; @@ -40,7 +41,7 @@ use crate::commands::{elapsed, ChangeEvent, ChangeEventKind, ExitStatus}; use crate::printer::Printer; use crate::requirements::{ExtrasSpecification, RequirementsSource, RequirementsSpecification}; -use super::Upgrade; +use super::{DryRunEvent, Upgrade}; /// Install packages into the current environment. #[allow(clippy::too_many_arguments)] @@ -137,6 +138,9 @@ pub(crate) async fn pip_install( ) .dimmed() )?; + if dry_run { + writeln!(printer, "Would make no changes")?; + } return Ok(ExitStatus::Success); } @@ -503,12 +507,7 @@ async fn install( .map(ResolvedEditable::Built) .collect::>(); - let Plan { - local, - remote, - reinstalls, - extraneous: _, - } = Planner::with_requirements(&requirements) + let plan = Planner::with_requirements(&requirements) .with_editable_requirements(editables) .build( site_packages, @@ -522,71 +521,16 @@ async fn install( .context("Failed to determine installation plan")?; if dry_run { - if !remote.is_empty() { - writeln!( - printer, - "{} The following packages would be downloaded:", - "DRY RUN".white().bold() - )?; - for dist in &remote { - let version = resolution - .get(&dist.name) - .map(|r| r.version().map_or(String::new(), |v| format!("=={v}"))) - .unwrap_or_default(); - writeln!( - printer, - " {} {}{}", - "~".blue(), - dist.name.as_ref().white().bold(), - version.dimmed() - )?; - } - } - - if !reinstalls.is_empty() { - writeln!( - printer, - "{} The following packages would be reinstalled:", - "DRY RUN".white().bold() - )?; - for dist_info in &reinstalls { - let version = resolution - .get(dist_info.name()) - .map(|r| r.version().map_or(String::new(), |v| format!("=={v}"))) - .unwrap_or_default(); - writeln!( - printer, - " {} {}{}", - "~".blue(), - dist_info.name().white().bold(), - version.dimmed() - )?; - } - } - - if !local.is_empty() { - writeln!( - printer, - "{} The following packages would be installed from local cache:", - "DRY RUN".white().bold() - )?; - for local_dist in &local { - let version = resolution - .get(local_dist.name()) - .map(|r| r.version().map_or(String::new(), |v| format!("=={v}"))) - .unwrap_or_default(); - writeln!( - printer, - " {} {}{}", - "~".blue(), - local_dist.name().white().bold(), - version.dimmed() - )?; - } - } - return Ok(()); + return report_dry_run(resolution, plan, start, printer); } + let Plan { + local, + remote, + reinstalls, + extraneous: _, + } = plan; + // Nothing to do. if remote.is_empty() && local.is_empty() && reinstalls.is_empty() { let s = if resolution.len() == 1 { "" } else { "s" }; @@ -600,7 +544,6 @@ async fn install( ) .dimmed() )?; - return Ok(()); } @@ -750,6 +693,140 @@ async fn install( Ok(()) } +fn report_dry_run( + resolution: &Resolution, + plan: Plan, + start: Instant, + mut printer: Printer, +) -> Result<(), Error> { + let Plan { + local, + remote, + reinstalls, + extraneous: _, + } = plan; + + // Nothing to do. + if remote.is_empty() && local.is_empty() && reinstalls.is_empty() { + let s = if resolution.len() == 1 { "" } else { "s" }; + writeln!( + printer, + "{}", + format!( + "Audited {} in {}", + format!("{} package{}", resolution.len(), s).bold(), + elapsed(start.elapsed()) + ) + .dimmed() + )?; + writeln!(printer, "Would make no changes")?; + return Ok(()); + } + + // Map any registry-based requirements back to those returned by the resolver. + let remote = remote + .iter() + .map(|dist| { + resolution + .get(&dist.name) + .cloned() + .expect("Resolution should contain all packages") + }) + .collect::>(); + + // Download, build, and unzip any missing distributions. + let wheels = if remote.is_empty() { + vec![] + } else { + let s = if remote.len() == 1 { "" } else { "s" }; + writeln!( + printer, + "{}", + format!( + "Would download {}", + format!("{} package{}", remote.len(), s).bold(), + ) + .dimmed() + )?; + remote + }; + + // Remove any existing installations. + if !reinstalls.is_empty() { + let s = if reinstalls.len() == 1 { "" } else { "s" }; + writeln!( + printer, + "{}", + format!( + "Would uninstall {}", + format!("{} package{}", reinstalls.len(), s).bold(), + ) + .dimmed() + )?; + } + + // Install the resolved distributions. + let installs = wheels.len() + local.len(); + + if installs > 0 { + let s = if installs == 1 { "" } else { "s" }; + writeln!( + printer, + "{}", + format!( + "Would install {}", + format!("{} package{}", installs, s).bold(), + ) + .dimmed() + )?; + } + + for event in reinstalls + .into_iter() + .map(|distribution| DryRunEvent { + name: distribution.name().clone(), + version: distribution.version().to_string(), + kind: ChangeEventKind::Removed, + }) + .chain(wheels.into_iter().map(|distribution| DryRunEvent { + name: distribution.name().clone(), + version: distribution.version().unwrap().to_string(), + kind: ChangeEventKind::Added, + })) + .chain(local.into_iter().map(|distribution| DryRunEvent { + name: distribution.name().clone(), + version: distribution.installed_version().to_string(), + kind: ChangeEventKind::Added, + })) + .sorted_unstable_by(|a, b| a.name.cmp(&b.name).then_with(|| a.kind.cmp(&b.kind))) + { + match event.kind { + ChangeEventKind::Added => { + writeln!( + printer, + " {} {}{}{}", + "+".green(), + event.name.as_ref().bold(), + "==".dimmed(), + event.version.dimmed() + )?; + } + ChangeEventKind::Removed => { + writeln!( + printer, + " {} {}{}{}", + "-".red(), + event.name.as_ref().bold(), + "==".dimmed(), + event.version.dimmed() + )?; + } + } + } + + Ok(()) +} + /// Validate the installed packages in the virtual environment. fn validate(resolution: &Resolution, venv: &Virtualenv, mut printer: Printer) -> Result<(), Error> { let site_packages = SitePackages::from_executable(venv)?;