diff --git a/src/bin/cargo/cli.rs b/src/bin/cargo/cli.rs index 004974ce3ba..7ab3f8a2180 100644 --- a/src/bin/cargo/cli.rs +++ b/src/bin/cargo/cli.rs @@ -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]'" ); diff --git a/src/bin/cargo/commands/install.rs b/src/bin/cargo/commands/install.rs index f4d6f430076..0ee192bfb6a 100644 --- a/src/bin/cargo/commands/install.rs +++ b/src/bin/cargo/commands/install.rs @@ -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(()) diff --git a/src/cargo/core/features.rs b/src/cargo/core/features.rs index b62dc50704b..59787c3c3e8 100644 --- a/src/cargo/core/features.rs +++ b/src/cargo/core/features.rs @@ -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), } diff --git a/src/cargo/ops/cargo_install.rs b/src/cargo/ops/cargo_install.rs index 76925ed4d8a..7591b148a07 100644 --- a/src/cargo/ops/cargo_install.rs +++ b/src/cargo/ops/cargo_install.rs @@ -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, @@ -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,66 @@ fn install_one( } }; - config.shell().status("Installing", pkg)?; + // For bare `cargo install` (no `--bin` or `--example`), check if there is + // *something* to install. Explicit `--bin` or `--example` flags will be + // checked at the start of `compile_ws`. + 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>> { + let duplicates: BTreeMap> = 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 = 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) + }; + + // WARNING: no_track does not perform locking, so there is no protection + // of concurrent installs. + 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 = Arc::new(DefaultExecutor); let compile = ops::compile_ws(&ws, Some(source), opts, &exec).chain_err(|| { if let Some(td) = td_opt.take() { @@ -258,14 +308,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 +323,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::>()?; 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 +357,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 +367,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 +375,44 @@ 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); - } - } - // 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::>(); - 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())); - } + if let Some(mut tracker) = tracker { + tracker.mark_installed( + pkg, + &successful_bins, + vers.map(|s| s.to_string()), + opts, + target, + rustc.verbose_version, + ); - 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?, + match tracker.save() { + Err(err) => replace_result.chain_err(|| err)?, + Ok(_) => replace_result?, + } } // Reaching here means all actions have succeeded. Clean up. @@ -402,98 +424,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>> { - // 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>(mut names: impl Iterator + 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::>() + .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> { - 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 = 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 = 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::>>() + // 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); diff --git a/src/cargo/ops/cargo_uninstall.rs b/src/cargo/ops/cargo_uninstall.rs index ca2fff7dfdc..96c64e03e07 100644 --- a/src/cargo/ops/cargo_uninstall.rs +++ b/src/cargo/ops/cargo_uninstall.rs @@ -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 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::>(); + let installed = match tracker.installed_bins(pkgid) { + Some(bins) => bins.clone(), + None => bail!("package `{}` is not installed", pkgid), + }; - for bin in bins.iter() { - if !installed.get().contains(bin) { - failure::bail!("binary `{}` not installed as part of `{}`", bin, pkgid) - } + 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() + ) } + } - 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); + 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::>(); + + for bin in bins.iter() { + if !installed.contains(bin) { + bail!("binary `{}` not installed as part of `{}`", bin, pkgid) } - if installed.get().is_empty() { - installed.remove(); + } + + 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); } - write_crate_list(crate_metadata, metadata)?; + tracker.save()?; for bin in to_remove { config.shell().status("Removing", bin.display())?; paths::remove_file(bin)?; diff --git a/src/cargo/ops/common_for_install_and_uninstall.rs b/src/cargo/ops/common_for_install_and_uninstall.rs index 48daab75e5c..c581772d457 100644 --- a/src/cargo/ops/common_for_install_and_uninstall.rs +++ b/src/cargo/ops/common_for_install_and_uninstall.rs @@ -1,45 +1,537 @@ -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}; - -#[derive(Deserialize, Serialize)] -#[serde(untagged)] -pub enum CrateListing { - V1(CrateListingV1), - Empty(Empty), +use crate::util::{Config, ToSemver}; +use crate::util::{FileLock, Filesystem, Freshness}; + +/// 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. +/// +/// If/when v2 is stabilized, it is intended that v1 is retained for a while +/// during a longish transition period, and then v1 can be removed. +pub struct InstallTracker { + v1: CrateListingV1, + v2: CrateListingV2, + v1_lock: FileLock, + v2_lock: Option, + unstable_upgrade: bool, +} + +/// Tracking information for the set of installed packages. +/// +/// This v2 format is unstable and requires the `-Z unstable-upgrade` option +/// to enable. +#[derive(Default, Deserialize, Serialize)] +struct CrateListingV2 { + installs: BTreeMap, + /// Forwards compatibility. + #[serde(flatten)] + other: BTreeMap, } -#[derive(Deserialize, Serialize)] -#[serde(deny_unknown_fields)] -pub struct Empty {} +/// Tracking information for the installation of a single package. +/// +/// This tracks the settings that were used when the package was installed. +/// Future attempts to install the same package will check these settings to +/// determine if it needs to be rebuilt/reinstalled. If nothing has changed, +/// then Cargo will inform the user that it is "up to date". +/// +/// This is only used for the (unstable) v2 format. +#[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, + /// Set of binary names installed. + bins: BTreeSet, + /// Set of features explicitly enabled. + features: BTreeSet, + 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, + /// Output of `rustc -V`. + /// None if unknown (when loading from v1). + /// Currently not used, possibly may be used in the future. + rustc: Option, + /// Forwards compatibility. + #[serde(flatten)] + other: BTreeMap, +} -#[derive(Deserialize, Serialize)] +/// Tracking information for the set of installed packages. +#[derive(Default, Deserialize, Serialize)] pub struct CrateListingV1 { v1: BTreeMap>, } +impl InstallTracker { + /// Create an InstallTracker from information on disk. + pub fn load(config: &Config, root: &Filesystem) -> CargoResult { + 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<_> { + match &v2_lock { + Some(lock) => { + let mut contents = String::new(); + lock.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) + } + None => 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, + }) + } + + /// 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`) and should be skipped. The map maps binary names to the + /// PackageId that installed it (which is None if not known). + /// + /// If there are no duplicates, then it will be considered `Dirty` (i.e., + /// it is OK to build/install). + /// + /// `force=true` will always be considered `Dirty` (i.e., it will always + /// be rebuilt/reinstalled). + /// + /// 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>)> { + let exes = exe_names(pkg, &opts.filter); + // Check if any tracked exe's are already installed. + let duplicates = self.find_duplicates(dst, &exes); + if force || duplicates.is_empty() { + return Ok((Freshness::Dirty, duplicates)); + } + // Check if all duplicates come from packages of the same name. If + // there are duplicates from other packages, then --force will be + // required. + // + // There may be multiple matching duplicates if different versions of + // the same package installed different binaries. + // + // This does not check the source_id in order to allow the user to + // switch between different sources. For example, installing from git, + // and then switching to the official crates.io release or vice-versa. + // If the source_id were included, then the user would get possibly + // confusing errors like "package `foo 1.0.0` is already installed" + // and the change of source may not be obvious why it fails. + let matching_duplicates: Vec = 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 both sets are the same length, that means all duplicates come + // from packages with the same name. + if self.unstable_upgrade && matching_duplicates.len() == duplicates.len() { + // Determine if it is dirty or fresh. + let source_id = pkg.package_id().source_id(); + if source_id.is_path() { + // `cargo install --path ...` is always rebuilt. + 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() { + // Git sources must have the exact same hash to be + // considered "fresh". + 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.is_up_to_date(opts, target, &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); + } + } + + /// Check if any executables are already installed. + /// + /// Returns a map of duplicates, the key is the executable name and the + /// value is the PackageId that is already installed. The PackageId is + /// None if it is an untracked executable. + fn find_duplicates( + &self, + dst: &Path, + exes: &BTreeSet, + ) -> BTreeMap> { + 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, + version_req: Option, + 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)> { + 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> { + self.v1.v1.get(&pkg_id) + } + + /// Remove a package from the tracker. + pub fn remove(&mut self, pkg_id: PackageId, bins: &BTreeSet) { + self.v1.remove(pkg_id, bins); + if self.unstable_upgrade { + self.v2.remove(pkg_id, bins); + } + } +} + impl CrateListingV1 { - pub fn v1(&self) -> &BTreeMap> { - &self.v1 + fn package_for_bin(&self, bin_name: &str) -> Option { + self.v1 + .iter() + .find(|(_, bins)| bins.contains(bin_name)) + .map(|(pkg_id, _)| *pkg_id) + } + + fn mark_installed(&mut self, pkg: &Package, bins: &BTreeSet) { + // Remove bins from any other packages. + for other_bins in self.v1.values_mut() { + for bin in bins { + other_bins.remove(bin); + } + } + // Remove entries where `bins` is empty. + let to_remove = self + .v1 + .iter() + .filter_map(|(&p, set)| if set.is_empty() { Some(p) } else { None }) + .collect::>(); + for p in to_remove.iter() { + self.v1.remove(p); + } + // Add these bins. + self.v1 + .entry(pkg.package_id()) + .or_insert_with(BTreeSet::new) + .append(&mut bins.clone()); } - pub fn v1_mut(&mut self) -> &mut BTreeMap> { - &mut self.v1 + fn remove(&mut self, pkg_id: PackageId, bins: &BTreeSet) { + 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 { + /// Incorporate any changes from v1 into self. + /// This handles the initial upgrade to v2, *and* handles the case + /// where v2 is in use, and a v1 update is made, then v2 is used again. + /// i.e., `cargo +new install foo ; cargo +old install bar ; cargo +new install bar` + /// For now, v1 is the source of truth, so its values are trusted over v2. + 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 { + self.installs + .iter() + .find(|(_, info)| info.bins.contains(bin_name)) + .map(|(pkg_id, _)| *pkg_id) + } + + fn mark_installed( + &mut self, + pkg: &Package, + bins: &BTreeSet, + version_req: Option, + 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 entries where `bins` is empty. + let to_remove = self + .installs + .iter() + .filter_map(|(&p, info)| if info.bins.is_empty() { Some(p) } else { None }) + .collect::>(); + for p in to_remove.iter() { + self.installs.remove(p); + } + // Add these bins. + if let Some(info) = self.installs.get_mut(&pkg.package_id()) { + info.bins.append(&mut bins.clone()); + 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) { + 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)?; + let data = serde_json::to_string(self)?; + file.write_all(data.as_bytes())?; + Ok(()) + } +} + +impl InstallInfo { + fn from_v1(set: &BTreeSet) -> 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(), + } + } + + /// Determine if this installation is "up to date", or if it needs to be reinstalled. + /// + /// This does not do Package/Source/Version checking. + fn is_up_to_date( + &self, + opts: &CompileOptions<'_>, + target: &str, + exes: &BTreeSet, + ) -> bool { + self.features == feature_set(&opts.features) + && self.all_features == opts.all_features + && self.no_default_features == opts.no_default_features + && self.profile == profile_name(opts.build_config.release) + && (self.target.is_none() || self.target.as_ref().map(|t| t.as_ref()) == Some(target)) + && &self.bins == exes + } +} + +/// Determines the root directory where installation is done. pub fn resolve_root(flag: Option<&str>, config: &Config) -> CargoResult { let config_root = config.get_path("install.root")?; Ok(flag @@ -50,14 +542,16 @@ pub fn resolve_root(flag: Option<&str>, config: &Config) -> CargoResult(source_id: SourceId, config: &'a Config) -> CargoResult> { 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 +567,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") - })?; - - match first { - '<' | '>' | '=' | '^' | '~' => match v.parse::() { - Ok(v) => Some(v.to_string()), - Err(_) => failure::bail!( + 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"))?; + + let is_req = "<>=^~".contains(first) || v.contains('*'); + if is_req { + match v.parse::() { + Ok(v) => Some(v.to_string()), + Err(_) => bail!( + "the `--vers` provided, `{}`, is \ + not a valid semver version requirement\n\n\ + Please have a look at \ + https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html \ + for the correct format", + v + ), + } + } else { + match v.to_semver() { + Ok(v) => Some(format!("={}", v)), + Err(e) => { + let mut msg = 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 + ) + }; + + // If it is not a valid version but it is a valid version + // requirement, add a note to the warning + if v.parse::().is_ok() { + msg.push_str(&format!( + "\nif you want to specify semver range, \ + add an explicit qualifier, like ^{}", v - ), - }, - _ => 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::().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 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::>() - .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::>() + .join(", ") + ) + } } } -pub fn one(mut i: I, f: F) -> CargoResult> +/// 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(mut i: I, f: F) -> CargoResult> where I: Iterator, F: FnOnce(Vec) -> String, @@ -199,53 +706,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 { - 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(), - }), - } - })() - .chain_err(|| { - failure::format_err!( - "failed to parse crate metadata at `{}`", - file.path().to_string_lossy() - ) - })?; - Ok(listing) +fn profile_name(release: bool) -> &'static str { + if release { + "release" + } else { + "dev" + } } -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(()) +/// Helper to convert features Vec to a BTreeSet. +fn feature_set(features: &[String]) -> BTreeSet { + features.iter().cloned().collect() } -pub fn metadata(config: &Config, root: &Filesystem) -> CargoResult { - root.open_rw(Path::new(".crates.toml"), config, "crate metadata") +/// Helper to get the executable names from a filter. +pub fn exe_names(pkg: &Package, filter: &ops::CompileFilter) -> BTreeSet { + 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 = 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 = 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() + } + } } diff --git a/src/doc/src/reference/unstable.md b/src/doc/src/reference/unstable.md index 8322ab8aa51..9a740d75881 100644 --- a/src/doc/src/reference/unstable.md +++ b/src/doc/src/reference/unstable.md @@ -232,3 +232,35 @@ 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. + +Installing with `--path` will always build and install, unless there are +conflicting binaries from another 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. diff --git a/tests/testsuite/directory.rs b/tests/testsuite/directory.rs index 80bcfcf7d65..b02183bcc5c 100644 --- a/tests/testsuite/directory.rs +++ b/tests/testsuite/directory.rs @@ -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(); diff --git a/tests/testsuite/install.rs b/tests/testsuite/install.rs index d870cc185cd..0a0ead7bc0a 100644 --- a/tests/testsuite/install.rs +++ b/tests/testsuite/install.rs @@ -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(); +} diff --git a/tests/testsuite/install_upgrade.rs b/tests/testsuite/install_upgrade.rs new file mode 100644 index 00000000000..2e4dcef43f6 --- /dev/null +++ b/tests/testsuite/install_upgrade.rs @@ -0,0 +1,786 @@ +use cargo::core::PackageId; +use std::collections::BTreeSet; +use std::env; +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_crates1() -> toml::Value { + toml::from_str(&fs::read_to_string(v1_path()).unwrap()).unwrap() +} + +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) +} + +/// Check that the given package name/version has the following bins listed in +/// the trackers. Also verifies that both trackers are in sync and valid. +fn validate_trackers(name: &str, version: &str, bins: &[&str]) { + let v1 = load_crates1(); + let v1_table = v1.get("v1").unwrap().as_table().unwrap(); + let v2 = load_crates2(); + let v2_table = v2["installs"].as_object().unwrap(); + assert_eq!(v1_table.len(), v2_table.len()); + // Convert `bins` to a BTreeSet. + let bins: BTreeSet = bins + .iter() + .map(|b| format!("{}{}", b, env::consts::EXE_SUFFIX)) + .collect(); + // Check every entry matches between v1 and v2. + for (pkg_id_str, v1_bins) in v1_table { + let pkg_id: PackageId = toml::Value::from(pkg_id_str.to_string()) + .try_into() + .unwrap(); + let v1_bins: BTreeSet = v1_bins + .as_array() + .unwrap() + .iter() + .map(|b| b.as_str().unwrap().to_string()) + .collect(); + if pkg_id.name().as_str() == name && pkg_id.version().to_string() == version { + assert_eq!(bins, v1_bins); + } + let pkg_id_value = serde_json::to_value(&pkg_id).unwrap(); + let pkg_id_str = pkg_id_value.as_str().unwrap(); + let v2_info = v2_table + .get(pkg_id_str) + .expect("v2 missing v1 pkg") + .as_object() + .unwrap(); + let v2_bins = v2_info["bins"].as_array().unwrap(); + let v2_bins: BTreeSet = v2_bins + .iter() + .map(|b| b.as_str().unwrap().to_string()) + .collect(); + assert_eq!(v1_bins, v2_bins); + } +} + +#[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(); + validate_trackers("foo", "1.0.0", &["foo"]); + + 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(); + validate_trackers("foo", "1.0.1", &["foo"]); + + 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(); + validate_trackers("foo", "1.0.0", &["foo"]); + + 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(); + validate_trackers("foo", "1.0.1", &["foo"]); + + 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_table = load_crates1(); + assert_eq!(v1_table.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(); + validate_trackers("foo", "1.0.0", &["foo"]); +} + +#[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()); + validate_trackers("foo", "1.0.0", &["foo"]); + 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()); + validate_trackers("foo", "1.0.0", &["a", "foo"]); + cargo_process("install foo -Z install-upgrade --example ex1") + .masquerade_as_nightly_cargo() + .run(); + installed_process("ex1").with_stdout("ex1").run(); + validate_trackers("foo", "1.0.0", &["a", "ex1", "foo"]); + cargo_process("uninstall foo -Z install-upgrade --bin foo") + .masquerade_as_nightly_cargo() + .run(); + assert!(!installed_exe("foo").exists()); + assert!(installed_exe("ex1").exists()); + validate_trackers("foo", "1.0.0", &["a", "ex1"]); + 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(); + validate_trackers("foo", "1.0.1", &["foo"]); +} + +#[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()); + validate_trackers("foo", "1.0.0", &["x"]); + 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()); + validate_trackers("foo", "1.0.0", &["x", "y"]); + 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()); + validate_trackers("foo", "1.0.0", &["foo", "x", "y"]); +} + +#[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(); + validate_trackers("one", "1.0.0", &["one"]); + p.cargo("install -Z install-upgrade --path .") + .masquerade_as_nightly_cargo() + .run(); + validate_trackers("foo", "1.0.0", &["x", "y"]); + // 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(); + validate_trackers("three", "1.0.0", &["three"]); + 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(); + validate_trackers("one", "1.0.0", &["one"]); + 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(); + validate_trackers("x", "1.0.0", &["x"]); + // 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(); +} diff --git a/tests/testsuite/main.rs b/tests/testsuite/main.rs index b8b86a93820..69ca0186988 100644 --- a/tests/testsuite/main.rs +++ b/tests/testsuite/main.rs @@ -44,6 +44,7 @@ mod generate_lockfile; mod git; mod init; mod install; +mod install_upgrade; mod jobserver; mod list_targets; mod local_registry; diff --git a/tests/testsuite/support/mod.rs b/tests/testsuite/support/mod.rs index 9e9beaac4c5..3c183330fc8 100644 --- a/tests/testsuite/support/mod.rs +++ b/tests/testsuite/support/mod.rs @@ -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 ¯os {