Add install-upgrade.

This commit is contained in:
Eric Huss 2019-03-30 17:33:35 -07:00
parent 6bdb9d3b11
commit e023a6672b
12 changed files with 1656 additions and 418 deletions

View File

@ -35,6 +35,7 @@ Available unstable (nightly-only) flags:
-Z offline -- Offline mode that does not perform network requests
-Z unstable-options -- Allow the usage of unstable options such as --registry
-Z config-profile -- Read profiles from .cargo/config files
-Z install-upgrade -- `cargo install` will upgrade instead of failing
Run with 'cargo -Z [FLAG] [SUBCOMMAND]'"
);

View File

@ -46,6 +46,10 @@ pub fn cli() -> App {
))
.arg_jobs()
.arg(opt("force", "Force overwriting existing crates or binaries").short("f"))
.arg(opt(
"no-track",
"Do not save tracking information (unstable)",
))
.arg_features()
.arg(opt("debug", "Build in debug mode instead of release mode"))
.arg_targets_bins_examples(
@ -148,6 +152,12 @@ pub fn exec(config: &mut Config, args: &ArgMatches<'_>) -> CliResult {
let version = args.value_of("version");
let root = args.value_of("root");
if args.is_present("no-track") && !config.cli_unstable().install_upgrade {
Err(failure::format_err!(
"`--no-track` flag is unstable, pass `-Z install-upgrade` to enable it"
))?;
};
if args.is_present("list") {
ops::install_list(root, config)?;
} else {
@ -159,6 +169,7 @@ pub fn exec(config: &mut Config, args: &ArgMatches<'_>) -> CliResult {
version,
&compile_opts,
args.is_present("force"),
args.is_present("no-track"),
)?;
}
Ok(())

View File

@ -328,6 +328,7 @@ pub struct CliUnstable {
pub config_profile: bool,
pub dual_proc_macros: bool,
pub mtime_on_use: bool,
pub install_upgrade: bool,
}
impl CliUnstable {
@ -372,6 +373,7 @@ impl CliUnstable {
"config-profile" => self.config_profile = true,
"dual-proc-macros" => self.dual_proc_macros = true,
"mtime-on-use" => self.mtime_on_use = true,
"install-upgrade" => self.install_upgrade = true,
_ => failure::bail!("unknown `-Z` flag specified: {}", k),
}

View File

@ -3,18 +3,16 @@ use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::{env, fs};
use failure::{bail, format_err};
use tempfile::Builder as TempFileBuilder;
use crate::core::compiler::{DefaultExecutor, Executor};
use crate::core::{Edition, Package, Source, SourceId};
use crate::core::{PackageId, Workspace};
use crate::core::{Edition, PackageId, Source, SourceId, Workspace};
use crate::ops;
use crate::ops::common_for_install_and_uninstall::*;
use crate::ops::{self, CompileFilter};
use crate::sources::{GitSource, SourceConfigMap};
use crate::util::errors::{CargoResult, CargoResultExt};
use crate::util::paths;
use crate::util::Config;
use crate::util::Filesystem;
use crate::util::{paths, Config, Filesystem, Freshness};
struct Transaction {
bins: Vec<PathBuf>,
@ -42,6 +40,7 @@ pub fn install(
vers: Option<&str>,
opts: &ops::CompileOptions<'_>,
force: bool,
no_track: bool,
) -> CargoResult<()> {
let root = resolve_root(root, opts.config)?;
let map = SourceConfigMap::new(opts.config)?;
@ -56,6 +55,7 @@ pub fn install(
vers,
opts,
force,
no_track,
true,
)?;
(true, false)
@ -75,6 +75,7 @@ pub fn install(
vers,
opts,
force,
no_track,
first,
) {
Ok(()) => succeeded.push(krate),
@ -106,7 +107,7 @@ pub fn install(
if installed_anything {
// Print a warning that if this directory isn't in PATH that they won't be
// able to run these commands.
let dst = metadata(opts.config, &root)?.parent().join("bin");
let dst = root.join("bin").into_path_unlocked();
let path = env::var_os("PATH").unwrap_or_default();
for path in env::split_paths(&path) {
if path == dst {
@ -122,7 +123,7 @@ pub fn install(
}
if scheduled_error {
failure::bail!("some crates failed to install");
bail!("some crates failed to install");
}
Ok(())
@ -137,6 +138,7 @@ fn install_one(
vers: Option<&str>,
opts: &ops::CompileOptions<'_>,
force: bool,
no_track: bool,
is_first_install: bool,
) -> CargoResult<()> {
let config = opts.config;
@ -153,7 +155,7 @@ fn install_one(
} else if source_id.is_path() {
let mut src = path_source(source_id, config)?;
if !src.path().is_dir() {
failure::bail!(
bail!(
"`{}` is not a directory. \
--path must point to a directory containing a Cargo.toml file.",
src.path().display()
@ -161,14 +163,14 @@ fn install_one(
}
if !src.path().join("Cargo.toml").exists() {
if from_cwd {
failure::bail!(
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 {
failure::bail!(
bail!(
"`{}` does not contain a Cargo.toml file. \
--path must point to a directory containing a Cargo.toml file.",
src.path().display()
@ -187,7 +189,7 @@ fn install_one(
config,
is_first_install,
&mut |_| {
failure::bail!(
bail!(
"must specify a crate to install from \
crates.io, or use --path or --git to \
specify alternate source"
@ -230,7 +232,7 @@ fn install_one(
Use `cargo build` if you want to simply build the package.",
)?
} else {
failure::bail!(
bail!(
"Using `cargo install` to install the binaries for the \
package in current working directory is no longer supported, \
use `cargo install --path .` instead. \
@ -239,18 +241,61 @@ fn install_one(
}
};
config.shell().status("Installing", pkg)?;
if !opts.filter.is_specific() && !pkg.targets().iter().any(|t| t.is_bin()) {
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 metadata = metadata(config, root)?;
let list = read_crate_list(&metadata)?;
let dst = metadata.parent().join("bin");
check_overwrites(&dst, pkg, &opts.filter, &list, force)?;
let dst = root.join("bin").into_path_unlocked();
let rustc = config.load_global_rustc(Some(&ws))?;
let target = opts
.build_config
.requested_target
.as_ref()
.unwrap_or(&rustc.host)
.clone();
// 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)
.into_iter()
.filter(|name| dst.join(name).exists())
.map(|name| (name, None))
.collect();
if !force && !duplicates.is_empty() {
let mut msg: Vec<String> = duplicates
.iter()
.map(|(name, _)| format!("binary `{}` already exists in destination", name))
.collect();
msg.push("Add --force to overwrite".to_string());
bail!("{}", msg.join("\n"));
}
Ok(duplicates)
};
if no_track {
// 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 {
let msg = format!(
"package `{}` is already installed, use --force to override",
pkg
);
config.shell().status("Ignored", &msg)?;
return Ok(());
}
// Unlock while building.
drop(tracker);
}
config.shell().status("Installing", pkg)?;
let exec: Arc<dyn Executor> = Arc::new(DefaultExecutor);
let compile = ops::compile_ws(&ws, Some(source), opts, &exec).chain_err(|| {
if let Some(td) = td_opt.take() {
@ -258,14 +303,14 @@ fn install_one(
td.into_path();
}
failure::format_err!(
format_err!(
"failed to compile `{}`, intermediate artifacts can be \
found at `{}`",
pkg,
ws.target_dir().display()
)
})?;
let binaries: Vec<(&str, &Path)> = compile
let mut binaries: Vec<(&str, &Path)> = compile
.binaries
.iter()
.map(|bin| {
@ -273,21 +318,24 @@ fn install_one(
if let Some(s) = name.to_str() {
Ok((s, bin.as_ref()))
} else {
failure::bail!("Binary `{:?}` name can't be serialized into string", name)
bail!("Binary `{:?}` name can't be serialized into string", name)
}
})
.collect::<CargoResult<_>>()?;
if binaries.is_empty() {
failure::bail!(
"no binaries are available for install using the selected \
features"
);
bail!("no binaries are available for install using the selected features");
}
// This is primarily to make testing easier.
binaries.sort_unstable();
let metadata = metadata(config, root)?;
let mut list = read_crate_list(&metadata)?;
let dst = metadata.parent().join("bin");
let duplicates = check_overwrites(&dst, pkg, &opts.filter, &list, force)?;
let (tracker, duplicates) = if no_track {
(None, no_track_duplicates()?)
} else {
let tracker = InstallTracker::load(config, root)?;
let (_freshness, duplicates) =
tracker.check_upgrade(&dst, pkg, force, opts, &target, &rustc.verbose_version)?;
(Some(tracker), duplicates)
};
fs::create_dir_all(&dst)?;
@ -304,7 +352,7 @@ fn install_one(
continue;
}
fs::copy(src, &dst).chain_err(|| {
failure::format_err!("failed to copy `{}` to `{}`", src.display(), dst.display())
format_err!("failed to copy `{}` to `{}`", src.display(), dst.display())
})?;
}
@ -314,6 +362,7 @@ fn install_one(
.partition(|&bin| duplicates.contains_key(bin));
let mut installed = Transaction { bins: Vec::new() };
let mut successful_bins = BTreeSet::new();
// Move the temporary copies into `dst` starting with new binaries.
for bin in to_install.iter() {
@ -321,76 +370,45 @@ fn install_one(
let dst = dst.join(bin);
config.shell().status("Installing", dst.display())?;
fs::rename(&src, &dst).chain_err(|| {
failure::format_err!("failed to move `{}` to `{}`", src.display(), dst.display())
format_err!("failed to move `{}` to `{}`", src.display(), dst.display())
})?;
installed.bins.push(dst);
successful_bins.insert(bin.to_string());
}
// Repeat for binaries which replace existing ones but don't pop the error
// up until after updating metadata.
let mut replaced_names = Vec::new();
let result = {
let replace_result = {
let mut try_install = || -> CargoResult<()> {
for &bin in to_replace.iter() {
let src = staging_dir.path().join(bin);
let dst = dst.join(bin);
config.shell().status("Replacing", dst.display())?;
fs::rename(&src, &dst).chain_err(|| {
failure::format_err!(
"failed to move `{}` to `{}`",
src.display(),
dst.display()
)
format_err!("failed to move `{}` to `{}`", src.display(), dst.display())
})?;
replaced_names.push(bin);
successful_bins.insert(bin.to_string());
}
Ok(())
};
try_install()
};
// Update records of replaced binaries.
for &bin in replaced_names.iter() {
if let Some(&Some(ref p)) = duplicates.get(bin) {
if let Some(set) = list.v1_mut().get_mut(p) {
set.remove(bin);
}
if !no_track {
let mut tracker = tracker.unwrap();
tracker.mark_installed(
pkg,
&successful_bins,
vers.map(|s| s.to_string()),
opts,
target,
rustc.verbose_version,
);
match tracker.save() {
Err(err) => replace_result.chain_err(|| err)?,
Ok(_) => replace_result?,
}
// Failsafe to force replacing metadata for git packages
// https://github.com/rust-lang/cargo/issues/4582
if let Some(set) = list.v1_mut().remove(&pkg.package_id()) {
list.v1_mut().insert(pkg.package_id(), set);
}
list.v1_mut()
.entry(pkg.package_id())
.or_insert_with(BTreeSet::new)
.insert(bin.to_string());
}
// Remove empty metadata lines.
let pkgs = list
.v1()
.iter()
.filter_map(|(&p, set)| if set.is_empty() { Some(p) } else { None })
.collect::<Vec<_>>();
for p in pkgs.iter() {
list.v1_mut().remove(p);
}
// If installation was successful record newly installed binaries.
if result.is_ok() {
list.v1_mut()
.entry(pkg.package_id())
.or_insert_with(BTreeSet::new)
.extend(to_install.iter().map(|s| s.to_string()));
}
let write_result = write_crate_list(&metadata, list);
match write_result {
// Replacement error (if any) isn't actually caused by write error
// but this seems to be the only way to show both.
Err(err) => result.chain_err(|| err)?,
Ok(_) => result?,
}
// Reaching here means all actions have succeeded. Clean up.
@ -402,98 +420,63 @@ fn install_one(
paths::remove_dir_all(&target_dir)?;
}
Ok(())
}
fn check_overwrites(
dst: &Path,
pkg: &Package,
filter: &ops::CompileFilter,
prev: &CrateListingV1,
force: bool,
) -> CargoResult<BTreeMap<String, Option<PackageId>>> {
// If explicit --bin or --example flags were passed then those'll
// get checked during cargo_compile, we only care about the "build
// everything" case here
if !filter.is_specific() && !pkg.targets().iter().any(|t| t.is_bin()) {
failure::bail!("specified package has no binaries")
}
let duplicates = find_duplicates(dst, pkg, filter, prev);
if force || duplicates.is_empty() {
return Ok(duplicates);
}
// Format the error message.
let mut msg = String::new();
for (bin, p) in duplicates.iter() {
msg.push_str(&format!("binary `{}` already exists in destination", bin));
if let Some(p) = p.as_ref() {
msg.push_str(&format!(" as part of `{}`\n", p));
// Helper for creating status messages.
fn executables<T: AsRef<str>>(mut names: impl Iterator<Item = T> + Clone) -> String {
if names.clone().count() == 1 {
format!("(executable `{}`)", names.next().unwrap().as_ref())
} else {
msg.push_str("\n");
format!(
"(executables {})",
names
.map(|b| format!("`{}`", b.as_ref()))
.collect::<Vec<_>>()
.join(", ")
)
}
}
msg.push_str("Add --force to overwrite");
Err(failure::format_err!("{}", msg))
}
fn find_duplicates(
dst: &Path,
pkg: &Package,
filter: &ops::CompileFilter,
prev: &CrateListingV1,
) -> BTreeMap<String, Option<PackageId>> {
let check = |name: String| {
// Need to provide type, works around Rust Issue #93349
let name = format!("{}{}", name, env::consts::EXE_SUFFIX);
if fs::metadata(dst.join(&name)).is_err() {
None
} else if let Some((&p, _)) = prev.v1().iter().find(|&(_, v)| v.contains(&name)) {
Some((name, Some(p)))
} else {
Some((name, None))
if duplicates.is_empty() {
config.shell().status(
"Installed",
format!("package `{}` {}", pkg, executables(successful_bins.iter())),
)?;
Ok(())
} else {
if !to_install.is_empty() {
config.shell().status(
"Installed",
format!("package `{}` {}", pkg, executables(to_install.iter())),
)?;
}
};
match *filter {
CompileFilter::Default { .. } => pkg
.targets()
.iter()
.filter(|t| t.is_bin())
.filter_map(|t| check(t.name().to_string()))
.collect(),
CompileFilter::Only {
ref bins,
ref examples,
..
} => {
let all_bins: Vec<String> = bins.try_collect().unwrap_or_else(|| {
pkg.targets()
.iter()
.filter(|t| t.is_bin())
.map(|t| t.name().to_string())
.collect()
});
let all_examples: Vec<String> = examples.try_collect().unwrap_or_else(|| {
pkg.targets()
.iter()
.filter(|t| t.is_exe_example())
.map(|t| t.name().to_string())
.collect()
});
all_bins
.iter()
.chain(all_examples.iter())
.filter_map(|t| check(t.clone()))
.collect::<BTreeMap<String, Option<PackageId>>>()
// Invert the duplicate map.
let mut pkg_map = BTreeMap::new();
for (bin_name, opt_pkg_id) in &duplicates {
let key = opt_pkg_id.map_or_else(|| "unknown".to_string(), |pkg_id| pkg_id.to_string());
pkg_map
.entry(key)
.or_insert_with(|| Vec::new())
.push(bin_name);
}
for (pkg_descr, bin_names) in &pkg_map {
config.shell().status(
"Replaced",
format!(
"package `{}` with `{}` {}",
pkg_descr,
pkg,
executables(bin_names.iter())
),
)?;
}
Ok(())
}
}
/// Display a list of installed binaries.
pub fn install_list(dst: Option<&str>, config: &Config) -> CargoResult<()> {
let dst = resolve_root(dst, config)?;
let dst = metadata(config, &dst)?;
let list = read_crate_list(&dst)?;
for (k, v) in list.v1().iter() {
let root = resolve_root(dst, config)?;
let tracker = InstallTracker::load(config, &root)?;
for (k, v) in tracker.all_installed_bins() {
println!("{}:", k);
for bin in v {
println!(" {}", bin);

View File

@ -1,5 +1,6 @@
use std::collections::btree_map::Entry;
use std::{env, fs};
use failure::bail;
use std::collections::BTreeSet;
use std::env;
use crate::core::PackageId;
use crate::core::{PackageIdSpec, SourceId};
@ -7,7 +8,7 @@ use crate::ops::common_for_install_and_uninstall::*;
use crate::util::errors::CargoResult;
use crate::util::paths;
use crate::util::Config;
use crate::util::{FileLock, Filesystem};
use crate::util::Filesystem;
pub fn uninstall(
root: Option<&str>,
@ -16,7 +17,7 @@ pub fn uninstall(
config: &Config,
) -> CargoResult<()> {
if specs.len() > 1 && !bins.is_empty() {
failure::bail!("A binary can only be associated with a single installed package, specifying multiple specs with --bin is redundant.");
bail!("A binary can only be associated with a single installed package, specifying multiple specs with --bin is redundant.");
}
let root = resolve_root(root, config)?;
@ -62,7 +63,7 @@ pub fn uninstall(
};
if scheduled_error {
failure::bail!("some packages failed to uninstall");
bail!("some packages failed to uninstall");
}
Ok(())
@ -74,80 +75,74 @@ pub fn uninstall_one(
bins: &[String],
config: &Config,
) -> CargoResult<()> {
let crate_metadata = metadata(config, root)?;
let metadata = read_crate_list(&crate_metadata)?;
let pkgid = PackageIdSpec::query_str(spec, metadata.v1().keys().cloned())?;
uninstall_pkgid(&crate_metadata, metadata, pkgid, bins, config)
let tracker = InstallTracker::load(config, root)?;
let all_pkgs = tracker.all_installed_bins().map(|(pkg_id, _set)| *pkg_id);
let pkgid = PackageIdSpec::query_str(spec, all_pkgs)?;
uninstall_pkgid(root, tracker, pkgid, bins, config)
}
fn uninstall_cwd(root: &Filesystem, bins: &[String], config: &Config) -> CargoResult<()> {
let crate_metadata = metadata(config, root)?;
let metadata = read_crate_list(&crate_metadata)?;
let tracker = InstallTracker::load(config, root)?;
let source_id = SourceId::for_path(config.cwd())?;
let src = path_source(source_id, config)?;
let (pkg, _source) = select_pkg(src, None, None, config, true, &mut |path| {
path.read_packages()
})?;
let pkgid = pkg.package_id();
uninstall_pkgid(&crate_metadata, metadata, pkgid, bins, config)
uninstall_pkgid(root, tracker, pkgid, bins, config)
}
fn uninstall_pkgid(
crate_metadata: &FileLock,
mut metadata: CrateListingV1,
root: &Filesystem,
mut tracker: InstallTracker,
pkgid: PackageId,
bins: &[String],
config: &Config,
) -> CargoResult<()> {
let mut to_remove = Vec::new();
{
let mut installed = match metadata.v1_mut().entry(pkgid) {
Entry::Occupied(e) => e,
Entry::Vacant(..) => failure::bail!("package `{}` is not installed", pkgid),
};
let installed = match tracker.installed_bins(pkgid) {
Some(bins) => bins.clone(),
None => bail!("package `{}` is not installed", pkgid),
};
let dst = crate_metadata.parent().join("bin");
for bin in installed.get() {
let bin = dst.join(bin);
if fs::metadata(&bin).is_err() {
failure::bail!(
"corrupt metadata, `{}` does not exist when it should",
bin.display()
)
}
}
let bins = bins
.iter()
.map(|s| {
if s.ends_with(env::consts::EXE_SUFFIX) {
s.to_string()
} else {
format!("{}{}", s, env::consts::EXE_SUFFIX)
}
})
.collect::<Vec<_>>();
for bin in bins.iter() {
if !installed.get().contains(bin) {
failure::bail!("binary `{}` not installed as part of `{}`", bin, pkgid)
}
}
if bins.is_empty() {
to_remove.extend(installed.get().iter().map(|b| dst.join(b)));
installed.get_mut().clear();
} else {
for bin in bins.iter() {
to_remove.push(dst.join(bin));
installed.get_mut().remove(bin);
}
}
if installed.get().is_empty() {
installed.remove();
let dst = root.join("bin").into_path_unlocked();
for bin in &installed {
let bin = dst.join(bin);
if !bin.exists() {
bail!(
"corrupt metadata, `{}` does not exist when it should",
bin.display()
)
}
}
write_crate_list(crate_metadata, metadata)?;
let bins = bins
.iter()
.map(|s| {
if s.ends_with(env::consts::EXE_SUFFIX) {
s.to_string()
} else {
format!("{}{}", s, env::consts::EXE_SUFFIX)
}
})
.collect::<BTreeSet<_>>();
for bin in bins.iter() {
if !installed.contains(bin) {
bail!("binary `{}` not installed as part of `{}`", bin, pkgid)
}
}
if bins.is_empty() {
to_remove.extend(installed.iter().map(|b| dst.join(b)));
tracker.remove(pkgid, &installed);
} else {
for bin in bins.iter() {
to_remove.push(dst.join(bin));
}
tracker.remove(pkgid, &bins);
}
tracker.save()?;
for bin in to_remove {
config.shell().status("Removing", bin.display())?;
paths::remove_file(bin)?;

View File

@ -1,45 +1,481 @@
use std::collections::{BTreeMap, BTreeSet};
use std::collections::{btree_map, BTreeMap, BTreeSet};
use std::env;
use std::io::prelude::*;
use std::io::SeekFrom;
use std::path::{Path, PathBuf};
use failure::{bail, format_err};
use semver::VersionReq;
use serde::{Deserialize, Serialize};
use crate::core::PackageId;
use crate::core::{Dependency, Package, Source, SourceId};
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::{internal, Config, ToSemver};
use crate::util::{FileLock, Filesystem};
use crate::util::{Config, ToSemver};
use crate::util::{FileLock, Filesystem, Freshness};
#[derive(Deserialize, Serialize)]
#[serde(untagged)]
pub enum CrateListing {
V1(CrateListingV1),
Empty(Empty),
/// On-disk tracking for which package installed which binary.
///
/// v1 is an older style, v2 is a new (experimental) style that tracks more
/// information. The new style is only enabled with the `-Z install-upgrade`
/// flag (which sets the `unstable_upgrade` flag). v1 is still considered the
/// source of truth. When v2 is used, it will sync with any changes with v1,
/// and will continue to update v1.
///
/// This maintains a filesystem lock, preventing other instances of Cargo from
/// modifying at the same time. Drop the value to unlock.
pub struct InstallTracker {
v1: CrateListingV1,
v2: CrateListingV2,
v1_lock: FileLock,
v2_lock: Option<FileLock>,
unstable_upgrade: bool,
}
#[derive(Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
pub struct Empty {}
#[derive(Default, Deserialize, Serialize)]
struct CrateListingV2 {
installs: BTreeMap<PackageId, InstallInfo>,
/// Forwards compatibility.
#[serde(flatten)]
other: BTreeMap<String, serde_json::Value>,
}
#[derive(Deserialize, Serialize)]
#[derive(Debug, Deserialize, Serialize)]
struct InstallInfo {
/// Version requested via `--version`.
/// None if `--version` not specified. Currently not used, possibly may be
/// used in the future.
version_req: Option<String>,
/// Set of binary names installed.
bins: BTreeSet<String>,
/// Set of features explicitly enabled.
features: BTreeSet<String>,
all_features: bool,
no_default_features: bool,
/// Either "debug" or "release".
profile: String,
/// The installation target.
/// Either the host or the value specified in `--target`.
/// None if unknown (when loading from v1).
target: Option<String>,
/// Output of `rustc -V`.
/// None if unknown (when loading from v1).
/// Currently not used, possibly may be used in the future.
rustc: Option<String>,
/// Forwards compatibility.
#[serde(flatten)]
other: BTreeMap<String, serde_json::Value>,
}
#[derive(Default, Deserialize, Serialize)]
pub struct CrateListingV1 {
v1: BTreeMap<PackageId, BTreeSet<String>>,
}
impl CrateListingV1 {
pub fn v1(&self) -> &BTreeMap<PackageId, BTreeSet<String>> {
&self.v1
impl InstallTracker {
/// Create an InstallTracker from information on disk.
pub fn load(config: &Config, root: &Filesystem) -> CargoResult<InstallTracker> {
let unstable_upgrade = config.cli_unstable().install_upgrade;
let v1_lock = root.open_rw(Path::new(".crates.toml"), config, "crate metadata")?;
let v2_lock = if unstable_upgrade {
Some(root.open_rw(Path::new(".crates2.json"), config, "crate metadata")?)
} else {
None
};
let v1 = (|| -> CargoResult<_> {
let mut contents = String::new();
v1_lock.file().read_to_string(&mut contents)?;
if contents.is_empty() {
Ok(CrateListingV1::default())
} else {
Ok(toml::from_str(&contents)
.chain_err(|| format_err!("invalid TOML found for metadata"))?)
}
})()
.chain_err(|| {
format_err!(
"failed to parse crate metadata at `{}`",
v1_lock.path().to_string_lossy()
)
})?;
let v2 = (|| -> CargoResult<_> {
if unstable_upgrade {
let mut contents = String::new();
v2_lock
.as_ref()
.unwrap()
.file()
.read_to_string(&mut contents)?;
let mut v2 = if contents.is_empty() {
CrateListingV2::default()
} else {
serde_json::from_str(&contents)
.chain_err(|| format_err!("invalid JSON found for metadata"))?
};
v2.sync_v1(&v1)?;
Ok(v2)
} else {
Ok(CrateListingV2::default())
}
})()
.chain_err(|| {
format_err!(
"failed to parse crate metadata at `{}`",
v2_lock.as_ref().unwrap().path().to_string_lossy()
)
})?;
Ok(InstallTracker {
v1,
v2,
v1_lock,
v2_lock,
unstable_upgrade,
})
}
pub fn v1_mut(&mut self) -> &mut BTreeMap<PackageId, BTreeSet<String>> {
&mut self.v1
/// Checks if the given package should be built, and checks if executables
/// already exist in the destination directory.
///
/// Returns a tuple `(freshness, map)`. `freshness` indicates if the
/// package should be built (`Dirty`) or if it is already up-to-date
/// (`Fresh`). The map maps binary names to the PackageId that installed
/// it (which is None if not known).
///
/// Returns an error if there is a duplicate and `--force` is not used.
pub fn check_upgrade(
&self,
dst: &Path,
pkg: &Package,
force: bool,
opts: &CompileOptions<'_>,
target: &str,
_rustc: &str,
) -> CargoResult<(Freshness, BTreeMap<String, Option<PackageId>>)> {
let exes = exe_names(pkg, &opts.filter);
let duplicates = self.find_duplicates(dst, &exes);
if force || duplicates.is_empty() {
return Ok((Freshness::Dirty, duplicates));
}
// If any duplicates are not tracked, then --force is required.
// If any duplicates are from a package with a different name, --force is required.
let matching_duplicates: Vec<PackageId> = duplicates
.values()
.filter_map(|v| match v {
Some(dupe_pkg_id) if dupe_pkg_id.name() == pkg.name() => Some(*dupe_pkg_id),
_ => None,
})
.collect();
if self.unstable_upgrade && matching_duplicates.len() == duplicates.len() {
let source_id = pkg.package_id().source_id();
if source_id.is_path() {
return Ok((Freshness::Dirty, duplicates));
}
if matching_duplicates.iter().all(|dupe_pkg_id| {
let info = self
.v2
.installs
.get(dupe_pkg_id)
.expect("dupes must be in sync");
let precise_equal = if source_id.is_git() {
dupe_pkg_id.source_id().precise() == source_id.precise()
} else {
true
};
dupe_pkg_id.version() == pkg.version()
&& dupe_pkg_id.source_id() == source_id
&& precise_equal
&& info.features == feature_set(&opts.features)
&& info.all_features == opts.all_features
&& info.no_default_features == opts.no_default_features
&& info.profile == profile_name(opts.build_config.release)
&& (info.target.is_none()
|| info.target.as_ref().map(|t| t.as_ref()) == Some(target))
&& info.bins == exes
}) {
Ok((Freshness::Fresh, duplicates))
} else {
Ok((Freshness::Dirty, duplicates))
}
} else {
// Format the error message.
let mut msg = String::new();
for (bin, p) in duplicates.iter() {
msg.push_str(&format!("binary `{}` already exists in destination", bin));
if let Some(p) = p.as_ref() {
msg.push_str(&format!(" as part of `{}`\n", p));
} else {
msg.push_str("\n");
}
}
msg.push_str("Add --force to overwrite");
bail!("{}", msg);
}
}
fn find_duplicates(
&self,
dst: &Path,
exes: &BTreeSet<String>,
) -> BTreeMap<String, Option<PackageId>> {
exes.iter()
.filter_map(|name| {
if !dst.join(&name).exists() {
None
} else if self.unstable_upgrade {
let p = self.v2.package_for_bin(name);
Some((name.clone(), p))
} else {
let p = self.v1.package_for_bin(name);
Some((name.clone(), p))
}
})
.collect()
}
/// Mark that a package was installed.
pub fn mark_installed(
&mut self,
package: &Package,
bins: &BTreeSet<String>,
version_req: Option<String>,
opts: &CompileOptions<'_>,
target: String,
rustc: String,
) {
if self.unstable_upgrade {
self.v2
.mark_installed(package, bins, version_req, opts, target, rustc)
}
self.v1.mark_installed(package, bins);
}
/// Save tracking information to disk.
pub fn save(&self) -> CargoResult<()> {
self.v1.save(&self.v1_lock).chain_err(|| {
format_err!(
"failed to write crate metadata at `{}`",
self.v1_lock.path().to_string_lossy()
)
})?;
if self.unstable_upgrade {
self.v2.save(self.v2_lock.as_ref().unwrap()).chain_err(|| {
format_err!(
"failed to write crate metadata at `{}`",
self.v2_lock.as_ref().unwrap().path().to_string_lossy()
)
})?;
}
Ok(())
}
/// Iterator of all installed binaries.
/// Items are `(pkg_id, bins)` where `bins` is the set of binaries that
/// package installed.
pub fn all_installed_bins(&self) -> impl Iterator<Item = (&PackageId, &BTreeSet<String>)> {
self.v1.v1.iter()
}
/// Set of binaries installed by a particular package.
/// Returns None if the package is not installed.
pub fn installed_bins(&self, pkg_id: PackageId) -> Option<&BTreeSet<String>> {
self.v1.v1.get(&pkg_id)
}
/// Remove a package from the tracker.
pub fn remove(&mut self, pkg_id: PackageId, bins: &BTreeSet<String>) {
self.v1.remove(pkg_id, bins);
if self.unstable_upgrade {
self.v2.remove(pkg_id, bins);
}
}
}
impl CrateListingV1 {
fn package_for_bin(&self, bin_name: &str) -> Option<PackageId> {
self.v1
.iter()
.find(|(_, bins)| bins.contains(bin_name))
.map(|(pkg_id, _)| *pkg_id)
}
fn mark_installed(&mut self, pkg: &Package, bins: &BTreeSet<String>) {
// Remove bins from any other packages.
for other_bins in self.v1.values_mut() {
for bin in bins {
other_bins.remove(bin);
}
}
// Remove empty metadata lines. If only BTreeMap had `retain`.
let to_remove = self
.v1
.iter()
.filter_map(|(&p, set)| if set.is_empty() { Some(p) } else { None })
.collect::<Vec<_>>();
for p in to_remove.iter() {
self.v1.remove(p);
}
// Add these bins.
let mut bins = bins.clone();
self.v1
.entry(pkg.package_id())
.and_modify(|set| set.append(&mut bins))
.or_insert(bins);
}
fn remove(&mut self, pkg_id: PackageId, bins: &BTreeSet<String>) {
let mut installed = match self.v1.entry(pkg_id) {
btree_map::Entry::Occupied(e) => e,
btree_map::Entry::Vacant(..) => panic!("v1 unexpected missing `{}`", pkg_id),
};
for bin in bins {
installed.get_mut().remove(bin);
}
if installed.get().is_empty() {
installed.remove();
}
}
fn save(&self, lock: &FileLock) -> CargoResult<()> {
let mut file = lock.file();
file.seek(SeekFrom::Start(0))?;
file.set_len(0)?;
let data = toml::to_string(self)?;
file.write_all(data.as_bytes())?;
Ok(())
}
}
impl CrateListingV2 {
fn sync_v1(&mut self, v1: &CrateListingV1) -> CargoResult<()> {
// Make the `bins` entries the same.
for (pkg_id, bins) in &v1.v1 {
self.installs
.entry(*pkg_id)
.and_modify(|info| info.bins = bins.clone())
.or_insert_with(|| InstallInfo::from_v1(bins));
}
// Remove any packages that aren't present in v1.
let to_remove: Vec<_> = self
.installs
.keys()
.filter(|pkg_id| !v1.v1.contains_key(pkg_id))
.cloned()
.collect();
for pkg_id in to_remove {
self.installs.remove(&pkg_id);
}
Ok(())
}
fn package_for_bin(&self, bin_name: &str) -> Option<PackageId> {
self.installs
.iter()
.find(|(_, info)| info.bins.contains(bin_name))
.map(|(pkg_id, _)| *pkg_id)
}
fn mark_installed(
&mut self,
pkg: &Package,
bins: &BTreeSet<String>,
version_req: Option<String>,
opts: &CompileOptions<'_>,
target: String,
rustc: String,
) {
// Remove bins from any other packages.
for info in &mut self.installs.values_mut() {
for bin in bins {
info.bins.remove(bin);
}
}
// Remove empty metadata lines. If only BTreeMap had `retain`.
let to_remove = self
.installs
.iter()
.filter_map(|(&p, info)| if info.bins.is_empty() { Some(p) } else { None })
.collect::<Vec<_>>();
for p in to_remove.iter() {
self.installs.remove(p);
}
// Add these bins.
if let Some(info) = self.installs.get_mut(&pkg.package_id()) {
for bin in bins {
info.bins.remove(bin);
}
info.version_req = version_req;
info.features = feature_set(&opts.features);
info.all_features = opts.all_features;
info.no_default_features = opts.no_default_features;
info.profile = profile_name(opts.build_config.release).to_string();
info.target = Some(target);
info.rustc = Some(rustc);
} else {
self.installs.insert(
pkg.package_id(),
InstallInfo {
version_req,
bins: bins.clone(),
features: feature_set(&opts.features),
all_features: opts.all_features,
no_default_features: opts.no_default_features,
profile: profile_name(opts.build_config.release).to_string(),
target: Some(target),
rustc: Some(rustc),
other: BTreeMap::new(),
},
);
}
}
fn remove(&mut self, pkg_id: PackageId, bins: &BTreeSet<String>) {
let mut info_entry = match self.installs.entry(pkg_id) {
btree_map::Entry::Occupied(e) => e,
btree_map::Entry::Vacant(..) => panic!("v2 unexpected missing `{}`", pkg_id),
};
for bin in bins {
info_entry.get_mut().bins.remove(bin);
}
if info_entry.get().bins.is_empty() {
info_entry.remove();
}
}
fn save(&self, lock: &FileLock) -> CargoResult<()> {
let mut file = lock.file();
file.seek(SeekFrom::Start(0))?;
file.set_len(0)?;
serde_json::to_writer(file, self)?;
Ok(())
}
}
impl InstallInfo {
fn from_v1(set: &BTreeSet<String>) -> InstallInfo {
InstallInfo {
version_req: None,
bins: set.clone(),
features: BTreeSet::new(),
all_features: false,
no_default_features: false,
profile: "release".to_string(),
target: None,
rustc: None,
other: BTreeMap::new(),
}
}
}
/// Determines the root directory where installation is done.
pub fn resolve_root(flag: Option<&str>, config: &Config) -> CargoResult<Filesystem> {
let config_root = config.get_path("install.root")?;
Ok(flag
@ -50,14 +486,16 @@ pub fn resolve_root(flag: Option<&str>, config: &Config) -> CargoResult<Filesyst
.unwrap_or_else(|| config.home().clone()))
}
/// Determines the `PathSource` from a `SourceId`.
pub fn path_source<'a>(source_id: SourceId, config: &'a Config) -> CargoResult<PathSource<'a>> {
let path = source_id
.url()
.to_file_path()
.map_err(|()| failure::format_err!("path sources must have a valid path"))?;
.map_err(|()| format_err!("path sources must have a valid path"))?;
Ok(PathSource::new(&path, source_id, config))
}
/// Gets a Package based on command-line requirements.
pub fn select_pkg<'a, T>(
mut source: T,
name: Option<&str>,
@ -73,124 +511,137 @@ where
source.update()?;
}
match name {
Some(name) => {
let vers = match vers {
Some(v) => {
// If the version begins with character <, >, =, ^, ~ parse it as a
// version range, otherwise parse it as a specific version
let first = v.chars().nth(0).ok_or_else(|| {
failure::format_err!("no version provided for the `--vers` flag")
})?;
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()
.nth(0)
.ok_or_else(|| format_err!("no version provided for the `--vers` flag"))?;
match first {
'<' | '>' | '=' | '^' | '~' => match v.parse::<VersionReq>() {
Ok(v) => Some(v.to_string()),
Err(_) => failure::bail!(
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 = if config.cli_unstable().install_upgrade {
format!(
"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",
not a valid semver version: {}\n",
v, e
)
} else {
format!(
"the `--vers` provided, `{}`, is \
not a valid semver version\n\n\
historically Cargo treated this \
as a semver version requirement \
accidentally\nand will continue \
to do so, but this behavior \
will be removed eventually",
v
),
},
_ => match v.to_semver() {
Ok(v) => Some(format!("={}", v)),
Err(_) => {
let mut msg = format!(
"\
the `--vers` provided, `{}`, is \
not a valid semver version\n\n\
historically Cargo treated this \
as a semver version requirement \
accidentally\nand will continue \
to do so, but this behavior \
will be removed eventually",
v
);
)
};
// 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
));
}
config.shell().warn(&msg)?;
Some(v.to_string())
}
},
// 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
));
}
if config.cli_unstable().install_upgrade {
bail!(msg);
} else {
config.shell().warn(&msg)?;
}
Some(v.to_string())
}
}
None => None,
};
let vers = vers.as_ref().map(|s| &**s);
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, Box::new(source)))
},
None => {
let vers_info = vers
.map(|v| format!(" with version `{}`", v))
.unwrap_or_default();
failure::bail!(
"could not find `{}` in {}{}",
name,
source.source_id(),
vers_info
)
}
}
}
None => {
let candidates = list_all(&mut source)?;
let binaries = candidates
.iter()
.filter(|cand| cand.targets().iter().filter(|t| t.is_bin()).count() > 0);
let examples = candidates
.iter()
.filter(|cand| cand.targets().iter().filter(|t| t.is_example()).count() > 0);
let pkg = match one(binaries, |v| multi_err("binaries", v))? {
Some(p) => p,
None => match one(examples, |v| multi_err("examples", v))? {
Some(p) => p,
None => failure::bail!(
"no packages found with binaries or \
examples"
),
},
};
return Ok((pkg.clone(), Box::new(source)));
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(", ")
} else {
None
};
let vers = vers.as_ref().map(|s| &**s);
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, Box::new(source)))
}
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
)
}
}
} else {
let candidates = list_all(&mut source)?;
let binaries = candidates
.iter()
.filter(|cand| cand.targets().iter().filter(|t| t.is_bin()).count() > 0);
let examples = candidates
.iter()
.filter(|cand| cand.targets().iter().filter(|t| t.is_example()).count() > 0);
let pkg = match one(binaries, |v| multi_err("binaries", v))? {
Some(p) => p,
None => match one(examples, |v| multi_err("examples", v))? {
Some(p) => p,
None => bail!(
"no packages found with binaries or \
examples"
),
},
};
return Ok((pkg.clone(), Box::new(source)));
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(", ")
)
}
}
}
pub fn one<I, F>(mut i: I, f: F) -> CargoResult<Option<I::Item>>
/// Get one element from the iterator.
/// Returns None if none left.
/// Returns error if there is more than one item in the iterator.
fn one<I, F>(mut i: I, f: F) -> CargoResult<Option<I::Item>>
where
I: Iterator,
F: FnOnce(Vec<I::Item>) -> String,
@ -199,53 +650,61 @@ where
(Some(i1), Some(i2)) => {
let mut v = vec![i1, i2];
v.extend(i);
Err(failure::format_err!("{}", f(v)))
Err(format_err!("{}", f(v)))
}
(Some(i), None) => Ok(Some(i)),
(None, _) => Ok(None),
}
}
pub fn read_crate_list(file: &FileLock) -> CargoResult<CrateListingV1> {
let listing = (|| -> CargoResult<_> {
let mut contents = String::new();
file.file().read_to_string(&mut contents)?;
let listing =
toml::from_str(&contents).chain_err(|| internal("invalid TOML found for metadata"))?;
match listing {
CrateListing::V1(v1) => Ok(v1),
CrateListing::Empty(_) => Ok(CrateListingV1 {
v1: BTreeMap::new(),
}),
fn profile_name(release: bool) -> &'static str {
if release {
"release"
} else {
"dev"
}
}
/// Helper to convert features Vec to a BTreeSet.
fn feature_set(features: &[String]) -> BTreeSet<String> {
features.iter().cloned().collect()
}
/// Helper to get the executable names from a filter.
pub fn exe_names(pkg: &Package, filter: &ops::CompileFilter) -> BTreeSet<String> {
let to_exe = |name| format!("{}{}", name, env::consts::EXE_SUFFIX);
match filter {
CompileFilter::Default { .. } => pkg
.targets()
.iter()
.filter(|t| t.is_bin())
.map(|t| to_exe(t.name()))
.collect(),
CompileFilter::Only {
ref bins,
ref examples,
..
} => {
let all_bins: Vec<String> = bins.try_collect().unwrap_or_else(|| {
pkg.targets()
.iter()
.filter(|t| t.is_bin())
.map(|t| t.name().to_string())
.collect()
});
let all_examples: Vec<String> = examples.try_collect().unwrap_or_else(|| {
pkg.targets()
.iter()
.filter(|t| t.is_exe_example())
.map(|t| t.name().to_string())
.collect()
});
all_bins
.iter()
.chain(all_examples.iter())
.map(|name| to_exe(name))
.collect()
}
})()
.chain_err(|| {
failure::format_err!(
"failed to parse crate metadata at `{}`",
file.path().to_string_lossy()
)
})?;
Ok(listing)
}
pub fn write_crate_list(file: &FileLock, listing: CrateListingV1) -> CargoResult<()> {
(|| -> CargoResult<_> {
let mut file = file.file();
file.seek(SeekFrom::Start(0))?;
file.set_len(0)?;
let data = toml::to_string(&CrateListing::V1(listing))?;
file.write_all(data.as_bytes())?;
Ok(())
})()
.chain_err(|| {
failure::format_err!(
"failed to write crate metadata at `{}`",
file.path().to_string_lossy()
)
})?;
Ok(())
}
pub fn metadata(config: &Config, root: &Filesystem) -> CargoResult<FileLock> {
root.open_rw(Path::new(".crates.toml"), config, "crate metadata")
}
}

View File

@ -232,3 +232,32 @@ extra-info = "qwerty"
Metabuild packages should have a public function called `metabuild` that
performs the same actions as a regular `build.rs` script would perform.
### install-upgrade
* Tracking Issue: [#6797](https://github.com/rust-lang/cargo/issues/6797)
The `install-upgrade` feature changes the behavior of `cargo install` so that
it will reinstall a package if it is not "up-to-date". If it is "up-to-date",
it will do nothing and exit with success instead of failing. Example:
```
cargo +nightly install foo -Z install-upgrade
```
Cargo tracks some information to determine if a package is "up-to-date",
including:
- The package version and source.
- The set of binary names installed.
- The chosen features.
- The release mode (`--debug`).
- The target (`--target`).
If any of these values change, then Cargo will reinstall the package.
Installation will still fail if a different package installs a binary of the
same name. `--force` may be used to unconditionally reinstall the package.
Additionally, a new flag `--no-track` is available to prevent `cargo install`
from writing tracking information in `$CARGO_HOME` about which packages are
installed.

View File

@ -141,12 +141,14 @@ fn simple_install() {
cargo_process("install bar")
.with_stderr(
" Installing bar v0.1.0
Compiling foo v0.0.1
Compiling bar v0.1.0
Finished release [optimized] target(s) in [..]s
Installing [..]bar[..]
warning: be sure to add `[..]` to your PATH to be able to run the installed binaries
"\
[INSTALLING] bar v0.1.0
[COMPILING] foo v0.0.1
[COMPILING] bar v0.1.0
[FINISHED] release [optimized] target(s) in [..]s
[INSTALLING] [..]bar[..]
[INSTALLED] package `bar v0.1.0` (executable `bar[EXE]`)
[WARNING] be sure to add `[..]` to your PATH to be able to run the installed binaries
",
)
.run();
@ -229,12 +231,14 @@ fn install_without_feature_dep() {
cargo_process("install bar")
.with_stderr(
" Installing bar v0.1.0
Compiling foo v0.0.1
Compiling bar v0.1.0
Finished release [optimized] target(s) in [..]s
Installing [..]bar[..]
warning: be sure to add `[..]` to your PATH to be able to run the installed binaries
"\
[INSTALLING] bar v0.1.0
[COMPILING] foo v0.0.1
[COMPILING] bar v0.1.0
[FINISHED] release [optimized] target(s) in [..]s
[INSTALLING] [..]bar[..]
[INSTALLED] package `bar v0.1.0` (executable `bar[EXE]`)
[WARNING] be sure to add `[..]` to your PATH to be able to run the installed binaries
",
)
.run();

View File

@ -35,7 +35,8 @@ fn simple() {
[COMPILING] foo v0.0.1
[FINISHED] release [optimized] target(s) in [..]
[INSTALLING] [CWD]/home/.cargo/bin/foo[EXE]
warning: be sure to add `[..]` to your PATH to be able to run the installed binaries
[INSTALLED] package `foo v0.0.1` (executable `foo[EXE]`)
[WARNING] be sure to add `[..]` to your PATH to be able to run the installed binaries
",
)
.run();
@ -63,16 +64,18 @@ fn multiple_pkgs() {
[COMPILING] foo v0.0.1
[FINISHED] release [optimized] target(s) in [..]
[INSTALLING] [CWD]/home/.cargo/bin/foo[EXE]
[INSTALLED] package `foo v0.0.1` (executable `foo[EXE]`)
[DOWNLOADING] crates ...
[DOWNLOADED] bar v0.0.2 (registry `[CWD]/registry`)
[INSTALLING] bar v0.0.2
[COMPILING] bar v0.0.2
[FINISHED] release [optimized] target(s) in [..]
[INSTALLING] [CWD]/home/.cargo/bin/bar[EXE]
error: could not find `baz` in registry `[..]`
[INSTALLED] package `bar v0.0.2` (executable `bar[EXE]`)
[ERROR] could not find `baz` in registry `[..]`
[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
[WARNING] be sure to add `[..]` to your PATH to be able to run the installed binaries
[ERROR] some crates failed to install
",
)
.run();
@ -111,7 +114,8 @@ fn pick_max_version() {
[COMPILING] foo v0.2.1
[FINISHED] release [optimized] target(s) in [..]
[INSTALLING] [CWD]/home/.cargo/bin/foo[EXE]
warning: be sure to add `[..]` to your PATH to be able to run the installed binaries
[INSTALLED] package `foo v0.2.1` (executable `foo[EXE]`)
[WARNING] be sure to add `[..]` to your PATH to be able to run the installed binaries
",
)
.run();
@ -251,7 +255,6 @@ fn install_path() {
.with_status(101)
.with_stderr(
"\
[INSTALLING] foo v0.0.1 [..]
[ERROR] binary `foo[..]` already exists in destination as part of `foo v0.0.1 [..]`
Add --force to overwrite
",
@ -427,8 +430,7 @@ fn no_binaries() {
.with_status(101)
.with_stderr(
"\
[INSTALLING] foo [..]
[ERROR] specified package has no binaries
[ERROR] specified package `foo v0.0.1 ([..])` has no binaries
",
)
.run();
@ -461,7 +463,6 @@ fn install_twice() {
.with_status(101)
.with_stderr(
"\
[INSTALLING] foo v0.0.1 [..]
[ERROR] binary `foo-bin1[..]` already exists in destination as part of `foo v0.0.1 ([..])`
binary `foo-bin2[..]` already exists in destination as part of `foo v0.0.1 ([..])`
Add --force to overwrite
@ -490,7 +491,8 @@ fn install_force() {
[COMPILING] foo v0.2.0 ([..])
[FINISHED] release [optimized] target(s) in [..]
[REPLACING] [CWD]/home/.cargo/bin/foo[EXE]
warning: be sure to add `[..]` to your PATH to be able to run the installed binaries
[REPLACED] package `foo v0.0.1 ([..]/foo)` with `foo v0.2.0 ([..]/foo2)` (executable `foo[EXE]`)
[WARNING] be sure to add `[..]` to your PATH to be able to run the installed binaries
",
)
.run();
@ -530,7 +532,9 @@ fn install_force_partial_overlap() {
[FINISHED] release [optimized] target(s) in [..]
[INSTALLING] [CWD]/home/.cargo/bin/foo-bin3[EXE]
[REPLACING] [CWD]/home/.cargo/bin/foo-bin2[EXE]
warning: be sure to add `[..]` to your PATH to be able to run the installed binaries
[INSTALLED] package `foo v0.2.0 ([..]/foo2)` (executable `foo-bin3[EXE]`)
[REPLACED] package `foo v0.0.1 ([..]/foo)` with `foo v0.2.0 ([..]/foo2)` (executable `foo-bin2[EXE]`)
[WARNING] be sure to add `[..]` to your PATH to be able to run the installed binaries
",
)
.run();
@ -572,7 +576,8 @@ fn install_force_bin() {
[COMPILING] foo v0.2.0 ([..])
[FINISHED] release [optimized] target(s) in [..]
[REPLACING] [CWD]/home/.cargo/bin/foo-bin2[EXE]
warning: be sure to add `[..]` to your PATH to be able to run the installed binaries
[REPLACED] package `foo v0.0.1 ([..]/foo)` with `foo v0.2.0 ([..]/foo2)` (executable `foo-bin2[EXE]`)
[WARNING] be sure to add `[..]` to your PATH to be able to run the installed binaries
",
)
.run();
@ -627,7 +632,8 @@ fn git_repo() {
[COMPILING] foo v0.1.0 ([..])
[FINISHED] release [optimized] target(s) in [..]
[INSTALLING] [CWD]/home/.cargo/bin/foo[EXE]
warning: be sure to add `[..]` to your PATH to be able to run the installed binaries
[INSTALLED] package `foo v0.1.0 ([..]/foo#[..])` (executable `foo[EXE]`)
[WARNING] be sure to add `[..]` to your PATH to be able to run the installed binaries
",
)
.run();
@ -809,7 +815,8 @@ fn uninstall_cwd() {
[COMPILING] foo v0.0.1 ([CWD])
[FINISHED] release [optimized] target(s) in [..]
[INSTALLING] {home}/bin/foo[EXE]
warning: be sure to add `{home}/bin` to your PATH to be able to run the installed binaries",
[INSTALLED] package `foo v0.0.1 ([..]/foo)` (executable `foo[EXE]`)
[WARNING] be sure to add `{home}/bin` to your PATH to be able to run the installed binaries",
home = cargo_home().display(),
))
.run();
@ -868,10 +875,12 @@ fn do_not_rebuilds_on_local_install() {
cargo_process("install --path")
.arg(p.root())
.with_stderr(
"[INSTALLING] [..]
"\
[INSTALLING] [..]
[FINISHED] release [optimized] target(s) in [..]
[INSTALLING] [..]
warning: be sure to add `[..]` to your PATH to be able to run the installed binaries
[INSTALLED] package `foo v0.0.1 ([..]/foo)` (executable `foo[EXE]`)
[WARNING] be sure to add `[..]` to your PATH to be able to run the installed binaries
",
)
.run();
@ -1313,7 +1322,8 @@ fn workspace_uses_workspace_target_dir() {
"[INSTALLING] [..]
[FINISHED] release [optimized] target(s) in [..]
[INSTALLING] [..]
warning: be sure to add `[..]` to your PATH to be able to run the installed binaries
[INSTALLED] package `bar v0.1.0 ([..]/bar)` (executable `bar[EXE]`)
[WARNING] be sure to add `[..]` to your PATH to be able to run the installed binaries
",
)
.run();
@ -1376,3 +1386,25 @@ fn install_path_config() {
.with_stderr_contains("[..]--target nonexistent[..]")
.run();
}
#[test]
fn install_version_req() {
// Try using a few versionreq styles.
pkg("foo", "0.0.3");
pkg("foo", "1.0.4");
pkg("foo", "1.0.5");
cargo_process("install foo --version=*")
.with_stderr_does_not_contain("[WARNING][..]is not a valid semver[..]")
.with_stderr_contains("[INSTALLING] foo v1.0.5")
.run();
cargo_process("uninstall foo").run();
cargo_process("install foo --version=^1.0")
.with_stderr_does_not_contain("[WARNING][..]is not a valid semver[..]")
.with_stderr_contains("[INSTALLING] foo v1.0.5")
.run();
cargo_process("uninstall foo").run();
cargo_process("install foo --version=0.0.*")
.with_stderr_does_not_contain("[WARNING][..]is not a valid semver[..]")
.with_stderr_contains("[INSTALLING] foo v0.0.3")
.run();
}

View File

@ -0,0 +1,718 @@
use std::fs;
use std::path::PathBuf;
use std::sync::atomic::{AtomicUsize, Ordering};
use crate::support::install::{cargo_home, exe};
use crate::support::paths::CargoPathExt;
use crate::support::registry::Package;
use crate::support::{
basic_manifest, cargo_process, cross_compile, execs, git, process, project, Execs,
};
// Helper for publishing a package.
fn pkg(name: &str, vers: &str) {
Package::new(name, vers)
.file(
"src/main.rs",
r#"fn main() { println!("{}", env!("CARGO_PKG_VERSION")) }"#,
)
.publish();
}
fn v1_path() -> PathBuf {
cargo_home().join(".crates.toml")
}
fn v2_path() -> PathBuf {
cargo_home().join(".crates2.json")
}
fn load_crates2() -> serde_json::Value {
serde_json::from_str(&fs::read_to_string(v2_path()).unwrap()).unwrap()
}
fn installed_exe(name: &str) -> PathBuf {
cargo_home().join("bin").join(exe(name))
}
/// Helper for executing binaries installed by cargo.
fn installed_process(name: &str) -> Execs {
static NEXT_ID: AtomicUsize = AtomicUsize::new(0);
thread_local!(static UNIQUE_ID: usize = NEXT_ID.fetch_add(1, Ordering::SeqCst));
// This copies the executable to a unique name so that it may be safely
// replaced on Windows. See Project::rename_run for details.
let src = installed_exe(name);
let dst = installed_exe(&UNIQUE_ID.with(|my_id| format!("{}-{}", name, my_id)));
// Note: Cannot use copy. On Linux, file descriptors may be left open to
// the executable as other tests in other threads are constantly spawning
// new processes (see https://github.com/rust-lang/cargo/pull/5557 for
// more).
fs::rename(&src, &dst)
.unwrap_or_else(|e| panic!("Failed to rename `{:?}` to `{:?}`: {}", src, dst, e));
// Leave behind a fake file so that reinstall duplicate check works.
fs::write(src, "").unwrap();
let p = process(dst);
execs().with_process_builder(p)
}
#[test]
fn registry_upgrade() {
// Installing and upgrading from a registry.
pkg("foo", "1.0.0");
cargo_process("install foo -Z install-upgrade")
.masquerade_as_nightly_cargo()
.with_stderr(
"\
[UPDATING] `[..]` index
[DOWNLOADING] crates ...
[DOWNLOADED] foo v1.0.0 (registry [..])
[INSTALLING] foo v1.0.0
[COMPILING] foo v1.0.0
[FINISHED] release [optimized] target(s) in [..]
[INSTALLING] [CWD]/home/.cargo/bin/foo[EXE]
[INSTALLED] package `foo v1.0.0` (executable `foo[EXE]`)
[WARNING] be sure to add [..]
",
)
.run();
installed_process("foo").with_stdout("1.0.0").run();
cargo_process("install foo -Z install-upgrade")
.masquerade_as_nightly_cargo()
.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 -Z install-upgrade")
.masquerade_as_nightly_cargo()
.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();
installed_process("foo").with_stdout("1.0.1").run();
cargo_process("install foo --version=1.0.0 -Z install-upgrade")
.masquerade_as_nightly_cargo()
.with_stderr_contains("[COMPILING] foo v1.0.0")
.run();
installed_process("foo").with_stdout("1.0.0").run();
cargo_process("install foo --version=^1.0 -Z install-upgrade")
.masquerade_as_nightly_cargo()
.with_stderr_contains("[COMPILING] foo v1.0.1")
.run();
installed_process("foo").with_stdout("1.0.1").run();
cargo_process("install foo --version=^1.0 -Z install-upgrade")
.masquerade_as_nightly_cargo()
.with_stderr_contains("[IGNORED] package `foo v1.0.1` is already installed[..]")
.run();
}
#[test]
fn uninstall() {
// Basic uninstall test.
pkg("foo", "1.0.0");
cargo_process("install foo -Z install-upgrade")
.masquerade_as_nightly_cargo()
.run();
cargo_process("uninstall foo -Z install-upgrade")
.masquerade_as_nightly_cargo()
.run();
let data = load_crates2();
assert_eq!(data["installs"].as_object().unwrap().len(), 0);
let v1 = cargo_home().join(".crates.toml");
let data: toml::Value = toml::from_str(&fs::read_to_string(&v1).unwrap()).unwrap();
assert_eq!(data.get("v1").unwrap().as_table().unwrap().len(), 0);
}
#[test]
fn upgrade_force() {
pkg("foo", "1.0.0");
cargo_process("install foo -Z install-upgrade")
.masquerade_as_nightly_cargo()
.run();
cargo_process("install foo -Z install-upgrade --force")
.masquerade_as_nightly_cargo()
.with_stderr(
"\
[UPDATING] `[..]` index
[INSTALLING] foo v1.0.0
[COMPILING] foo v1.0.0
[FINISHED] release [optimized] target(s) in [..]
[REPLACING] [..]/.cargo/bin/foo[EXE]
[REPLACED] package `foo v1.0.0` with `foo v1.0.0` (executable `foo[EXE]`)
[WARNING] be sure to add `[..]/.cargo/bin` to your PATH [..]
",
)
.run();
}
#[test]
fn ambiguous_version_no_longer_allowed() {
// Non-semver-requirement is not allowed for `--version`.
pkg("foo", "1.0.0");
cargo_process("install foo --version=1.0 -Z install-upgrade")
.masquerade_as_nightly_cargo()
.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
",
)
.with_status(101)
.run();
}
#[test]
fn path_is_always_dirty() {
// --path should always reinstall.
let p = project().file("src/main.rs", "fn main() {}").build();
p.cargo("install --path . -Z install-upgrade")
.masquerade_as_nightly_cargo()
.run();
p.cargo("install --path . -Z install-upgrade")
.masquerade_as_nightly_cargo()
.with_stderr_contains("[REPLACING] [..]/foo[EXE]")
.run();
}
#[test]
fn fails_for_conflicts_unknown() {
// If an untracked file is in the way, it should fail.
pkg("foo", "1.0.0");
let exe = installed_exe("foo");
exe.parent().unwrap().mkdir_p();
fs::write(exe, "").unwrap();
cargo_process("install foo -Z install-upgrade")
.masquerade_as_nightly_cargo()
.with_stderr_contains("[ERROR] binary `foo[EXE]` already exists in destination")
.with_status(101)
.run();
}
#[test]
fn fails_for_conflicts_known() {
// If the same binary exists in another package, it should fail.
pkg("foo", "1.0.0");
Package::new("bar", "1.0.0")
.file("src/bin/foo.rs", "fn main() {}")
.publish();
cargo_process("install foo -Z install-upgrade")
.masquerade_as_nightly_cargo()
.run();
cargo_process("install bar -Z install-upgrade")
.masquerade_as_nightly_cargo()
.with_stderr_contains(
"[ERROR] binary `foo[EXE]` already exists in destination as part of `foo v1.0.0`",
)
.with_status(101)
.run();
}
#[test]
fn supports_multiple_binary_names() {
// Can individually install with --bin or --example
Package::new("foo", "1.0.0")
.file("src/main.rs", r#"fn main() { println!("foo"); }"#)
.file("src/bin/a.rs", r#"fn main() { println!("a"); }"#)
.file("examples/ex1.rs", r#"fn main() { println!("ex1"); }"#)
.publish();
cargo_process("install foo -Z install-upgrade --bin foo")
.masquerade_as_nightly_cargo()
.run();
installed_process("foo").with_stdout("foo").run();
assert!(!installed_exe("a").exists());
assert!(!installed_exe("ex1").exists());
cargo_process("install foo -Z install-upgrade --bin a")
.masquerade_as_nightly_cargo()
.run();
installed_process("a").with_stdout("a").run();
assert!(!installed_exe("ex1").exists());
cargo_process("install foo -Z install-upgrade --example ex1")
.masquerade_as_nightly_cargo()
.run();
installed_process("ex1").with_stdout("ex1").run();
cargo_process("uninstall foo -Z install-upgrade --bin foo")
.masquerade_as_nightly_cargo()
.run();
assert!(!installed_exe("foo").exists());
assert!(installed_exe("ex1").exists());
cargo_process("uninstall foo -Z install-upgrade")
.masquerade_as_nightly_cargo()
.run();
assert!(!installed_exe("ex1").exists());
assert!(!installed_exe("a").exists());
}
#[test]
fn v1_already_installed_fresh() {
// Install with v1, then try to install again with v2.
pkg("foo", "1.0.0");
cargo_process("install foo").run();
cargo_process("install foo -Z install-upgrade")
.with_stderr_contains("[IGNORED] package `foo v1.0.0` is already installed[..]")
.masquerade_as_nightly_cargo()
.run();
}
#[test]
fn v1_already_installed_dirty() {
// Install with v1, then install a new version with v2.
pkg("foo", "1.0.0");
cargo_process("install foo").run();
pkg("foo", "1.0.1");
cargo_process("install foo -Z install-upgrade")
.with_stderr_contains("[COMPILING] foo v1.0.1")
.with_stderr_contains("[REPLACING] [..]/foo[EXE]")
.masquerade_as_nightly_cargo()
.run();
}
#[test]
fn change_features_rebuilds() {
Package::new("foo", "1.0.0")
.file(
"src/main.rs",
r#"fn main() {
if cfg!(feature = "f1") {
println!("f1");
}
if cfg!(feature = "f2") {
println!("f2");
}
}"#,
)
.file(
"Cargo.toml",
r#"
[package]
name = "foo"
version = "1.0.0"
[features]
f1 = []
f2 = []
default = ["f1"]
"#,
)
.publish();
cargo_process("install foo -Z install-upgrade")
.masquerade_as_nightly_cargo()
.run();
installed_process("foo").with_stdout("f1").run();
cargo_process("install foo -Z install-upgrade --no-default-features")
.masquerade_as_nightly_cargo()
.run();
installed_process("foo").with_stdout("").run();
cargo_process("install foo -Z install-upgrade --all-features")
.masquerade_as_nightly_cargo()
.run();
installed_process("foo").with_stdout("f1\nf2").run();
cargo_process("install foo -Z install-upgrade --no-default-features --features=f1")
.masquerade_as_nightly_cargo()
.run();
installed_process("foo").with_stdout("f1").run();
}
#[test]
fn change_profile_rebuilds() {
pkg("foo", "1.0.0");
cargo_process("install foo -Z install-upgrade")
.masquerade_as_nightly_cargo()
.run();
cargo_process("install foo -Z install-upgrade --debug")
.masquerade_as_nightly_cargo()
.with_stderr_contains("[COMPILING] foo v1.0.0")
.with_stderr_contains("[REPLACING] [..]foo[EXE]")
.run();
cargo_process("install foo -Z install-upgrade --debug")
.masquerade_as_nightly_cargo()
.with_stderr_contains("[IGNORED] package `foo v1.0.0` is already installed[..]")
.run();
}
#[test]
fn change_target_rebuilds() {
if cross_compile::disabled() {
return;
}
pkg("foo", "1.0.0");
cargo_process("install foo -Z install-upgrade")
.masquerade_as_nightly_cargo()
.run();
let target = cross_compile::alternate();
cargo_process("install foo -v -Z install-upgrade --target")
.arg(&target)
.masquerade_as_nightly_cargo()
.with_stderr_contains("[COMPILING] foo v1.0.0")
.with_stderr_contains("[REPLACING] [..]foo[EXE]")
.with_stderr_contains(&format!("[..]--target {}[..]", target))
.run();
}
#[test]
fn change_bin_sets_rebuilds() {
// Changing which bins in a multi-bin project should reinstall.
Package::new("foo", "1.0.0")
.file("src/main.rs", "fn main() { }")
.file("src/bin/x.rs", "fn main() { }")
.file("src/bin/y.rs", "fn main() { }")
.publish();
cargo_process("install foo -Z install-upgrade --bin x")
.masquerade_as_nightly_cargo()
.run();
assert!(installed_exe("x").exists());
assert!(!installed_exe("y").exists());
assert!(!installed_exe("foo").exists());
cargo_process("install foo -Z install-upgrade --bin y")
.masquerade_as_nightly_cargo()
.with_stderr_contains("[INSTALLED] package `foo v1.0.0` (executable `y[EXE]`)")
.run();
assert!(installed_exe("x").exists());
assert!(installed_exe("y").exists());
assert!(!installed_exe("foo").exists());
cargo_process("install foo -Z install-upgrade")
.masquerade_as_nightly_cargo()
.with_stderr_contains("[INSTALLED] package `foo v1.0.0` (executable `foo[EXE]`)")
.with_stderr_contains(
"[REPLACED] package `foo v1.0.0` with `foo v1.0.0` (executables `x[EXE]`, `y[EXE]`)",
)
.run();
assert!(installed_exe("x").exists());
assert!(installed_exe("y").exists());
assert!(installed_exe("foo").exists());
}
#[test]
fn forwards_compatible() {
// Unknown fields should be preserved.
pkg("foo", "1.0.0");
pkg("bar", "1.0.0");
cargo_process("install foo -Z install-upgrade")
.masquerade_as_nightly_cargo()
.run();
let key = "foo 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)";
let v2 = cargo_home().join(".crates2.json");
let mut data = load_crates2();
data["newfield"] = serde_json::Value::Bool(true);
data["installs"][key]["moreinfo"] = serde_json::Value::String("shazam".to_string());
fs::write(&v2, serde_json::to_string(&data).unwrap()).unwrap();
cargo_process("install bar -Z install-upgrade")
.masquerade_as_nightly_cargo()
.run();
let data: serde_json::Value = serde_json::from_str(&fs::read_to_string(&v2).unwrap()).unwrap();
assert_eq!(data["newfield"].as_bool().unwrap(), true);
assert_eq!(
data["installs"][key]["moreinfo"].as_str().unwrap(),
"shazam"
);
}
#[test]
fn v2_syncs() {
// V2 inherits the installs from V1.
pkg("one", "1.0.0");
pkg("two", "1.0.0");
pkg("three", "1.0.0");
let p = project()
.file("src/bin/x.rs", "fn main() {}")
.file("src/bin/y.rs", "fn main() {}")
.build();
cargo_process("install one -Z install-upgrade")
.masquerade_as_nightly_cargo()
.run();
p.cargo("install -Z install-upgrade --path .")
.masquerade_as_nightly_cargo()
.run();
// v1 add/remove
cargo_process("install two").run();
cargo_process("uninstall one").run();
// This should pick up that `two` was added, `one` was removed.
cargo_process("install three -Z install-upgrade")
.masquerade_as_nightly_cargo()
.run();
cargo_process("install --list")
.with_stdout(
"\
foo v0.0.1 ([..]/foo):
x[EXE]
y[EXE]
three v1.0.0:
three[EXE]
two v1.0.0:
two[EXE]
",
)
.run();
cargo_process("install one -Z install-upgrade")
.masquerade_as_nightly_cargo()
.run();
installed_process("one").with_stdout("1.0.0").run();
cargo_process("install two -Z install-upgrade")
.masquerade_as_nightly_cargo()
.with_stderr_contains("[IGNORED] package `two v1.0.0` is already installed[..]")
.run();
// v1 remove
p.cargo("uninstall --bin x").run();
pkg("x", "1.0.0");
pkg("y", "1.0.0");
// This should succeed because `x` was removed in V1.
cargo_process("install x -Z install-upgrade")
.masquerade_as_nightly_cargo()
.run();
// This should fail because `y` still exists in a different package.
cargo_process("install y -Z install-upgrade")
.masquerade_as_nightly_cargo()
.with_stderr_contains(
"[ERROR] binary `y[EXE]` already exists in destination \
as part of `foo v0.0.1 ([..])`",
)
.with_status(101)
.run();
}
#[test]
fn upgrade_git() {
let git_project =
git::new("foo", |project| project.file("src/main.rs", "fn main() {}")).unwrap();
// install
cargo_process("install -Z install-upgrade --git")
.arg(git_project.url().to_string())
.masquerade_as_nightly_cargo()
.run();
// Check install stays fresh.
cargo_process("install -Z install-upgrade --git")
.arg(git_project.url().to_string())
.masquerade_as_nightly_cargo()
.with_stderr_contains(
"[IGNORED] package `foo v0.0.1 (file://[..]/foo#[..])` is \
already installed,[..]",
)
.run();
// Modify a file.
let repo = git2::Repository::open(git_project.root()).unwrap();
git_project.change_file("src/main.rs", r#"fn main() {println!("onomatopoeia");}"#);
git::add(&repo);
git::commit(&repo);
// Install should reinstall.
cargo_process("install -Z install-upgrade --git")
.arg(git_project.url().to_string())
.masquerade_as_nightly_cargo()
.with_stderr_contains("[COMPILING] foo v0.0.1 ([..])")
.with_stderr_contains("[REPLACING] [..]/foo[EXE]")
.run();
installed_process("foo").with_stdout("onomatopoeia").run();
// Check install stays fresh.
cargo_process("install -Z install-upgrade --git")
.arg(git_project.url().to_string())
.masquerade_as_nightly_cargo()
.with_stderr_contains(
"[IGNORED] package `foo v0.0.1 (file://[..]/foo#[..])` is \
already installed,[..]",
)
.run();
}
#[test]
fn switch_sources() {
// Installing what appears to be the same thing, but from different
// sources should reinstall.
pkg("foo", "1.0.0");
Package::new("foo", "1.0.0")
.file("src/main.rs", r#"fn main() { println!("alt"); }"#)
.alternative(true)
.publish();
let p = project()
.at("foo-local") // so it doesn't use the same directory as the git project
.file("Cargo.toml", &basic_manifest("foo", "1.0.0"))
.file("src/main.rs", r#"fn main() { println!("local"); }"#)
.build();
let git_project = git::new("foo", |project| {
project.file("src/main.rs", r#"fn main() { println!("git"); }"#)
})
.unwrap();
cargo_process("install -Z install-upgrade foo")
.masquerade_as_nightly_cargo()
.run();
installed_process("foo").with_stdout("1.0.0").run();
cargo_process("install -Z install-upgrade foo --registry alternative")
.masquerade_as_nightly_cargo()
.run();
installed_process("foo").with_stdout("alt").run();
p.cargo("install -Z install-upgrade --path .")
.masquerade_as_nightly_cargo()
.run();
installed_process("foo").with_stdout("local").run();
cargo_process("install -Z install-upgrade --git")
.arg(git_project.url().to_string())
.masquerade_as_nightly_cargo()
.run();
installed_process("foo").with_stdout("git").run();
}
#[test]
fn multiple_report() {
// Testing the full output that indicates installed/ignored/replaced/summary.
pkg("one", "1.0.0");
pkg("two", "1.0.0");
fn three(vers: &str) {
Package::new("three", vers)
.file("src/main.rs", "fn main() { }")
.file("src/bin/x.rs", "fn main() { }")
.file("src/bin/y.rs", "fn main() { }")
.publish();
}
three("1.0.0");
cargo_process("install -Z install-upgrade one two three")
.masquerade_as_nightly_cargo()
.with_stderr(
"\
[UPDATING] `[..]` index
[DOWNLOADING] crates ...
[DOWNLOADED] one v1.0.0 (registry `[..]`)
[INSTALLING] one v1.0.0
[COMPILING] one v1.0.0
[FINISHED] release [optimized] target(s) in [..]
[INSTALLING] [..]/.cargo/bin/one[EXE]
[INSTALLED] package `one v1.0.0` (executable `one[EXE]`)
[DOWNLOADING] crates ...
[DOWNLOADED] two v1.0.0 (registry `[..]`)
[INSTALLING] two v1.0.0
[COMPILING] two v1.0.0
[FINISHED] release [optimized] target(s) in [..]
[INSTALLING] [..]/.cargo/bin/two[EXE]
[INSTALLED] package `two v1.0.0` (executable `two[EXE]`)
[DOWNLOADING] crates ...
[DOWNLOADED] three v1.0.0 (registry `[..]`)
[INSTALLING] three v1.0.0
[COMPILING] three v1.0.0
[FINISHED] release [optimized] target(s) in [..]
[INSTALLING] [..]/.cargo/bin/three[EXE]
[INSTALLING] [..]/.cargo/bin/x[EXE]
[INSTALLING] [..]/.cargo/bin/y[EXE]
[INSTALLED] package `three v1.0.0` (executables `three[EXE]`, `x[EXE]`, `y[EXE]`)
[SUMMARY] Successfully installed one, two, three!
[WARNING] be sure to add `[..]/.cargo/bin` to your PATH [..]
",
)
.run();
pkg("foo", "1.0.1");
pkg("bar", "1.0.1");
three("1.0.1");
cargo_process("install -Z install-upgrade one two three")
.masquerade_as_nightly_cargo()
.with_stderr(
"\
[UPDATING] `[..]` index
[IGNORED] package `one v1.0.0` is already installed, use --force to override
[IGNORED] package `two v1.0.0` is already installed, use --force to override
[DOWNLOADING] crates ...
[DOWNLOADED] three v1.0.1 (registry `[..]`)
[INSTALLING] three v1.0.1
[COMPILING] three v1.0.1
[FINISHED] release [optimized] target(s) in [..]
[REPLACING] [..]/.cargo/bin/three[EXE]
[REPLACING] [..]/.cargo/bin/x[EXE]
[REPLACING] [..]/.cargo/bin/y[EXE]
[REPLACED] package `three v1.0.0` with `three v1.0.1` (executables `three[EXE]`, `x[EXE]`, `y[EXE]`)
[SUMMARY] Successfully installed one, two, three!
[WARNING] be sure to add `[..]/.cargo/bin` to your PATH [..]
",
)
.run();
cargo_process("uninstall -Z install-upgrade three")
.masquerade_as_nightly_cargo()
.with_stderr(
"\
[REMOVING] [..]/.cargo/bin/three[EXE]
[REMOVING] [..]/.cargo/bin/x[EXE]
[REMOVING] [..]/.cargo/bin/y[EXE]
",
)
.run();
cargo_process("install -Z install-upgrade three --bin x")
.masquerade_as_nightly_cargo()
.with_stderr(
"\
[UPDATING] `[..]` index
[INSTALLING] three v1.0.1
[COMPILING] three v1.0.1
[FINISHED] release [optimized] target(s) in [..]
[INSTALLING] [..]/.cargo/bin/x[EXE]
[INSTALLED] package `three v1.0.1` (executable `x[EXE]`)
[WARNING] be sure to add `[..]/.cargo/bin` to your PATH [..]
",
)
.run();
cargo_process("install -Z install-upgrade three")
.masquerade_as_nightly_cargo()
.with_stderr(
"\
[UPDATING] `[..]` index
[INSTALLING] three v1.0.1
[COMPILING] three v1.0.1
[FINISHED] release [optimized] target(s) in [..]
[INSTALLING] [..]/.cargo/bin/three[EXE]
[INSTALLING] [..]/.cargo/bin/y[EXE]
[REPLACING] [..]/.cargo/bin/x[EXE]
[INSTALLED] package `three v1.0.1` (executables `three[EXE]`, `y[EXE]`)
[REPLACED] package `three v1.0.1` with `three v1.0.1` (executable `x[EXE]`)
[WARNING] be sure to add `[..]/.cargo/bin` to your PATH [..]
",
)
.run();
}
#[test]
fn no_track_gated() {
cargo_process("install --no-track foo")
.masquerade_as_nightly_cargo()
.with_stderr(
"[ERROR] `--no-track` flag is unstable, pass `-Z install-upgrade` to enable it",
)
.with_status(101)
.run();
}
#[test]
fn no_track() {
pkg("foo", "1.0.0");
cargo_process("install --no-track foo -Z install-upgrade")
.masquerade_as_nightly_cargo()
.run();
assert!(!v1_path().exists());
assert!(!v2_path().exists());
cargo_process("install --no-track foo -Z install-upgrade")
.masquerade_as_nightly_cargo()
.with_stderr(
"\
[UPDATING] `[..]` index
[ERROR] binary `foo[EXE]` already exists in destination
Add --force to overwrite
",
)
.with_status(101)
.run();
}

View File

@ -44,6 +44,7 @@ mod generate_lockfile;
mod git;
mod init;
mod install;
mod install_upgrade;
mod jobserver;
mod list_targets;
mod local_registry;

View File

@ -1616,6 +1616,9 @@ fn substitute_macros(input: &str) -> String {
("[SUMMARY]", " Summary"),
("[FIXING]", " Fixing"),
("[EXE]", env::consts::EXE_SUFFIX),
("[IGNORED]", " Ignored"),
("[INSTALLED]", " Installed"),
("[REPLACED]", " Replaced"),
];
let mut result = input.to_owned();
for &(pat, subst) in &macros {