diff --git a/src/cargo/ops/cargo_install.rs b/src/cargo/ops/cargo_install.rs index e450f82f3de..9b1b08a7254 100644 --- a/src/cargo/ops/cargo_install.rs +++ b/src/cargo/ops/cargo_install.rs @@ -4,15 +4,16 @@ use std::sync::Arc; use std::{env, fs}; use anyhow::{bail, format_err}; +use semver::VersionReq; use tempfile::Builder as TempFileBuilder; use crate::core::compiler::Freshness; use crate::core::compiler::{CompileKind, DefaultExecutor, Executor}; -use crate::core::{Edition, Package, PackageId, Source, SourceId, Workspace}; +use crate::core::{Dependency, Edition, Package, PackageId, Source, SourceId, Workspace}; use crate::ops::common_for_install_and_uninstall::*; -use crate::sources::{GitSource, SourceConfigMap}; +use crate::sources::{GitSource, PathSource, SourceConfigMap}; use crate::util::errors::{CargoResult, CargoResultExt}; -use crate::util::{paths, Config, Filesystem}; +use crate::util::{paths, Config, Filesystem, Rustc, ToSemver}; use crate::{drop_println, ops}; struct Transaction { @@ -65,7 +66,9 @@ pub fn install( } else { let mut succeeded = vec![]; let mut failed = vec![]; - let mut first = true; + // "Tracks whether or not the source (such as a registry or git repo) has been updated. + // This is used to avoid updating it multiple times when installing multiple crates. + let mut did_update = false; for krate in krates { let root = root.clone(); let map = map.clone(); @@ -80,15 +83,19 @@ pub fn install( opts, force, no_track, - first, + !did_update, ) { - Ok(()) => succeeded.push(krate), + Ok(still_needs_update) => { + succeeded.push(krate); + did_update |= !still_needs_update; + } Err(e) => { crate::display_error(&e, &mut config.shell()); - failed.push(krate) + failed.push(krate); + // We assume an update was performed if we got an error. + did_update = true; } } - first = false; } let mut summary = vec![]; @@ -133,6 +140,11 @@ pub fn install( Ok(()) } +// Returns whether a subsequent call should attempt to update again. +// The `needs_update_if_source_is_index` parameter indicates whether or not the source index should +// be updated. This is used ensure it is only updated once when installing multiple crates. +// The return value here is used so that the caller knows what to pass to the +// `needs_update_if_source_is_index` parameter when `install_one` is called again. fn install_one( config: &Config, root: &Filesystem, @@ -144,8 +156,8 @@ fn install_one( opts: &ops::CompileOptions, force: bool, no_track: bool, - is_first_install: bool, -) -> CargoResult<()> { + needs_update_if_source_is_index: bool, +) -> CargoResult { if let Some(name) = krate { if name == "." { bail!( @@ -155,72 +167,110 @@ fn install_one( ) } } - let pkg = if source_id.is_git() { - select_pkg( - GitSource::new(source_id, config)?, - krate, - vers, - config, - true, - &mut |git| git.read_packages(), - )? - } else if source_id.is_path() { - let mut src = path_source(source_id, config)?; - if !src.path().is_dir() { - bail!( - "`{}` is not a directory. \ - --path must point to a directory containing a Cargo.toml file.", - src.path().display() - ) - } - if !src.path().join("Cargo.toml").exists() { - if from_cwd { - bail!( - "`{}` is not a crate root; specify a crate to \ - install from crates.io, or use --path or --git to \ - specify an alternate source", - src.path().display() - ); + + let dst = root.join("bin").into_path_unlocked(); + + let pkg = { + let dep = { + if let Some(krate) = krate { + let vers = if let Some(vers_flag) = vers { + Some(parse_semver_flag(vers_flag)?.to_string()) + } else { + if source_id.is_registry() { + // Avoid pre-release versions from crate.io + // unless explicitly asked for + Some(String::from("*")) + } else { + None + } + }; + Some(Dependency::parse_no_deprecated( + krate, + vers.as_deref(), + source_id, + )?) } else { + None + } + }; + + if source_id.is_git() { + let mut source = GitSource::new(source_id, config)?; + select_pkg( + &mut source, + dep, + |git: &mut GitSource<'_>| git.read_packages(), + config, + )? + } else if source_id.is_path() { + let mut src = path_source(source_id, config)?; + if !src.path().is_dir() { bail!( - "`{}` does not contain a Cargo.toml file. \ - --path must point to a directory containing a Cargo.toml file.", + "`{}` is not a directory. \ + --path must point to a directory containing a Cargo.toml file.", src.path().display() ) } - } - src.update()?; - select_pkg(src, krate, vers, config, false, &mut |path| { - path.read_packages() - })? - } else { - select_pkg( - map.load(source_id, &HashSet::new())?, - krate, - vers, - config, - is_first_install, - &mut |_| { + if !src.path().join("Cargo.toml").exists() { + if from_cwd { + bail!( + "`{}` is not a crate root; specify a crate to \ + install from crates.io, or use --path or --git to \ + specify an alternate source", + src.path().display() + ); + } else { + bail!( + "`{}` does not contain a Cargo.toml file. \ + --path must point to a directory containing a Cargo.toml file.", + src.path().display() + ) + } + } + select_pkg( + &mut src, + dep, + |path: &mut PathSource<'_>| path.read_packages(), + config, + )? + } else { + if let Some(dep) = dep { + let mut source = map.load(source_id, &HashSet::new())?; + if let Ok(Some(pkg)) = installed_exact_package( + dep.clone(), + &mut source, + config, + opts, + root, + &dst, + force, + ) { + let msg = format!( + "package `{}` is already installed, use --force to override", + pkg + ); + config.shell().status("Ignored", &msg)?; + return Ok(true); + } + select_dep_pkg(&mut source, dep, config, needs_update_if_source_is_index)? + } else { bail!( "must specify a crate to install from \ crates.io, or use --path or --git to \ specify alternate source" ) - }, - )? + } + } }; - let (mut ws, git_package) = if source_id.is_git() { + let (mut ws, rustc, target) = make_ws_rustc_target(config, opts, &source_id, pkg.clone())?; + let pkg = if source_id.is_git() { // Don't use ws.current() in order to keep the package source as a git source so that // install tracking uses the correct source. - (Workspace::new(pkg.manifest_path(), config)?, Some(&pkg)) - } else if source_id.is_path() { - (Workspace::new(pkg.manifest_path(), config)?, None) + pkg } else { - (Workspace::ephemeral(pkg, config, None, false)?, None) + ws.current()?.clone() }; - ws.set_ignore_lock(config.lock_update_allowed()); - ws.set_require_optional_deps(false); let mut td_opt = None; let mut needs_cleanup = false; @@ -238,8 +288,6 @@ fn install_one( ws.set_target_dir(target_dir); } - let pkg = git_package.map_or_else(|| ws.current(), |pkg| Ok(pkg))?; - if from_cwd { if pkg.manifest().edition() == Edition::Edition2015 { config.shell().warn( @@ -265,20 +313,9 @@ fn install_one( bail!("specified package `{}` has no binaries", pkg); } - // Preflight checks to check up front whether we'll overwrite something. - // We have to check this again afterwards, but may as well avoid building - // anything if we're gonna throw it away anyway. - let dst = root.join("bin").into_path_unlocked(); - let rustc = config.load_global_rustc(Some(&ws))?; - let requested_kind = opts.build_config.single_requested_kind()?; - let target = match &requested_kind { - CompileKind::Host => rustc.host.as_str(), - CompileKind::Target(target) => target.short_name(), - }; - // Helper for --no-track flag to make sure it doesn't overwrite anything. let no_track_duplicates = || -> CargoResult>> { - let duplicates: BTreeMap> = exe_names(pkg, &opts.filter) + let duplicates: BTreeMap> = exe_names(&pkg, &opts.filter) .into_iter() .filter(|name| dst.join(name).exists()) .map(|name| (name, None)) @@ -300,22 +337,17 @@ fn install_one( // Check for conflicts. no_track_duplicates()?; } else { - let tracker = InstallTracker::load(config, root)?; - let (freshness, _duplicates) = - tracker.check_upgrade(&dst, pkg, force, opts, target, &rustc.verbose_version)?; - if freshness == Freshness::Fresh { + if is_installed(&pkg, config, opts, &rustc, &target, root, &dst, force)? { let msg = format!( "package `{}` is already installed, use --force to override", pkg ); config.shell().status("Ignored", &msg)?; - return Ok(()); + return Ok(false); } - // Unlock while building. - drop(tracker); } - config.shell().status("Installing", pkg)?; + config.shell().status("Installing", &pkg)?; check_yanked_install(&ws)?; @@ -356,7 +388,7 @@ fn install_one( } else { let tracker = InstallTracker::load(config, root)?; let (_freshness, duplicates) = - tracker.check_upgrade(&dst, pkg, force, opts, target, &rustc.verbose_version)?; + tracker.check_upgrade(&dst, &pkg, force, opts, &target, &rustc.verbose_version)?; (Some(tracker), duplicates) }; @@ -417,15 +449,15 @@ fn install_one( if let Some(mut tracker) = tracker { tracker.mark_installed( - pkg, + &pkg, &successful_bins, vers.map(|s| s.to_string()), opts, - target, + &target, &rustc.verbose_version, ); - if let Err(e) = remove_orphaned_bins(&ws, &mut tracker, &duplicates, pkg, &dst) { + if let Err(e) = remove_orphaned_bins(&ws, &mut tracker, &duplicates, &pkg, &dst) { // Don't hard error on remove. config .shell() @@ -467,7 +499,7 @@ fn install_one( "Installed", format!("package `{}` {}", pkg, executables(successful_bins.iter())), )?; - Ok(()) + Ok(false) } else { if !to_install.is_empty() { config.shell().status( @@ -492,7 +524,128 @@ fn install_one( ), )?; } - Ok(()) + Ok(false) + } +} + +fn is_installed( + pkg: &Package, + config: &Config, + opts: &ops::CompileOptions, + rustc: &Rustc, + target: &str, + root: &Filesystem, + dst: &Path, + force: bool, +) -> CargoResult { + let tracker = InstallTracker::load(config, root)?; + let (freshness, _duplicates) = + tracker.check_upgrade(dst, pkg, force, opts, target, &rustc.verbose_version)?; + Ok(freshness == Freshness::Fresh) +} + +/// Checks if vers can only be satisfied by exactly one version of a package in a registry, and it's +/// already installed. If this is the case, we can skip interacting with a registry to check if +/// newer versions may be installable, as no newer version can exist. +fn installed_exact_package( + dep: Dependency, + source: &mut T, + config: &Config, + opts: &ops::CompileOptions, + root: &Filesystem, + dst: &Path, + force: bool, +) -> CargoResult> +where + T: Source, +{ + if !dep.is_locked() { + // If the version isn't exact, we may need to update the registry and look for a newer + // version - we can't know if the package is installed without doing so. + return Ok(None); + } + // Try getting the package from the registry without updating it, to avoid a potentially + // expensive network call in the case that the package is already installed. + // If this fails, the caller will possibly do an index update and try again, this is just a + // best-effort check to see if we can avoid hitting the network. + if let Ok(pkg) = select_dep_pkg(source, dep, config, false) { + let (_ws, rustc, target) = + make_ws_rustc_target(&config, opts, &source.source_id(), pkg.clone())?; + if let Ok(true) = is_installed(&pkg, config, opts, &rustc, &target, root, &dst, force) { + return Ok(Some(pkg)); + } + } + Ok(None) +} + +fn make_ws_rustc_target<'cfg>( + config: &'cfg Config, + opts: &ops::CompileOptions, + source_id: &SourceId, + pkg: Package, +) -> CargoResult<(Workspace<'cfg>, Rustc, String)> { + let mut ws = if source_id.is_git() || source_id.is_path() { + Workspace::new(pkg.manifest_path(), config)? + } else { + Workspace::ephemeral(pkg, config, None, false)? + }; + ws.set_ignore_lock(config.lock_update_allowed()); + ws.set_require_optional_deps(false); + + let rustc = config.load_global_rustc(Some(&ws))?; + let target = match &opts.build_config.single_requested_kind()? { + CompileKind::Host => rustc.host.as_str().to_owned(), + CompileKind::Target(target) => target.short_name().to_owned(), + }; + + Ok((ws, rustc, target)) +} + +/// Parses x.y.z as if it were =x.y.z, and gives CLI-specific error messages in the case of invalid +/// values. +fn parse_semver_flag(v: &str) -> CargoResult { + // If the version begins with character <, >, =, ^, ~ parse it as a + // version range, otherwise parse it as a specific version + let first = v + .chars() + .next() + .ok_or_else(|| format_err!("no version provided for the `--vers` flag"))?; + + let is_req = "<>=^~".contains(first) || v.contains('*'); + if is_req { + match v.parse::() { + Ok(v) => Ok(v), + Err(_) => bail!( + "the `--vers` provided, `{}`, is \ + not a valid semver version requirement\n\n\ + Please have a look at \ + https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html \ + for the correct format", + v + ), + } + } else { + match v.to_semver() { + Ok(v) => Ok(VersionReq::exact(&v)), + Err(e) => { + let mut msg = format!( + "the `--vers` provided, `{}`, is \ + not a valid semver version: {}\n", + v, e + ); + + // If it is not a valid version but it is a valid version + // requirement, add a note to the warning + if v.parse::().is_ok() { + msg.push_str(&format!( + "\nif you want to specify semver range, \ + add an explicit qualifier, like ^{}", + v + )); + } + bail!(msg); + } + } } } diff --git a/src/cargo/ops/cargo_uninstall.rs b/src/cargo/ops/cargo_uninstall.rs index 2d69eabd023..6f07940c215 100644 --- a/src/cargo/ops/cargo_uninstall.rs +++ b/src/cargo/ops/cargo_uninstall.rs @@ -5,6 +5,7 @@ use std::env; use crate::core::PackageId; use crate::core::{PackageIdSpec, SourceId}; use crate::ops::common_for_install_and_uninstall::*; +use crate::sources::PathSource; use crate::util::errors::CargoResult; use crate::util::paths; use crate::util::Config; @@ -84,10 +85,13 @@ pub fn uninstall_one( fn uninstall_cwd(root: &Filesystem, bins: &[String], config: &Config) -> CargoResult<()> { let tracker = InstallTracker::load(config, root)?; let source_id = SourceId::for_path(config.cwd())?; - let src = path_source(source_id, config)?; - let pkg = select_pkg(src, None, None, config, true, &mut |path| { - path.read_packages() - })?; + let mut src = path_source(source_id, config)?; + let pkg = select_pkg( + &mut src, + None, + |path: &mut PathSource<'_>| path.read_packages(), + config, + )?; let pkgid = pkg.package_id(); uninstall_pkgid(root, tracker, pkgid, bins, config) } diff --git a/src/cargo/ops/common_for_install_and_uninstall.rs b/src/cargo/ops/common_for_install_and_uninstall.rs index 221e1afc38e..78e53af5c4f 100644 --- a/src/cargo/ops/common_for_install_and_uninstall.rs +++ b/src/cargo/ops/common_for_install_and_uninstall.rs @@ -5,7 +5,6 @@ use std::io::SeekFrom; use std::path::{Path, PathBuf}; use anyhow::{bail, format_err}; -use semver::VersionReq; use serde::{Deserialize, Serialize}; use crate::core::compiler::Freshness; @@ -13,7 +12,7 @@ use crate::core::{Dependency, Package, PackageId, Source, SourceId}; use crate::ops::{self, CompileFilter, CompileOptions}; use crate::sources::PathSource; use crate::util::errors::{CargoResult, CargoResultExt}; -use crate::util::{Config, ToSemver}; +use crate::util::Config; use crate::util::{FileLock, Filesystem}; /// On-disk tracking for which package installed which binary. @@ -521,16 +520,14 @@ pub fn path_source(source_id: SourceId, config: &Config) -> CargoResult( - mut source: T, - name: Option<&str>, - vers: Option<&str>, +pub fn select_dep_pkg( + source: &mut T, + dep: Dependency, config: &Config, needs_update: bool, - list_all: &mut dyn FnMut(&mut T) -> CargoResult>, ) -> CargoResult where - T: Source + 'a, + T: Source, { // This operation may involve updating some sources or making a few queries // which may involve frobbing caches, as a result make sure we synchronize @@ -541,83 +538,42 @@ where source.update()?; } - if let Some(name) = name { - let vers = if let Some(v) = vers { - // If the version begins with character <, >, =, ^, ~ parse it as a - // version range, otherwise parse it as a specific version - let first = v - .chars() - .next() - .ok_or_else(|| format_err!("no version provided for the `--vers` flag"))?; - - let is_req = "<>=^~".contains(first) || v.contains('*'); - if is_req { - match v.parse::() { - Ok(v) => Some(v.to_string()), - Err(_) => bail!( - "the `--vers` provided, `{}`, is \ - not a valid semver version requirement\n\n\ - Please have a look at \ - https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html \ - for the correct format", - v - ), - } - } else { - match v.to_semver() { - Ok(v) => Some(format!("={}", v)), - Err(e) => { - let mut msg = format!( - "the `--vers` provided, `{}`, is \ - not a valid semver version: {}\n", - v, e - ); - - // If it is not a valid version but it is a valid version - // requirement, add a note to the warning - if v.parse::().is_ok() { - msg.push_str(&format!( - "\nif you want to specify semver range, \ - add an explicit qualifier, like ^{}", - v - )); - } - bail!(msg); - } - } - } - } else { - None - }; - let vers = vers.as_deref(); - let vers_spec = if vers.is_none() && source.source_id().is_registry() { - // Avoid pre-release versions from crate.io - // unless explicitly asked for - Some("*") - } else { - vers - }; - let dep = Dependency::parse_no_deprecated(name, vers_spec, source.source_id())?; - let deps = source.query_vec(&dep)?; - match deps.iter().map(|p| p.package_id()).max() { - Some(pkgid) => { - let pkg = Box::new(&mut source).download_now(pkgid, config)?; - Ok(pkg) - } - None => { - let vers_info = vers - .map(|v| format!(" with version `{}`", v)) - .unwrap_or_default(); - bail!( - "could not find `{}` in {}{}", - name, - source.source_id(), - vers_info - ) - } + let deps = source.query_vec(&dep)?; + match deps.iter().map(|p| p.package_id()).max() { + Some(pkgid) => { + let pkg = Box::new(source).download_now(pkgid, config)?; + Ok(pkg) } + None => bail!( + "could not find `{}` in {} with version `{}`", + dep.package_name(), + source.source_id(), + dep.version_req(), + ), + } +} + +pub fn select_pkg( + source: &mut T, + dep: Option, + mut list_all: F, + config: &Config, +) -> CargoResult +where + T: Source, + F: FnMut(&mut T) -> CargoResult>, +{ + // This operation may involve updating some sources or making a few queries + // which may involve frobbing caches, as a result make sure we synchronize + // with other global Cargos + let _lock = config.acquire_package_cache_lock()?; + + source.update()?; + + return if let Some(dep) = dep { + select_dep_pkg(source, dep, config, false) } else { - let candidates = list_all(&mut source)?; + let candidates = list_all(source)?; let binaries = candidates .iter() .filter(|cand| cand.targets().iter().filter(|t| t.is_bin()).count() > 0); @@ -630,23 +586,23 @@ where Some(p) => p, None => bail!( "no packages found with binaries or \ - examples" + examples" ), }, }; - return Ok(pkg.clone()); - - fn multi_err(kind: &str, mut pkgs: Vec<&Package>) -> String { - pkgs.sort_unstable_by_key(|a| a.name()); - format!( - "multiple packages with {} found: {}", - kind, - pkgs.iter() - .map(|p| p.name().as_str()) - .collect::>() - .join(", ") - ) - } + Ok(pkg.clone()) + }; + + fn multi_err(kind: &str, mut pkgs: Vec<&Package>) -> String { + pkgs.sort_unstable_by_key(|a| a.name()); + format!( + "multiple packages with {} found: {}", + kind, + pkgs.iter() + .map(|p| p.name().as_str()) + .collect::>() + .join(", ") + ) } } diff --git a/tests/testsuite/install.rs b/tests/testsuite/install.rs index 5ede0bb4870..68bcf22e8b0 100644 --- a/tests/testsuite/install.rs +++ b/tests/testsuite/install.rs @@ -75,7 +75,7 @@ fn multiple_pkgs() { [FINISHED] release [optimized] target(s) in [..] [INSTALLING] [CWD]/home/.cargo/bin/bar[EXE] [INSTALLED] package `bar v0.0.2` (executable `bar[EXE]`) -[ERROR] could not find `baz` in registry `[..]` +[ERROR] could not find `baz` in registry `[..]` with version `*` [SUMMARY] Successfully installed foo, bar! Failed to install baz (see error(s) above). [WARNING] be sure to add `[..]` to your PATH to be able to run the installed binaries [ERROR] some crates failed to install @@ -147,7 +147,7 @@ fn missing() { .with_stderr( "\ [UPDATING] [..] index -[ERROR] could not find `bar` in registry `[..]` +[ERROR] could not find `bar` in registry `[..]` with version `*` ", ) .run(); @@ -175,7 +175,7 @@ fn bad_version() { .with_stderr( "\ [UPDATING] [..] index -[ERROR] could not find `foo` in registry `[..]` with version `=0.2.0` +[ERROR] could not find `foo` in registry `[..]` with version `= 0.2.0` ", ) .run(); diff --git a/tests/testsuite/install_upgrade.rs b/tests/testsuite/install_upgrade.rs index ee69bd85b71..59a904d5854 100644 --- a/tests/testsuite/install_upgrade.rs +++ b/tests/testsuite/install_upgrade.rs @@ -14,9 +14,9 @@ use cargo_test_support::{ basic_manifest, cargo_process, cross_compile, execs, git, process, project, Execs, }; -// Helper for publishing a package. -fn pkg(name: &str, vers: &str) { +fn pkg_maybe_yanked(name: &str, vers: &str, yanked: bool) { Package::new(name, vers) + .yanked(yanked) .file( "src/main.rs", r#"fn main() { println!("{}", env!("CARGO_PKG_VERSION")) }"#, @@ -24,6 +24,11 @@ fn pkg(name: &str, vers: &str) { .publish(); } +// Helper for publishing a package. +fn pkg(name: &str, vers: &str) { + pkg_maybe_yanked(name, vers, false) +} + fn v1_path() -> PathBuf { cargo_home().join(".crates.toml") } @@ -225,7 +230,6 @@ fn ambiguous_version_no_longer_allowed() { cargo_process("install foo --version=1.0") .with_stderr( "\ -[UPDATING] `[..]` index [ERROR] the `--vers` provided, `1.0`, is not a valid semver version: cannot parse '1.0' as a semver if you want to specify semver range, add an explicit qualifier, like ^1.0 @@ -746,3 +750,111 @@ fn deletes_orphaned() { // 0.1.0 should not have any entries. validate_trackers("foo", "0.1.0", &[]); } + +#[cargo_test] +fn already_installed_exact_does_not_update() { + pkg("foo", "1.0.0"); + cargo_process("install foo --version=1.0.0").run(); + cargo_process("install foo --version=1.0.0") + .with_stderr( + "\ +[IGNORED] package `foo v1.0.0` is already installed[..] +[WARNING] be sure to add [..] +", + ) + .run(); + + cargo_process("install foo --version=>=1.0.0") + .with_stderr( + "\ +[UPDATING] `[..]` index +[IGNORED] package `foo v1.0.0` is already installed[..] +[WARNING] be sure to add [..] +", + ) + .run(); + pkg("foo", "1.0.1"); + cargo_process("install foo --version=>=1.0.0") + .with_stderr( + "\ +[UPDATING] `[..]` index +[DOWNLOADING] crates ... +[DOWNLOADED] foo v1.0.1 (registry [..]) +[INSTALLING] foo v1.0.1 +[COMPILING] foo v1.0.1 +[FINISHED] release [optimized] target(s) in [..] +[REPLACING] [CWD]/home/.cargo/bin/foo[EXE] +[REPLACED] package `foo v1.0.0` with `foo v1.0.1` (executable `foo[EXE]`) +[WARNING] be sure to add [..] +", + ) + .run(); +} + +#[cargo_test] +fn already_installed_updates_yank_status_on_upgrade() { + pkg("foo", "1.0.0"); + pkg_maybe_yanked("foo", "1.0.1", true); + cargo_process("install foo --version=1.0.0").run(); + + cargo_process("install foo --version=1.0.1") + .with_status(101) + .with_stderr( + "\ +[UPDATING] `[..]` index +[ERROR] could not find `foo` in registry `[..]` with version `= 1.0.1` +", + ) + .run(); + + pkg_maybe_yanked("foo", "1.0.1", false); + + pkg("foo", "1.0.1"); + cargo_process("install foo --version=1.0.1") + .with_stderr( + "\ +[UPDATING] `[..]` index +[DOWNLOADING] crates ... +[DOWNLOADED] foo v1.0.1 (registry [..]) +[INSTALLING] foo v1.0.1 +[COMPILING] foo v1.0.1 +[FINISHED] release [optimized] target(s) in [..] +[REPLACING] [CWD]/home/.cargo/bin/foo[EXE] +[REPLACED] package `foo v1.0.0` with `foo v1.0.1` (executable `foo[EXE]`) +[WARNING] be sure to add [..] +", + ) + .run(); +} + +#[cargo_test] +fn partially_already_installed_does_one_update() { + pkg("foo", "1.0.0"); + cargo_process("install foo --version=1.0.0").run(); + pkg("bar", "1.0.0"); + pkg("baz", "1.0.0"); + cargo_process("install foo bar baz --version=1.0.0") + .with_stderr( + "\ +[IGNORED] package `foo v1.0.0` is already installed[..] +[UPDATING] `[..]` index +[DOWNLOADING] crates ... +[DOWNLOADED] bar v1.0.0 (registry [..]) +[INSTALLING] bar v1.0.0 +[COMPILING] bar v1.0.0 +[FINISHED] release [optimized] target(s) in [..] +[INSTALLING] [CWD]/home/.cargo/bin/bar[EXE] +[INSTALLED] package `bar v1.0.0` (executable `bar[EXE]`) +[DOWNLOADING] crates ... +[DOWNLOADED] baz v1.0.0 (registry [..]) +[INSTALLING] baz v1.0.0 +[COMPILING] baz v1.0.0 +[FINISHED] release [optimized] target(s) in [..] +[INSTALLING] [CWD]/home/.cargo/bin/baz[EXE] +[INSTALLED] package `baz v1.0.0` (executable `baz[EXE]`) +[SUMMARY] Successfully installed foo, bar, baz! +[WARNING] be sure to add [..] +", + ) + .run(); +}