Auto merge of #8022 - illicitonion:trywithout, r=ehuss

Try installing exact versions before updating

When an exact version is being installed, if we already have that
version from the index, we don't need to update the index before
installing it. Don't do this if it's not an exact version, because the
update may find us a newer version.

This is particularly useful for scripts which unconditionally run
`cargo install some-crate --version=1.2.3`. Before install-update, I
wrote a crate to do this
(https://crates.io/crates/cargo-ensure-installed) which I'm trying to
replace with just `cargo install`, but the extra latency of updating the
index for a no-op is noticeable.
This commit is contained in:
bors 2020-05-20 20:48:44 +00:00
commit 9b9451325b
5 changed files with 419 additions and 194 deletions

View File

@ -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<bool> {
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<BTreeMap<String, Option<PackageId>>> {
let duplicates: BTreeMap<String, Option<PackageId>> = exe_names(pkg, &opts.filter)
let duplicates: BTreeMap<String, Option<PackageId>> = 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<bool> {
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<T>(
dep: Dependency,
source: &mut T,
config: &Config,
opts: &ops::CompileOptions,
root: &Filesystem,
dst: &Path,
force: bool,
) -> CargoResult<Option<Package>>
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<VersionReq> {
// 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::<VersionReq>() {
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::<VersionReq>().is_ok() {
msg.push_str(&format!(
"\nif you want to specify semver range, \
add an explicit qualifier, like ^{}",
v
));
}
bail!(msg);
}
}
}
}

View File

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

View File

@ -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<PathSour
}
/// Gets a Package based on command-line requirements.
pub fn select_pkg<'a, T>(
mut source: T,
name: Option<&str>,
vers: Option<&str>,
pub fn select_dep_pkg<T>(
source: &mut T,
dep: Dependency,
config: &Config,
needs_update: bool,
list_all: &mut dyn FnMut(&mut T) -> CargoResult<Vec<Package>>,
) -> CargoResult<Package>
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::<VersionReq>() {
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::<VersionReq>().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<T, F>(
source: &mut T,
dep: Option<Dependency>,
mut list_all: F,
config: &Config,
) -> CargoResult<Package>
where
T: Source,
F: FnMut(&mut T) -> CargoResult<Vec<Package>>,
{
// 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());
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::<Vec<_>>()
.join(", ")
)
}
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::<Vec<_>>()
.join(", ")
)
}
}

View File

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

View File

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