mirror of
https://github.com/rust-lang/cargo
synced 2024-08-25 10:35:53 +00:00
install-upgrade: Fix bugs, more comments, review updates.
This commit is contained in:
parent
e023a6672b
commit
9ecdfe0475
|
@ -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,
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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()
|
||||
|
|
Loading…
Reference in a new issue