install-upgrade: Fix bugs, more comments, review updates.

This commit is contained in:
Eric Huss 2019-04-03 18:05:55 -07:00
parent e023a6672b
commit 9ecdfe0475
4 changed files with 164 additions and 42 deletions

View file

@ -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,

View file

@ -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.

View file

@ -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.

View file

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