From e023a6672b088ad6aa104e226c8951cfe2ef6429 Mon Sep 17 00:00:00 2001
From: Eric Huss <eric@huss.org>
Date: Sat, 30 Mar 2019 17:33:35 -0700
Subject: [PATCH 1/3] Add install-upgrade.

---
 src/bin/cargo/cli.rs                          |   1 +
 src/bin/cargo/commands/install.rs             |  11 +
 src/cargo/core/features.rs                    |   2 +
 src/cargo/ops/cargo_install.rs                | 309 ++++---
 src/cargo/ops/cargo_uninstall.rs              | 105 ++-
 .../ops/common_for_install_and_uninstall.rs   | 791 ++++++++++++++----
 src/doc/src/reference/unstable.md             |  29 +
 tests/testsuite/directory.rs                  |  28 +-
 tests/testsuite/install.rs                    |  66 +-
 tests/testsuite/install_upgrade.rs            | 718 ++++++++++++++++
 tests/testsuite/main.rs                       |   1 +
 tests/testsuite/support/mod.rs                |   3 +
 12 files changed, 1651 insertions(+), 413 deletions(-)
 create mode 100644 tests/testsuite/install_upgrade.rs

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..d802089673e 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<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);
-            }
-        }
-        // 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()));
-    }
+    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,
+        );
 
-    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 +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);
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::<Vec<_>>();
+    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::<BTreeSet<_>>();
+
+    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..5d0f62fae27 100644
--- a/src/cargo/ops/common_for_install_and_uninstall.rs
+++ b/src/cargo/ops/common_for_install_and_uninstall.rs
@@ -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};
-
-#[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.
+pub struct InstallTracker {
+    v1: CrateListingV1,
+    v2: CrateListingV2,
+    v1_lock: FileLock,
+    v2_lock: Option<FileLock>,
+    unstable_upgrade: bool,
+}
+
+#[derive(Default, Deserialize, Serialize)]
+struct CrateListingV2 {
+    installs: BTreeMap<PackageId, InstallInfo>,
+    /// Forwards compatibility.
+    #[serde(flatten)]
+    other: BTreeMap<String, serde_json::Value>,
 }
 
-#[derive(Deserialize, Serialize)]
-#[serde(deny_unknown_fields)]
-pub struct Empty {}
+#[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(Deserialize, Serialize)]
+#[derive(Default, Deserialize, Serialize)]
 pub struct CrateListingV1 {
     v1: BTreeMap<PackageId, BTreeSet<String>>,
 }
 
+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,
+        })
+    }
+
+    /// 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 {
-    pub fn v1(&self) -> &BTreeMap<PackageId, BTreeSet<String>> {
-        &self.v1
+    fn package_for_bin(&self, bin_name: &str) -> Option<PackageId> {
+        self.v1
+            .iter()
+            .find(|(_, bins)| bins.contains(bin_name))
+            .map(|(pkg_id, _)| *pkg_id)
     }
 
-    pub fn v1_mut(&mut self) -> &mut BTreeMap<PackageId, BTreeSet<String>> {
-        &mut self.v1
+    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")
-                    })?;
-
-                    match first {
-                        '<' | '>' | '=' | '^' | '~' => match v.parse::<VersionReq>() {
-                            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::<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
+                            )
+                        };
+
+                        // 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
-                            ),
-                        },
-                        _ => 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 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(),
-            }),
-        }
-    })()
-    .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<String> {
+    features.iter().cloned().collect()
 }
 
-pub fn metadata(config: &Config, root: &Filesystem) -> CargoResult<FileLock> {
-    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<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()
+        }
+    }
 }
diff --git a/src/doc/src/reference/unstable.md b/src/doc/src/reference/unstable.md
index 8322ab8aa51..37c8f13b586 100644
--- a/src/doc/src/reference/unstable.md
+++ b/src/doc/src/reference/unstable.md
@@ -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.
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..3104a07f1d8
--- /dev/null
+++ b/tests/testsuite/install_upgrade.rs
@@ -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();
+}
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 &macros {

From 9ecdfe0475568b9a9f048403d91bbb0f40463585 Mon Sep 17 00:00:00 2001
From: Eric Huss <eric@huss.org>
Date: Wed, 3 Apr 2019 18:05:55 -0700
Subject: [PATCH 2/3] install-upgrade: Fix bugs, more comments, review updates.

---
 src/cargo/ops/cargo_install.rs                |   6 +-
 .../ops/common_for_install_and_uninstall.rs   | 123 ++++++++++++------
 src/doc/src/reference/unstable.md             |   3 +
 tests/testsuite/install_upgrade.rs            |  74 ++++++++++-
 4 files changed, 164 insertions(+), 42 deletions(-)

diff --git a/src/cargo/ops/cargo_install.rs b/src/cargo/ops/cargo_install.rs
index d802089673e..880c41f089e 100644
--- a/src/cargo/ops/cargo_install.rs
+++ b/src/cargo/ops/cargo_install.rs
@@ -241,6 +241,9 @@ fn install_one(
         }
     };
 
