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 std::{env, fs};
use anyhow::{bail, format_err}; use anyhow::{bail, format_err};
use semver::VersionReq;
use tempfile::Builder as TempFileBuilder; use tempfile::Builder as TempFileBuilder;
use crate::core::compiler::Freshness; use crate::core::compiler::Freshness;
use crate::core::compiler::{CompileKind, DefaultExecutor, Executor}; 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::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::errors::{CargoResult, CargoResultExt};
use crate::util::{paths, Config, Filesystem}; use crate::util::{paths, Config, Filesystem, Rustc, ToSemver};
use crate::{drop_println, ops}; use crate::{drop_println, ops};
struct Transaction { struct Transaction {
@ -65,7 +66,9 @@ pub fn install(
} else { } else {
let mut succeeded = vec![]; let mut succeeded = vec![];
let mut failed = 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 { for krate in krates {
let root = root.clone(); let root = root.clone();
let map = map.clone(); let map = map.clone();
@ -80,15 +83,19 @@ pub fn install(
opts, opts,
force, force,
no_track, no_track,
first, !did_update,
) { ) {
Ok(()) => succeeded.push(krate), Ok(still_needs_update) => {
succeeded.push(krate);
did_update |= !still_needs_update;
}
Err(e) => { Err(e) => {
crate::display_error(&e, &mut config.shell()); 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![]; let mut summary = vec![];
@ -133,6 +140,11 @@ pub fn install(
Ok(()) 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( fn install_one(
config: &Config, config: &Config,
root: &Filesystem, root: &Filesystem,
@ -144,8 +156,8 @@ fn install_one(
opts: &ops::CompileOptions, opts: &ops::CompileOptions,
force: bool, force: bool,
no_track: bool, no_track: bool,
is_first_install: bool, needs_update_if_source_is_index: bool,
) -> CargoResult<()> { ) -> CargoResult<bool> {
if let Some(name) = krate { if let Some(name) = krate {
if name == "." { if name == "." {
bail!( bail!(
@ -155,72 +167,110 @@ fn install_one(
) )
} }
} }
let pkg = if source_id.is_git() {
select_pkg( let dst = root.join("bin").into_path_unlocked();
GitSource::new(source_id, config)?,
krate, let pkg = {
vers, let dep = {
config, if let Some(krate) = krate {
true, let vers = if let Some(vers_flag) = vers {
&mut |git| git.read_packages(), Some(parse_semver_flag(vers_flag)?.to_string())
)? } else {
} else if source_id.is_path() { if source_id.is_registry() {
let mut src = path_source(source_id, config)?; // Avoid pre-release versions from crate.io
if !src.path().is_dir() { // unless explicitly asked for
bail!( Some(String::from("*"))
"`{}` is not a directory. \ } else {
--path must point to a directory containing a Cargo.toml file.", None
src.path().display() }
) };
} Some(Dependency::parse_no_deprecated(
if !src.path().join("Cargo.toml").exists() { krate,
if from_cwd { vers.as_deref(),
bail!( source_id,
"`{}` 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 { } 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!( bail!(
"`{}` does not contain a Cargo.toml file. \ "`{}` is not a directory. \
--path must point to a directory containing a Cargo.toml file.", --path must point to a directory containing a Cargo.toml file.",
src.path().display() src.path().display()
) )
} }
} if !src.path().join("Cargo.toml").exists() {
src.update()?; if from_cwd {
select_pkg(src, krate, vers, config, false, &mut |path| { bail!(
path.read_packages() "`{}` is not a crate root; specify a crate to \
})? install from crates.io, or use --path or --git to \
} else { specify an alternate source",
select_pkg( src.path().display()
map.load(source_id, &HashSet::new())?, );
krate, } else {
vers, bail!(
config, "`{}` does not contain a Cargo.toml file. \
is_first_install, --path must point to a directory containing a Cargo.toml file.",
&mut |_| { 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!( bail!(
"must specify a crate to install from \ "must specify a crate to install from \
crates.io, or use --path or --git to \ crates.io, or use --path or --git to \
specify alternate source" 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 // Don't use ws.current() in order to keep the package source as a git source so that
// install tracking uses the correct source. // install tracking uses the correct source.
(Workspace::new(pkg.manifest_path(), config)?, Some(&pkg)) pkg
} else if source_id.is_path() {
(Workspace::new(pkg.manifest_path(), config)?, None)
} else { } 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 td_opt = None;
let mut needs_cleanup = false; let mut needs_cleanup = false;
@ -238,8 +288,6 @@ fn install_one(
ws.set_target_dir(target_dir); ws.set_target_dir(target_dir);
} }
let pkg = git_package.map_or_else(|| ws.current(), |pkg| Ok(pkg))?;
if from_cwd { if from_cwd {
if pkg.manifest().edition() == Edition::Edition2015 { if pkg.manifest().edition() == Edition::Edition2015 {
config.shell().warn( config.shell().warn(
@ -265,20 +313,9 @@ fn install_one(
bail!("specified package `{}` has no binaries", pkg); 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. // Helper for --no-track flag to make sure it doesn't overwrite anything.
let no_track_duplicates = || -> CargoResult<BTreeMap<String, Option<PackageId>>> { 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() .into_iter()
.filter(|name| dst.join(name).exists()) .filter(|name| dst.join(name).exists())
.map(|name| (name, None)) .map(|name| (name, None))
@ -300,22 +337,17 @@ fn install_one(
// Check for conflicts. // Check for conflicts.
no_track_duplicates()?; no_track_duplicates()?;
} else { } else {
let tracker = InstallTracker::load(config, root)?; if is_installed(&pkg, config, opts, &rustc, &target, root, &dst, force)? {
let (freshness, _duplicates) =
tracker.check_upgrade(&dst, pkg, force, opts, target, &rustc.verbose_version)?;
if freshness == Freshness::Fresh {
let msg = format!( let msg = format!(
"package `{}` is already installed, use --force to override", "package `{}` is already installed, use --force to override",
pkg pkg
); );
config.shell().status("Ignored", &msg)?; 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)?; check_yanked_install(&ws)?;
@ -356,7 +388,7 @@ fn install_one(
} else { } else {
let tracker = InstallTracker::load(config, root)?; let tracker = InstallTracker::load(config, root)?;
let (_freshness, duplicates) = 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) (Some(tracker), duplicates)
}; };
@ -417,15 +449,15 @@ fn install_one(
if let Some(mut tracker) = tracker { if let Some(mut tracker) = tracker {
tracker.mark_installed( tracker.mark_installed(
pkg, &pkg,
&successful_bins, &successful_bins,
vers.map(|s| s.to_string()), vers.map(|s| s.to_string()),
opts, opts,
target, &target,
&rustc.verbose_version, &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. // Don't hard error on remove.
config config
.shell() .shell()
@ -467,7 +499,7 @@ fn install_one(
"Installed", "Installed",
format!("package `{}` {}", pkg, executables(successful_bins.iter())), format!("package `{}` {}", pkg, executables(successful_bins.iter())),
)?; )?;
Ok(()) Ok(false)
} else { } else {
if !to_install.is_empty() { if !to_install.is_empty() {
config.shell().status( 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::PackageId;
use crate::core::{PackageIdSpec, SourceId}; use crate::core::{PackageIdSpec, SourceId};
use crate::ops::common_for_install_and_uninstall::*; use crate::ops::common_for_install_and_uninstall::*;
use crate::sources::PathSource;
use crate::util::errors::CargoResult; use crate::util::errors::CargoResult;
use crate::util::paths; use crate::util::paths;
use crate::util::Config; use crate::util::Config;
@ -84,10 +85,13 @@ pub fn uninstall_one(
fn uninstall_cwd(root: &Filesystem, bins: &[String], config: &Config) -> CargoResult<()> { fn uninstall_cwd(root: &Filesystem, bins: &[String], config: &Config) -> CargoResult<()> {
let tracker = InstallTracker::load(config, root)?; let tracker = InstallTracker::load(config, root)?;
let source_id = SourceId::for_path(config.cwd())?; let source_id = SourceId::for_path(config.cwd())?;
let src = path_source(source_id, config)?; let mut src = path_source(source_id, config)?;
let pkg = select_pkg(src, None, None, config, true, &mut |path| { let pkg = select_pkg(
path.read_packages() &mut src,
})?; None,
|path: &mut PathSource<'_>| path.read_packages(),
config,
)?;
let pkgid = pkg.package_id(); let pkgid = pkg.package_id();
uninstall_pkgid(root, tracker, pkgid, bins, config) uninstall_pkgid(root, tracker, pkgid, bins, config)
} }

View File

@ -5,7 +5,6 @@ use std::io::SeekFrom;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use anyhow::{bail, format_err}; use anyhow::{bail, format_err};
use semver::VersionReq;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::core::compiler::Freshness; 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::ops::{self, CompileFilter, CompileOptions};
use crate::sources::PathSource; use crate::sources::PathSource;
use crate::util::errors::{CargoResult, CargoResultExt}; use crate::util::errors::{CargoResult, CargoResultExt};
use crate::util::{Config, ToSemver}; use crate::util::Config;
use crate::util::{FileLock, Filesystem}; use crate::util::{FileLock, Filesystem};
/// On-disk tracking for which package installed which binary. /// 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. /// Gets a Package based on command-line requirements.
pub fn select_pkg<'a, T>( pub fn select_dep_pkg<T>(
mut source: T, source: &mut T,
name: Option<&str>, dep: Dependency,
vers: Option<&str>,
config: &Config, config: &Config,
needs_update: bool, needs_update: bool,
list_all: &mut dyn FnMut(&mut T) -> CargoResult<Vec<Package>>,
) -> CargoResult<Package> ) -> CargoResult<Package>
where where
T: Source + 'a, T: Source,
{ {
// This operation may involve updating some sources or making a few queries // This operation may involve updating some sources or making a few queries
// which may involve frobbing caches, as a result make sure we synchronize // which may involve frobbing caches, as a result make sure we synchronize
@ -541,83 +538,42 @@ where
source.update()?; source.update()?;
} }
if let Some(name) = name { let deps = source.query_vec(&dep)?;
let vers = if let Some(v) = vers { match deps.iter().map(|p| p.package_id()).max() {
// If the version begins with character <, >, =, ^, ~ parse it as a Some(pkgid) => {
// version range, otherwise parse it as a specific version let pkg = Box::new(source).download_now(pkgid, config)?;
let first = v Ok(pkg)
.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
)
}
} }
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 { } else {
let candidates = list_all(&mut source)?; let candidates = list_all(source)?;
let binaries = candidates let binaries = candidates
.iter() .iter()
.filter(|cand| cand.targets().iter().filter(|t| t.is_bin()).count() > 0); .filter(|cand| cand.targets().iter().filter(|t| t.is_bin()).count() > 0);
@ -630,23 +586,23 @@ where
Some(p) => p, Some(p) => p,
None => bail!( None => bail!(
"no packages found with binaries or \ "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 { fn multi_err(kind: &str, mut pkgs: Vec<&Package>) -> String {
pkgs.sort_unstable_by_key(|a| a.name()); pkgs.sort_unstable_by_key(|a| a.name());
format!( format!(
"multiple packages with {} found: {}", "multiple packages with {} found: {}",
kind, kind,
pkgs.iter() pkgs.iter()
.map(|p| p.name().as_str()) .map(|p| p.name().as_str())
.collect::<Vec<_>>() .collect::<Vec<_>>()
.join(", ") .join(", ")
) )
}
} }
} }

View File

@ -75,7 +75,7 @@ fn multiple_pkgs() {
[FINISHED] release [optimized] target(s) in [..] [FINISHED] release [optimized] target(s) in [..]
[INSTALLING] [CWD]/home/.cargo/bin/bar[EXE] [INSTALLING] [CWD]/home/.cargo/bin/bar[EXE]
[INSTALLED] package `bar v0.0.2` (executable `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). [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 [WARNING] be sure to add `[..]` to your PATH to be able to run the installed binaries
[ERROR] some crates failed to install [ERROR] some crates failed to install
@ -147,7 +147,7 @@ fn missing() {
.with_stderr( .with_stderr(
"\ "\
[UPDATING] [..] index [UPDATING] [..] index
[ERROR] could not find `bar` in registry `[..]` [ERROR] could not find `bar` in registry `[..]` with version `*`
", ",
) )
.run(); .run();
@ -175,7 +175,7 @@ fn bad_version() {
.with_stderr( .with_stderr(
"\ "\
[UPDATING] [..] index [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(); .run();

View File

@ -14,9 +14,9 @@ use cargo_test_support::{
basic_manifest, cargo_process, cross_compile, execs, git, process, project, Execs, basic_manifest, cargo_process, cross_compile, execs, git, process, project, Execs,
}; };
// Helper for publishing a package. fn pkg_maybe_yanked(name: &str, vers: &str, yanked: bool) {
fn pkg(name: &str, vers: &str) {
Package::new(name, vers) Package::new(name, vers)
.yanked(yanked)
.file( .file(
"src/main.rs", "src/main.rs",
r#"fn main() { println!("{}", env!("CARGO_PKG_VERSION")) }"#, r#"fn main() { println!("{}", env!("CARGO_PKG_VERSION")) }"#,
@ -24,6 +24,11 @@ fn pkg(name: &str, vers: &str) {
.publish(); .publish();
} }
// Helper for publishing a package.
fn pkg(name: &str, vers: &str) {
pkg_maybe_yanked(name, vers, false)
}
fn v1_path() -> PathBuf { fn v1_path() -> PathBuf {
cargo_home().join(".crates.toml") cargo_home().join(".crates.toml")
} }
@ -225,7 +230,6 @@ fn ambiguous_version_no_longer_allowed() {
cargo_process("install foo --version=1.0") cargo_process("install foo --version=1.0")
.with_stderr( .with_stderr(
"\ "\
[UPDATING] `[..]` index
[ERROR] the `--vers` provided, `1.0`, is not a valid semver version: cannot parse '1.0' as a semver [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 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. // 0.1.0 should not have any entries.
validate_trackers("foo", "0.1.0", &[]); 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();
}