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::compiler::Freshness;
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::{Config, ToSemver};
use crate::util::{FileLock, Filesystem};
pub struct InstallTracker {
v1: CrateListingV1,
v2: CrateListingV2,
v1_lock: FileLock,
v2_lock: FileLock,
}
#[derive(Default, Deserialize, Serialize)]
struct CrateListingV2 {
installs: BTreeMap<PackageId, InstallInfo>,
#[serde(flatten)]
other: BTreeMap<String, serde_json::Value>,
}
#[derive(Debug, Deserialize, Serialize)]
struct InstallInfo {
version_req: Option<String>,
bins: BTreeSet<String>,
features: BTreeSet<String>,
all_features: bool,
no_default_features: bool,
profile: String,
target: Option<String>,
rustc: Option<String>,
#[serde(flatten)]
other: BTreeMap<String, serde_json::Value>,
}
#[derive(Default, Deserialize, Serialize)]
pub struct CrateListingV1 {
v1: BTreeMap<PackageId, BTreeSet<String>>,
}
impl InstallTracker {
pub fn load(config: &Config, root: &Filesystem) -> CargoResult<InstallTracker> {
let v1_lock = root.open_rw(Path::new(".crates.toml"), config, "crate metadata")?;
let v2_lock = root.open_rw(Path::new(".crates2.json"), config, "crate metadata")?;
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<_> {
let mut contents = String::new();
v2_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)
})()
.chain_err(|| {
format_err!(
"failed to parse crate metadata at `{}`",
v2_lock.path().to_string_lossy()
)
})?;
Ok(InstallTracker {
v1,
v2,
v1_lock,
v2_lock,
})
}
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));
}
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 matching_duplicates.len() == duplicates.len() {
let source_id = pkg.package_id().source_id();
if source_id.is_path() {
return Ok((Freshness::Dirty, duplicates));
}
let is_up_to_date = |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.is_up_to_date(opts, target, &exes)
};
if matching_duplicates.iter().all(is_up_to_date) {
Ok((Freshness::Fresh, duplicates))
} else {
Ok((Freshness::Dirty, duplicates))
}
} else {
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 {
let p = self.v2.package_for_bin(name);
Some((name.clone(), p))
}
})
.collect()
}
pub fn mark_installed(
&mut self,
package: &Package,
bins: &BTreeSet<String>,
version_req: Option<String>,
opts: &CompileOptions<'_>,
target: &str,
rustc: &str,
) {
self.v2
.mark_installed(package, bins, version_req, opts, target, rustc);
self.v1.mark_installed(package, bins);
}
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()
)
})?;
self.v2.save(&self.v2_lock).chain_err(|| {
format_err!(
"failed to write crate metadata at `{}`",
self.v2_lock.path().to_string_lossy()
)
})?;
Ok(())
}
pub fn all_installed_bins(&self) -> impl Iterator<Item = (&PackageId, &BTreeSet<String>)> {
self.v1.v1.iter()
}
pub fn installed_bins(&self, pkg_id: PackageId) -> Option<&BTreeSet<String>> {
self.v1.v1.get(&pkg_id)
}
pub fn remove(&mut self, pkg_id: PackageId, bins: &BTreeSet<String>) {
self.v1.remove(pkg_id, bins);
self.v2.remove(pkg_id, bins);
}
}
impl CrateListingV1 {
fn mark_installed(&mut self, pkg: &Package, bins: &BTreeSet<String>) {
for other_bins in self.v1.values_mut() {
for bin in bins {
other_bins.remove(bin);
}
}
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);
}
self.v1
.entry(pkg.package_id())
.or_insert_with(BTreeSet::new)
.append(&mut bins.clone());
}
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<()> {
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));
}
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: &str,
rustc: &str,
) {
for info in &mut self.installs.values_mut() {
for bin in bins {
info.bins.remove(bin);
}
}
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);
}
if let Some(info) = self.installs.get_mut(&pkg.package_id()) {
info.bins.append(&mut bins.clone());
info.version_req = version_req;
info.features = feature_set(&opts.features);
info.all_features = opts.all_features;
info.no_default_features = opts.no_default_features;
info.profile = opts.build_config.profile_name().to_string();
info.target = Some(target.to_string());
info.rustc = Some(rustc.to_string());
} 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: opts.build_config.profile_name().to_string(),
target: Some(target.to_string()),
rustc: Some(rustc.to_string()),
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)?;
let data = serde_json::to_string(self)?;
file.write_all(data.as_bytes())?;
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(),
}
}
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 == opts.build_config.profile_name()
&& (self.target.is_none() || self.target.as_ref().map(|t| t.as_ref()) == Some(target))
&& &self.bins == exes
}
}
pub fn resolve_root(flag: Option<&str>, config: &Config) -> CargoResult<Filesystem> {
let config_root = config.get_path("install.root")?;
Ok(flag
.map(PathBuf::from)
.or_else(|| env::var_os("CARGO_INSTALL_ROOT").map(PathBuf::from))
.or_else(move || config_root.map(|v| v.val))
.map(Filesystem::new)
.unwrap_or_else(|| config.home().clone()))
}
pub fn path_source(source_id: SourceId, config: &Config) -> CargoResult<PathSource<'_>> {
let path = source_id
.url()
.to_file_path()
.map_err(|()| format_err!("path sources must have a valid path"))?;
Ok(PathSource::new(&path, source_id, config))
}
pub fn select_pkg<'a, T>(
mut source: T,
name: Option<&str>,
vers: Option<&str>,
config: &Config,
needs_update: bool,
list_all: &mut dyn FnMut(&mut T) -> CargoResult<Vec<Package>>,
) -> CargoResult<Package>
where
T: Source + 'a,
{
let _lock = config.acquire_package_cache_lock()?;
if needs_update {
source.update()?;
}
if let Some(name) = name {
let vers = if let Some(v) = vers {
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 = format!(
"the `--vers` provided, `{}`, is \
not a valid semver version: {}\n",
v, e
);
if v.parse::<VersionReq>().is_ok() {
msg.push_str(&format!(
"\nif you want to specify semver range, \
add an explicit qualifier, like ^{}",
v
));
}
bail!(msg);
}
}
}
} else {
None
};
let vers = vers.as_ref().map(|s| &**s);
let vers_spec = if vers.is_none() && source.source_id().is_registry() {
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)
}
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());
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(", ")
)
}
}
}
fn one<I, F>(mut i: I, f: F) -> CargoResult<Option<I::Item>>
where
I: Iterator,
F: FnOnce(Vec<I::Item>) -> String,
{
match (i.next(), i.next()) {
(Some(i1), Some(i2)) => {
let mut v = vec![i1, i2];
v.extend(i);
Err(format_err!("{}", f(v)))
}
(Some(i), None) => Ok(Some(i)),
(None, _) => Ok(None),
}
}
fn feature_set(features: &[String]) -> BTreeSet<String> {
features.iter().cloned().collect()
}
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 {
all_targets: true, ..
} => pkg
.targets()
.iter()
.filter(|target| target.is_executable())
.map(|target| to_exe(target.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()
}
}
}