+    // 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);
     }
@@ -394,8 +397,7 @@ fn install_one(
         try_install()
     };
 
-    if !no_track {
-        let mut tracker = tracker.unwrap();
+    if let Some(mut tracker) = tracker {
         tracker.mark_installed(
             pkg,
             &successful_bins,
diff --git a/src/cargo/ops/common_for_install_and_uninstall.rs b/src/cargo/ops/common_for_install_and_uninstall.rs
index 5d0f62fae27..84d67e9ed12 100644
--- a/src/cargo/ops/common_for_install_and_uninstall.rs
+++ b/src/cargo/ops/common_for_install_and_uninstall.rs
@@ -25,6 +25,9 @@ use crate::util::{FileLock, Filesystem, Freshness};
 ///
 /// 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,
@@ -33,6 +36,10 @@ pub struct InstallTracker {
     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<PackageId, InstallInfo>,
@@ -41,6 +48,14 @@ struct CrateListingV2 {
     other: BTreeMap<String, serde_json::Value>,
 }
 
+/// 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`.
@@ -68,6 +83,7 @@ struct InstallInfo {
     other: BTreeMap<String, serde_json::Value>,
 }
 
+/// Tracking information for the set of installed packages.
 #[derive(Default, Deserialize, Serialize)]
 pub struct CrateListingV1 {
     v1: BTreeMap<PackageId, BTreeSet<String>>,
@@ -102,23 +118,20 @@ impl InstallTracker {
         })?;
 
         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())
+            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(|| {
@@ -142,8 +155,14 @@ impl InstallTracker {
     ///
     /// 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).
+    /// (`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(
@@ -156,12 +175,17 @@ impl InstallTracker {
         _rustc: &str,
     ) -> CargoResult<(Freshness, BTreeMap<String, Option<PackageId>>)> {
         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));
         }
-        // If any duplicates are not tracked, then --force is required.
-        // If any duplicates are from a package with a different name, --force is required.
+        // 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.
         let matching_duplicates: Vec<PackageId> = duplicates
             .values()
             .filter_map(|v| match v {
@@ -170,9 +194,13 @@ impl InstallTracker {
             })
             .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| {
@@ -182,6 +210,8 @@ impl InstallTracker {
                     .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
@@ -190,13 +220,7 @@ impl InstallTracker {
                 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
+                    && info.is_up_to_date(opts, target, &exes)
             }) {
                 Ok((Freshness::Fresh, duplicates))
             } else {
@@ -218,6 +242,11 @@ impl InstallTracker {
         }
     }
 
+    /// 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,
@@ -312,7 +341,7 @@ impl CrateListingV1 {
                 other_bins.remove(bin);
             }
         }
-        // Remove empty metadata lines. If only BTreeMap had `retain`.
+        // Remove entries where `bins` is empty.
         let to_remove = self
             .v1
             .iter()
@@ -322,11 +351,10 @@ impl CrateListingV1 {
             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);
+            .or_insert_with(BTreeSet::new)
+            .append(&mut bins.clone());
     }
 
     fn remove(&mut self, pkg_id: PackageId, bins: &BTreeSet<String>) {
@@ -354,6 +382,11 @@ impl CrateListingV1 {
 }
 
 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 {
@@ -397,7 +430,7 @@ impl CrateListingV2 {
                 info.bins.remove(bin);
             }
         }
-        // Remove empty metadata lines. If only BTreeMap had `retain`.
+        // Remove entries where `bins` is empty.
         let to_remove = self
             .installs
             .iter()
@@ -408,9 +441,7 @@ impl CrateListingV2 {
         }
         // Add these bins.
         if let Some(info) = self.installs.get_mut(&pkg.package_id()) {
-            for bin in bins {
-                info.bins.remove(bin);
-            }
+            info.bins.append(&mut bins.clone());
             info.version_req = version_req;
             info.features = feature_set(&opts.features);
             info.all_features = opts.all_features;
@@ -454,7 +485,8 @@ impl CrateListingV2 {
         let mut file = lock.file();
         file.seek(SeekFrom::Start(0))?;
         file.set_len(0)?;
-        serde_json::to_writer(file, self)?;
+        let data = serde_json::to_string(self)?;
+        file.write_all(data.as_bytes())?;
         Ok(())
     }
 }
@@ -473,6 +505,23 @@ impl InstallInfo {
             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<String>,
+    ) -> 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.
diff --git a/src/doc/src/reference/unstable.md b/src/doc/src/reference/unstable.md
index 37c8f13b586..9a740d75881 100644
--- a/src/doc/src/reference/unstable.md
+++ b/src/doc/src/reference/unstable.md
@@ -258,6 +258,9 @@ 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/install_upgrade.rs b/tests/testsuite/install_upgrade.rs
index 3104a07f1d8..2e4dcef43f6 100644
--- a/tests/testsuite/install_upgrade.rs
+++ b/tests/testsuite/install_upgrade.rs
@@ -1,3 +1,6 @@
+use cargo::core::PackageId;
+use std::collections::BTreeSet;
+use std::env;
 use std::fs;
 use std::path::PathBuf;
 use std::sync::atomic::{AtomicUsize, Ordering};
@@ -27,6 +30,10 @@ 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()
 }
@@ -56,6 +63,49 @@ fn installed_process(name: &str) -> Execs {
     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<String> = 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<String> = 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<String> = 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.
@@ -77,6 +127,7 @@ fn registry_upgrade() {
         )
         .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()
@@ -109,18 +160,22 @@ fn registry_upgrade() {
         .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[..]")
@@ -139,9 +194,8 @@ fn uninstall() {
         .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);
+    let v1_table = load_crates1();
+    assert_eq!(v1_table.get("v1").unwrap().as_table().unwrap().len(), 0);
 }
 
 #[test]
@@ -164,6 +218,7 @@ fn upgrade_force() {
 ",
         )
         .run();
+    validate_trackers("foo", "1.0.0", &["foo"]);
 }
 
 #[test]
@@ -244,20 +299,24 @@ fn supports_multiple_binary_names() {
     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();
@@ -287,6 +346,7 @@ fn v1_already_installed_dirty() {
         .with_stderr_contains("[REPLACING] [..]/foo[EXE]")
         .masquerade_as_nightly_cargo()
         .run();
+    validate_trackers("foo", "1.0.1", &["foo"]);
 }
 
 #[test]
@@ -385,6 +445,7 @@ fn change_bin_sets_rebuilds() {
     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]`)")
@@ -392,6 +453,7 @@ fn change_bin_sets_rebuilds() {
     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]`)")
@@ -402,6 +464,7 @@ fn change_bin_sets_rebuilds() {
     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]
@@ -442,9 +505,11 @@ fn v2_syncs() {
     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();
@@ -452,6 +517,7 @@ fn v2_syncs() {
     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(
             "\
@@ -469,6 +535,7 @@ two v1.0.0:
         .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[..]")
@@ -481,6 +548,7 @@ two v1.0.0:
     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()

From 5a9ff8a62192dbbc068ad9a59368d3fc221b0dff Mon Sep 17 00:00:00 2001
From: Eric Huss <eric@huss.org>
Date: Fri, 5 Apr 2019 10:48:23 -0700
Subject: [PATCH 3/3] install-upgrade: Add some more comments.

---
 src/cargo/ops/cargo_install.rs                    | 2 ++
 src/cargo/ops/common_for_install_and_uninstall.rs | 7 +++++++
 2 files changed, 9 insertions(+)

diff --git a/src/cargo/ops/cargo_install.rs b/src/cargo/ops/cargo_install.rs
index 880c41f089e..7591b148a07 100644
--- a/src/cargo/ops/cargo_install.rs
+++ b/src/cargo/ops/cargo_install.rs
@@ -278,6 +278,8 @@ fn install_one(
         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()?;
diff --git a/src/cargo/ops/common_for_install_and_uninstall.rs b/src/cargo/ops/common_for_install_and_uninstall.rs
index 84d67e9ed12..c581772d457 100644
--- a/src/cargo/ops/common_for_install_and_uninstall.rs
+++ b/src/cargo/ops/common_for_install_and_uninstall.rs
@@ -186,6 +186,13 @@ impl InstallTracker {
         //
         // 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<PackageId> = duplicates
             .values()
             .filter_map(|v| match v {