use std::{
collections::{BTreeMap, HashMap},
ffi::OsStr,
ops,
path::{Path, PathBuf},
};
use anyhow::{Context as _, Result, format_err};
use cargo_config2::Config;
use serde_json::{Map, Value};
use crate::{cargo, cli::Args, fs, process::ProcessBuilder, restore, term};
type Object = Map<String, Value>;
type ParseResult<T> = Result<T, &'static str>;
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[repr(transparent)]
pub(crate) struct PackageId {
index: usize,
}
pub(crate) struct Metadata {
pub(crate) cargo_version: u32,
pub(crate) packages: Box<[Package]>,
pub(crate) workspace_members: Box<[PackageId]>,
pub(crate) resolve: Resolve,
pub(crate) workspace_root: PathBuf,
}
impl Metadata {
pub(crate) fn new(
manifest_path: Option<&str>,
cargo: &OsStr,
mut cargo_version: u32,
args: &Args,
restore: &mut restore::Manager,
) -> Result<Self> {
let stable_cargo_version =
cargo::version(cmd!("rustup", "run", "stable", "cargo")).map_or(0, |v| v.minor);
let config;
let include_deps_features = if args.include_deps_features {
config = Config::load()?;
let targets = config.build_target_for_cli(&args.target)?;
let host = config.host_triple()?;
Some((targets, host))
} else {
None
};
let mut cmd;
let append_metadata_args = |cmd: &mut ProcessBuilder<'_>| {
cmd.arg("metadata");
cmd.arg("--format-version=1");
if let Some(manifest_path) = manifest_path {
cmd.arg("--manifest-path");
cmd.arg(manifest_path);
}
if let Some((targets, host)) = &include_deps_features {
if targets.is_empty() {
cmd.arg("--filter-platform");
cmd.arg(host);
} else {
for target in targets {
cmd.arg("--filter-platform");
cmd.arg(target);
}
}
} else {
cmd.arg("--no-deps");
}
};
let json = if stable_cargo_version > cargo_version {
cmd = cmd!(cargo, "metadata", "--format-version=1", "--no-deps");
if let Some(manifest_path) = manifest_path {
cmd.arg("--manifest-path");
cmd.arg(manifest_path);
}
let no_deps_raw = cmd.read()?;
let no_deps: Object = serde_json::from_str(&no_deps_raw)
.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) = manifest_path {
cmd.arg("--manifest-path");
cmd.arg(manifest_path);
}
cmd.run_with_output()?;
}
let guard = term::verbose::scoped(false);
restore.register_always(fs::read(&lockfile)?, lockfile);
cmd = cmd!("rustup", "run", "stable", "cargo");
append_metadata_args(&mut cmd);
let json = cmd.read();
restore.restore_last()?;
drop(guard);
match json {
Ok(json) => {
cargo_version = stable_cargo_version;
json
}
Err(_e) => {
if include_deps_features.is_some() {
cmd = cmd!(cargo);
append_metadata_args(&mut cmd);
cmd.read()?
} else {
no_deps_raw
}
}
}
} else {
cmd = cmd!(cargo);
append_metadata_args(&mut cmd);
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 `{s}` field from metadata"))
}
fn from_obj(mut map: Object, cargo_version: u32) -> ParseResult<Self> {
let raw_packages = map.remove_array("packages")?;
let mut packages = Vec::with_capacity(raw_packages.len());
let mut pkg_id_map = HashMap::with_capacity(raw_packages.len());
for (i, pkg) in raw_packages.into_iter().enumerate() {
let (id, pkg) = Package::from_value(pkg, cargo_version)?;
pkg_id_map.insert(id, i);
packages.push(pkg);
}
let workspace_members = map
.remove_array("workspace_members")?
.into_iter()
.map(|v| -> ParseResult<_> {
let id: String = into_string(v).ok_or("workspace_members")?;
Ok(PackageId { index: pkg_id_map[&id] })
})
.collect::<Result<_, _>>()?;
let resolve = match map.remove_nullable("resolve", into_object)? {
Some(resolve) => Resolve::from_obj(resolve, &pkg_id_map, cargo_version)?,
None => Resolve { nodes: HashMap::default() },
};
Ok(Self {
cargo_version,
packages: packages.into_boxed_slice(),
workspace_members,
resolve,
workspace_root: map.remove_string("workspace_root")?,
})
}
}
impl ops::Index<PackageId> for Metadata {
type Output = Package;
#[inline]
fn index(&self, index: PackageId) -> &Self::Output {
&self.packages[index.index]
}
}
pub(crate) struct Resolve {
pub(crate) nodes: HashMap<PackageId, Node>,
}
impl Resolve {
fn from_obj(
mut map: Object,
pkg_id_map: &HashMap<String, usize>,
cargo_version: u32,
) -> ParseResult<Self> {
let nodes = map
.remove_array("nodes")?
.into_iter()
.map(|v| -> ParseResult<_> {
let (id, node) = Node::from_value(v, pkg_id_map, cargo_version)?;
Ok((PackageId { index: pkg_id_map[&id] }, node))
})
.collect::<Result<_, _>>()?;
Ok(Self { nodes })
}
}
pub(crate) struct Node {
pub(crate) deps: Box<[NodeDep]>,
}
impl Node {
fn from_value(
mut value: Value,
pkg_id_map: &HashMap<String, usize>,
cargo_version: u32,
) -> ParseResult<(String, 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, pkg_id_map, cargo_version))
.collect::<Result<_, _>>()?
} else {
Box::default()
},
}))
}
}
pub(crate) struct NodeDep {
pub(crate) pkg: PackageId,
pub(crate) dep_kinds: Box<[DepKindInfo]>,
}
impl NodeDep {
fn from_value(
mut value: Value,
pkg_id_map: &HashMap<String, usize>,
cargo_version: u32,
) -> ParseResult<Self> {
let map = value.as_object_mut().ok_or("deps")?;
let id: String = map.remove_string("pkg")?;
Ok(Self {
pkg: PackageId { index: pkg_id_map[&id] },
dep_kinds: if cargo_version >= 41 {
map.remove_array("dep_kinds")?
.into_iter()
.map(DepKindInfo::from_value)
.collect::<Result<_, _>>()?
} else {
Box::default()
},
})
}
}
pub(crate) struct DepKindInfo {
pub(crate) kind: Option<Box<str>>,
pub(crate) target: Option<Box<str>>,
}
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: Box<str>,
pub(crate) dependencies: Box<[Dependency]>,
pub(crate) features: BTreeMap<Box<str>, Box<[Box<str>]>>,
pub(crate) manifest_path: Box<Path>,
pub(crate) publish: bool,
pub(crate) rust_version: Option<Box<str>>,
}
impl Package {
fn from_value(mut value: Value, cargo_version: u32) -> ParseResult<(String, 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::<Box<str>>).collect::<Option<_>>()
})
.map(|v| (k.into_boxed_str(), v))
})
.collect::<Option<_>>()
.ok_or("features")?,
manifest_path: map.remove_string::<PathBuf>("manifest_path")?.into_boxed_path(),
publish: if cargo_version >= 39 {
map.remove_nullable("publish", into_array)?.is_none_or(|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: Box<str>,
pub(crate) optional: bool,
pub(crate) rename: Option<Box<str>>,
}
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)
}
}