use std::{
collections::{BTreeMap, HashMap},
ffi::OsStr,
path::{Path, PathBuf},
rc::Rc,
};
use anyhow::{format_err, Context as _, Result};
use serde_json::{Map, Value};
use crate::{cargo, cli::Args, fs, restore, term};
type Object = Map<String, Value>;
type ParseResult<T> = Result<T, &'static str>;
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub(crate) struct PackageId {
repr: Rc<str>,
}
impl From<String> for PackageId {
fn from(repr: String) -> Self {
Self { repr: repr.into() }
}
}
pub(crate) struct Metadata {
pub(crate) cargo_version: u32,
pub(crate) packages: HashMap<PackageId, Package>,
pub(crate) workspace_members: Vec<PackageId>,
pub(crate) resolve: Resolve,
pub(crate) workspace_root: PathBuf,
}
impl Metadata {
pub(crate) fn new(args: &Args, cargo: &OsStr, restore: &restore::Manager) -> Result<Self> {
let mut cargo_version = cargo::minor_version(cmd!(cargo))
.map_err(|e| warn!("unable to determine cargo version: {:#}", e))
.unwrap_or(0);
let stable_cargo_version = cargo::minor_version(cmd!("cargo", "+stable")).unwrap_or(0);
let mut cmd;
let json = if stable_cargo_version > cargo_version {
cmd = cmd!(cargo, "metadata", "--format-version=1", "--no-deps");
if let Some(manifest_path) = &args.manifest_path {
cmd.arg("--manifest-path");
cmd.arg(manifest_path);
}
let no_deps: Object = serde_json::from_str(&cmd.read()?)
.with_context(|| format!("failed to parse output from {}", cmd))?;
let lockfile =
Path::new(no_deps["workspace_root"].as_str().unwrap()).join("Cargo.lock");
if !lockfile.exists() {
let mut cmd = cmd!(cargo, "generate-lockfile");
if let Some(manifest_path) = &args.manifest_path {
cmd.arg("--manifest-path");
cmd.arg(manifest_path);
}
cmd.run_with_output()?;
}
let guard = term::verbose::scoped(false);
let mut handle = restore.set(&fs::read_to_string(&lockfile)?, lockfile);
cmd = cmd!("cargo", "+stable", "metadata", "--format-version=1");
if let Some(manifest_path) = &args.manifest_path {
cmd.arg("--manifest-path");
cmd.arg(manifest_path);
}
let json = cmd.read();
handle.close()?;
drop(guard);
match json {
Ok(json) => {
cargo_version = stable_cargo_version;
json
}
Err(_e) => {
cmd = cmd!(cargo, "metadata", "--format-version=1");
if let Some(manifest_path) = &args.manifest_path {
cmd.arg("--manifest-path");
cmd.arg(manifest_path);
}
cmd.read()?
}
}
} else {
cmd = cmd!(cargo, "metadata", "--format-version=1");
if let Some(manifest_path) = &args.manifest_path {
cmd.arg("--manifest-path");
cmd.arg(manifest_path);
}
cmd.read()?
};
let map = serde_json::from_str(&json)
.with_context(|| format!("failed to parse output from {}", cmd))?;
Self::from_obj(map, cargo_version)
.map_err(|s| format_err!("failed to parse `{}` field from metadata", s))
}
fn from_obj(mut map: Object, cargo_version: u32) -> ParseResult<Self> {
let workspace_members: Vec<_> = map
.remove_array("workspace_members")?
.into_iter()
.map(|v| into_string(v).ok_or("workspace_members"))
.collect::<Result<_, _>>()?;
Ok(Self {
cargo_version,
packages: map
.remove_array("packages")?
.into_iter()
.map(|v| Package::from_value(v, cargo_version))
.collect::<Result<_, _>>()?,
workspace_members,
resolve: Resolve::from_obj(map.remove_object("resolve")?, cargo_version)?,
workspace_root: map.remove_string("workspace_root")?,
})
}
}
pub(crate) struct Resolve {
pub(crate) nodes: HashMap<PackageId, Node>,
pub(crate) root: Option<PackageId>,
}
impl Resolve {
fn from_obj(mut map: Object, cargo_version: u32) -> ParseResult<Self> {
Ok(Self {
nodes: map
.remove_array("nodes")?
.into_iter()
.map(|v| Node::from_value(v, cargo_version))
.collect::<Result<_, _>>()?,
root: map.remove_nullable("root", into_string)?,
})
}
}
pub(crate) struct Node {
pub(crate) deps: Vec<NodeDep>,
}
impl Node {
fn from_value(mut value: Value, cargo_version: u32) -> ParseResult<(PackageId, Self)> {
let map = value.as_object_mut().ok_or("nodes")?;
let id = map.remove_string("id")?;
Ok((id, Self {
deps: if cargo_version >= 30 {
map.remove_array("deps")?
.into_iter()
.map(|v| NodeDep::from_value(v, cargo_version))
.collect::<Result<_, _>>()?
} else {
Vec::new()
},
}))
}
}
pub(crate) struct NodeDep {
pub(crate) pkg: PackageId,
pub(crate) dep_kinds: Vec<DepKindInfo>,
}
impl NodeDep {
fn from_value(mut value: Value, cargo_version: u32) -> ParseResult<Self> {
let map = value.as_object_mut().ok_or("deps")?;
Ok(Self {
pkg: map.remove_string("pkg")?,
dep_kinds: if cargo_version >= 41 {
map.remove_array("dep_kinds")?
.into_iter()
.map(DepKindInfo::from_value)
.collect::<Result<_, _>>()?
} else {
Vec::new()
},
})
}
}
pub(crate) struct DepKindInfo {
pub(crate) kind: Option<String>,
pub(crate) target: Option<String>,
}
impl DepKindInfo {
fn from_value(mut value: Value) -> ParseResult<Self> {
let map = value.as_object_mut().ok_or("dep_kinds")?;
Ok(Self {
kind: map.remove_nullable("kind", into_string)?,
target: map.remove_nullable("target", into_string)?,
})
}
}
pub(crate) struct Package {
pub(crate) name: String,
pub(crate) dependencies: Vec<Dependency>,
pub(crate) features: BTreeMap<String, Vec<String>>,
pub(crate) manifest_path: PathBuf,
pub(crate) publish: bool,
#[allow(dead_code)]
pub(crate) rust_version: Option<String>,
}
impl Package {
fn from_value(mut value: Value, cargo_version: u32) -> ParseResult<(PackageId, Self)> {
let map = value.as_object_mut().ok_or("packages")?;
let id = map.remove_string("id")?;
Ok((id, Self {
name: map.remove_string("name")?,
dependencies: map
.remove_array("dependencies")?
.into_iter()
.map(Dependency::from_value)
.collect::<Result<_, _>>()?,
features: map
.remove_object("features")?
.into_iter()
.map(|(k, v)| {
into_array(v)
.and_then(|v| v.into_iter().map(into_string).collect::<Option<_>>())
.map(|v| (k, v))
})
.collect::<Option<_>>()
.ok_or("features")?,
manifest_path: map.remove_string("manifest_path")?,
publish: if cargo_version >= 39 {
map.remove_nullable("publish", into_array)?.map_or(true, |a| !a.is_empty())
} else {
true
},
rust_version: if cargo_version >= 58 {
map.remove_nullable("rust_version", into_string)?
} else {
None
},
}))
}
pub(crate) fn optional_deps(&self) -> impl Iterator<Item = &str> + '_ {
self.dependencies.iter().filter_map(Dependency::as_feature)
}
}
pub(crate) struct Dependency {
pub(crate) name: String,
pub(crate) optional: bool,
pub(crate) rename: Option<String>,
}
impl Dependency {
fn from_value(mut value: Value) -> ParseResult<Self> {
let map = value.as_object_mut().ok_or("dependencies")?;
Ok(Self {
name: map.remove_string("name")?,
optional: map.get("optional").and_then(Value::as_bool).ok_or("optional")?,
rename: map.remove_nullable("rename", into_string)?,
})
}
pub(crate) fn as_feature(&self) -> Option<&str> {
if self.optional {
Some(self.rename.as_ref().unwrap_or(&self.name))
} else {
None
}
}
}
#[allow(clippy::option_option)]
fn allow_null<T>(value: Value, f: impl FnOnce(Value) -> Option<T>) -> Option<Option<T>> {
if value.is_null() {
Some(None)
} else {
f(value).map(Some)
}
}
fn into_string<S: From<String>>(value: Value) -> Option<S> {
if let Value::String(string) = value {
Some(string.into())
} else {
None
}
}
fn into_array(value: Value) -> Option<Vec<Value>> {
if let Value::Array(array) = value {
Some(array)
} else {
None
}
}
fn into_object(value: Value) -> Option<Object> {
if let Value::Object(object) = value {
Some(object)
} else {
None
}
}
trait ObjectExt {
fn remove_string<S: From<String>>(&mut self, key: &'static str) -> ParseResult<S>;
fn remove_array(&mut self, key: &'static str) -> ParseResult<Vec<Value>>;
fn remove_object(&mut self, key: &'static str) -> ParseResult<Object>;
fn remove_nullable<T>(
&mut self,
key: &'static str,
f: impl FnOnce(Value) -> Option<T>,
) -> ParseResult<Option<T>>;
}
impl ObjectExt for Object {
fn remove_string<S: From<String>>(&mut self, key: &'static str) -> ParseResult<S> {
self.remove(key).and_then(into_string).ok_or(key)
}
fn remove_array(&mut self, key: &'static str) -> ParseResult<Vec<Value>> {
self.remove(key).and_then(into_array).ok_or(key)
}
fn remove_object(&mut self, key: &'static str) -> ParseResult<Object> {
self.remove(key).and_then(into_object).ok_or(key)
}
fn remove_nullable<T>(
&mut self,
key: &'static str,
f: impl FnOnce(Value) -> Option<T>,
) -> ParseResult<Option<T>> {
self.remove(key).and_then(|v| allow_null(v, f)).ok_or(key)
}
}