diff --git a/crates/cargo-test-support/src/compare.rs b/crates/cargo-test-support/src/compare.rs index 7ebb330f8ce..04fedc25fdc 100644 --- a/crates/cargo-test-support/src/compare.rs +++ b/crates/cargo-test-support/src/compare.rs @@ -222,6 +222,12 @@ fn substitute_macros(input: &str) -> String { ("[REPLACING]", " Replacing"), ("[UNPACKING]", " Unpacking"), ("[SUMMARY]", " Summary"), + ("[DESTINATION]", " Destination"), + ("[INSTALL]", " Install"), + ("[UPGRADE]", " Upgrade"), + ("[DOWNGRADE]", " Downgrade"), + ("[UPTODATE]", " Up-to-date"), + ("[REINSTALL]", " Re-install"), ("[FIXED]", " Fixed"), ("[FIXING]", " Fixing"), ("[EXE]", env::consts::EXE_SUFFIX), diff --git a/src/bin/cargo/commands/install.rs b/src/bin/cargo/commands/install.rs index e188d6a0d67..ec86e91be0e 100644 --- a/src/bin/cargo/commands/install.rs +++ b/src/bin/cargo/commands/install.rs @@ -74,6 +74,13 @@ pub fn cli() -> Command { "list", "List all installed packages and their versions", )) + .arg( + flag( + "dry-run", + "Display what would be installed without actually performing anything (unstable)", + ) + .short('n'), + ) .arg_ignore_rust_version() .arg_message_format() .arg_silent_suggestion() @@ -201,6 +208,9 @@ pub fn exec(gctx: &mut GlobalContext, args: &ArgMatches) -> CliResult { if args.flag("list") { ops::install_list(root, gctx)?; } else { + if args.flag("dry-run") { + gctx.cli_unstable().fail_if_stable_opt("--dry-run", 11123)?; + } ops::install( gctx, root, @@ -210,6 +220,7 @@ pub fn exec(gctx: &mut GlobalContext, args: &ArgMatches) -> CliResult { &compile_opts, args.flag("force"), args.flag("no-track"), + args.flag("dry-run"), )?; } Ok(()) diff --git a/src/cargo/ops/cargo_install.rs b/src/cargo/ops/cargo_install.rs index 4e51ef30ea4..fccc9c2c9ce 100644 --- a/src/cargo/ops/cargo_install.rs +++ b/src/cargo/ops/cargo_install.rs @@ -1,3 +1,4 @@ +use std::cmp::Ordering; use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet}; use std::path::{Path, PathBuf}; use std::sync::Arc; @@ -15,7 +16,7 @@ use crate::{drop_println, ops}; use anyhow::{bail, Context as _}; use cargo_util::paths; -use cargo_util_schemas::core::PartialVersion; +use cargo_util_schemas::core::{PartialVersion, SourceKind}; use itertools::Itertools; use semver::VersionReq; use tempfile::Builder as TempFileBuilder; @@ -619,6 +620,7 @@ pub fn install( opts: &ops::CompileOptions, force: bool, no_track: bool, + dry_run: bool, ) -> CargoResult<()> { let root = resolve_root(root, gctx)?; let dst = root.join("bin").into_path_unlocked(); @@ -639,7 +641,7 @@ pub fn install( .unwrap_or((None, None)); let installable_pkg = InstallablePackage::new( gctx, - root, + root.clone(), map, krate, source_id, @@ -651,11 +653,59 @@ pub fn install( true, current_rust_version.as_ref(), )?; - let mut installed_anything = true; - if let Some(installable_pkg) = installable_pkg { - installed_anything = installable_pkg.install_one()?; + if dry_run { + match installable_pkg { + Some(installable_pkg) => dry_run_report( + gctx, + &root, + dst.as_path(), + &[( + &krate + .unwrap_or_else(|| installable_pkg.pkg.name().as_str()) + .to_owned(), + installable_pkg, + )], + &[], + &[], + )?, + None => dry_run_report( + gctx, + &root, + dst.as_path(), + &[], + &[krate.map_or_else( + || -> CargoResult<&str> { + // `krate` is `None` usually for a Git source at + // this point. Select the package again in order to + // be able to retrieve its name. + if let SourceKind::Git(_) = source_id.kind() { + Ok(select_pkg( + &mut GitSource::new(source_id, gctx)?, + None, + |git_src| git_src.read_packages(), + gctx, + current_rust_version.as_ref(), + )? + .name() + .as_str()) + } else { + Ok("") + } + }, + |kr| Ok(kr), + )?], + &[], + )?, + } + + (false, false) + } else { + let mut installed_anything = true; + if let Some(installable_pkg) = installable_pkg { + installed_anything = installable_pkg.install_one()?; + } + (installed_anything, false) } - (installed_anything, false) } else { let mut succeeded = vec![]; let mut failed = vec![]; @@ -702,40 +752,52 @@ pub fn install( }) .collect(); - let install_results: Vec<_> = pkgs_to_install - .into_iter() - .map(|(krate, installable_pkg)| (krate, installable_pkg.install_one())) - .collect(); + if dry_run { + dry_run_report( + gctx, + &root, + dst.as_path(), + &pkgs_to_install, + &succeeded, + &failed, + )?; + (false, !failed.is_empty()) + } else { + let install_results: Vec<_> = pkgs_to_install + .into_iter() + .map(|(krate, installable_pkg)| (krate, installable_pkg.install_one())) + .collect(); - for (krate, result) in install_results { - match result { - Ok(installed) => { - if installed { - succeeded.push(krate); + for (krate, result) in install_results { + match result { + Ok(installed) => { + if installed { + succeeded.push(krate); + } + } + Err(e) => { + crate::display_error(&e, &mut gctx.shell()); + failed.push(krate); } - } - Err(e) => { - crate::display_error(&e, &mut gctx.shell()); - failed.push(krate); } } - } - let mut summary = vec![]; - if !succeeded.is_empty() { - summary.push(format!("Successfully installed {}!", succeeded.join(", "))); - } - if !failed.is_empty() { - summary.push(format!( - "Failed to install {} (see error(s) above).", - failed.join(", ") - )); - } - if !succeeded.is_empty() || !failed.is_empty() { - gctx.shell().status("Summary", summary.join(" "))?; - } + let mut summary = vec![]; + if !succeeded.is_empty() { + summary.push(format!("Successfully installed {}!", succeeded.join(", "))); + } + if !failed.is_empty() { + summary.push(format!( + "Failed to install {} (see error(s) above).", + failed.join(", ") + )); + } + if !succeeded.is_empty() || !failed.is_empty() { + gctx.shell().status("Summary", summary.join(" "))?; + } - (!succeeded.is_empty(), !failed.is_empty()) + (!succeeded.is_empty(), !failed.is_empty()) + } }; if installed_anything { @@ -760,6 +822,90 @@ pub fn install( Ok(()) } +/// Sends to `gctx`'s shell status messages reporting an early dry-run of a +/// currently-ongoing installation. +/// +/// * `pkgs_to_install`: The packages that need to be installed from scratch +/// or updated to match the requested requirements. +/// * `uptodate_pkgs`: The package names that are already up-to-date. +/// * `failed_pkgs`: The package names that had some kind of failure. +fn dry_run_report( + gctx: &GlobalContext, + root: &Filesystem, + dst: &Path, + pkgs_to_install: &[(&String, InstallablePackage<'_>)], + uptodate_pkgs: &[&str], + failed_pkgs: &[&str], +) -> CargoResult<()> { + let mut pkgs_needing_fresh_install = Vec::new(); + let mut pkgs_needing_update = Vec::new(); + let instld_pkg_vers_by_semi_ids = InstallTracker::load(gctx, root)? + .all_installed_bins() + .map(|(pkg_id, _)| ((pkg_id.name(), pkg_id.source_id()), pkg_id.version())) + .collect::>(); + + for (krate, instlbl_pkg) in pkgs_to_install { + let instlbl_pkg_id = instlbl_pkg.pkg.package_id(); + + if instld_pkg_vers_by_semi_ids + .contains_key(&(instlbl_pkg_id.name(), instlbl_pkg_id.source_id())) + { + &mut pkgs_needing_update + } else { + &mut pkgs_needing_fresh_install + } + .push((krate, instlbl_pkg)); + } + + let mut shell = gctx.shell(); + shell.status("Summary", "dry-run")?; + shell.status("Destination", dst.display())?; + + for (krate, instlbl_pkg) in pkgs_needing_fresh_install { + let instlbl_pkg_id = instlbl_pkg.pkg.package_id(); + shell.status( + "Install", + format!( + "{krate}: to {} using {}", + instlbl_pkg_id.version(), + instlbl_pkg_id.source_id(), + ), + )?; + } + + for (krate, instlbl_pkg) in pkgs_needing_update { + let instlbl_pkg_id = instlbl_pkg.pkg.package_id(); + let instlbl_pkg_id_ver = instlbl_pkg_id.version(); + let instld_pkg_ver = instld_pkg_vers_by_semi_ids + .get(&(instlbl_pkg_id.name(), instlbl_pkg_id.source_id())) + .unwrap(); + + shell.status( + match instlbl_pkg_id_ver.cmp(&instld_pkg_ver) { + Ordering::Greater => "Upgrade", + Ordering::Equal => "Re-install", + Ordering::Less => "Downgrade", + }, + format!( + "{krate}: from {} to {} using {}", + instld_pkg_ver, + instlbl_pkg_id_ver, + instlbl_pkg_id.source_id(), + ), + )?; + } + + for krate in uptodate_pkgs { + shell.status("Up-to-date", krate)?; + } + + for krate in failed_pkgs { + shell.status("Failed", krate)?; + } + + Ok(()) +} + fn is_installed( pkg: &Package, gctx: &GlobalContext, diff --git a/src/doc/man/cargo-install.md b/src/doc/man/cargo-install.md index ff149271a41..a8feabc0f91 100644 --- a/src/doc/man/cargo-install.md +++ b/src/doc/man/cargo-install.md @@ -137,6 +137,20 @@ protect against multiple concurrent invocations of Cargo installing at the same time. {{/option}} +{{#option "`-n`" "`--dry-run`" }} +(unstable) Cut the install operation short and display what it *would* have +usually done. The normal version check and crate downloads still occur exactly +like without the option flag, but the compilation and the actual installation +do not. Only the operation that would have been performed is displayed for each +requested package, one of: + * fresh install if the package is not installed yet, + * upgrade if it is already installed but a newer version exists, + * downgrade if it is already installed but an older version has been + explicitely requested, + * nothing if it is already installed and up-to-date, + * re-install if it is already installed and up-to-date but `--force` was set. +{{/option}} + {{#option "`--bin` _name_..." }} Install only the specified binary. {{/option}} diff --git a/src/doc/man/generated_txt/cargo-install.txt b/src/doc/man/generated_txt/cargo-install.txt index 7b162c85d35..75af35b4f4b 100644 --- a/src/doc/man/generated_txt/cargo-install.txt +++ b/src/doc/man/generated_txt/cargo-install.txt @@ -135,6 +135,26 @@ OPTIONS multiple concurrent invocations of Cargo installing at the same time. + -n, --dry-run + (unstable) Cut the install operation short and display what it would + have usually done. The normal version check and crate downloads + still occur exactly like without the option flag, but the + compilation and the actual installation do not. Only the operation + that would have been performed is displayed for each requested + package, one of: + + o fresh install if the package is not installed yet, + + o upgrade if it is already installed but a newer version exists, + + o downgrade if it is already installed but an older version has + been explicitely requested, + + o nothing if it is already installed and up-to-date, + + o re-install if it is already installed and up-to-date but --force + was set. + --bin name… Install only the specified binary. diff --git a/src/doc/src/commands/cargo-install.md b/src/doc/src/commands/cargo-install.md index f4b7a611604..f232c0abfbc 100644 --- a/src/doc/src/commands/cargo-install.md +++ b/src/doc/src/commands/cargo-install.md @@ -141,6 +141,23 @@ protect against multiple concurrent invocations of Cargo installing at the same time. +
-n
+
--dry-run
+
(unstable) Cut the install operation short and display what it would have +usually done. The normal version check and crate downloads still occur exactly +like without the option flag, but the compilation and the actual installation +do not. Only the operation that would have been performed is displayed for each +requested package, one of:

+
+ +
--bin name
Install only the specified binary.
diff --git a/src/etc/man/cargo-install.1 b/src/etc/man/cargo-install.1 index 44de0a8053a..cd066b54649 100644 --- a/src/etc/man/cargo-install.1 +++ b/src/etc/man/cargo-install.1 @@ -170,6 +170,37 @@ protect against multiple concurrent invocations of Cargo installing at the same time. .RE .sp +\fB\-n\fR, +\fB\-\-dry\-run\fR +.RS 4 +(unstable) Cut the install operation short and display what it \fIwould\fR have +usually done. The normal version check and crate downloads still occur exactly +like without the option flag, but the compilation and the actual installation +do not. Only the operation that would have been performed is displayed for each +requested package, one of: +.sp +.RS 4 +\h'-04'\(bu\h'+02'fresh install if the package is not installed yet, +.RE +.sp +.RS 4 +\h'-04'\(bu\h'+02'upgrade if it is already installed but a newer version exists, +.RE +.sp +.RS 4 +\h'-04'\(bu\h'+02'downgrade if it is already installed but an older version has been +explicitely requested, +.RE +.sp +.RS 4 +\h'-04'\(bu\h'+02'nothing if it is already installed and up\-to\-date, +.RE +.sp +.RS 4 +\h'-04'\(bu\h'+02're\-install if it is already installed and up\-to\-date but \fB\-\-force\fR was set. +.RE +.RE +.sp \fB\-\-bin\fR \fIname\fR\[u2026] .RS 4 Install only the specified binary. diff --git a/tests/testsuite/cargo_install/help/stdout.term.svg b/tests/testsuite/cargo_install/help/stdout.term.svg index 5fac4877432..ccd06b44bae 100644 --- a/tests/testsuite/cargo_install/help/stdout.term.svg +++ b/tests/testsuite/cargo_install/help/stdout.term.svg @@ -1,4 +1,4 @@ - +