diff --git a/.deny.toml b/.deny.toml index 84147b05..854fe1d9 100644 --- a/.deny.toml +++ b/.deny.toml @@ -19,10 +19,8 @@ build.allow-build-scripts = [ { name = "libc" }, { name = "proc-macro2" }, { name = "rustix" }, # via is-terminal - { name = "semver" }, # via cargo_metadata { name = "serde_json" }, { name = "serde" }, - { name = "thiserror" }, # via cargo_metadata { name = "winapi-i686-pc-windows-gnu" }, { name = "winapi-x86_64-pc-windows-gnu" }, { name = "winapi" }, diff --git a/Cargo.toml b/Cargo.toml index 0cafdcf5..19398861 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,8 +25,7 @@ resolver = "2" [dependencies] anyhow = "1.0.47" -camino = "1.0.3" -cargo_metadata = "0.18" +camino = { version = "1.0.3", features = ["serde1"] } cargo-config2 = "0.1.5" duct = "0.13.1" fs-err = "2.5" diff --git a/src/cargo.rs b/src/cargo.rs index a8a6234d..8714a314 100644 --- a/src/cargo.rs +++ b/src/cargo.rs @@ -10,13 +10,14 @@ use crate::{ cli::{ManifestOptions, Subcommand}, context::Context, env, + metadata::Metadata, process::ProcessBuilder, }; pub(crate) struct Workspace { pub(crate) name: String, pub(crate) config: Config, - pub(crate) metadata: cargo_metadata::Metadata, + pub(crate) metadata: Metadata, pub(crate) current_manifest: Utf8PathBuf, pub(crate) target_dir: Utf8PathBuf, @@ -44,7 +45,7 @@ impl Workspace { // Metadata and config let config = Config::load()?; let current_manifest = package_root(config.cargo(), options.manifest_path.as_deref())?; - let metadata = metadata(config.cargo(), ¤t_manifest)?; + let metadata = Metadata::new(current_manifest.as_std_path(), config.cargo())?; let mut target_for_config = config.build_target_for_config(target)?; if target_for_config.len() != 1 { bail!("cargo-llvm-cov doesn't currently supports multi-target builds: {target_for_config:?}"); @@ -188,19 +189,6 @@ fn locate_project(cargo: &OsStr) -> Result { cmd!(cargo, "locate-project", "--message-format", "plain").read() } -// https://doc.rust-lang.org/nightly/cargo/commands/cargo-metadata.html -fn metadata(cargo: &OsStr, manifest_path: &Utf8Path) -> Result { - let mut cmd = cmd!( - cargo, - "metadata", - "--format-version=1", - "--no-deps", - "--manifest-path", - manifest_path - ); - serde_json::from_str(&cmd.read()?).with_context(|| format!("failed to parse output from {cmd}")) -} - // https://doc.rust-lang.org/nightly/cargo/commands/cargo-test.html // https://doc.rust-lang.org/nightly/cargo/commands/cargo-run.html pub(crate) fn test_or_run_args(cx: &Context, cmd: &mut ProcessBuilder) { diff --git a/src/clean.rs b/src/clean.rs index 72450b04..b07c2739 100644 --- a/src/clean.rs +++ b/src/clean.rs @@ -8,7 +8,6 @@ use std::path::Path; use anyhow::Result; use camino::Utf8Path; -use cargo_metadata::PackageId; use walkdir::WalkDir; use crate::{ @@ -16,6 +15,7 @@ use crate::{ cli::{self, Args, ManifestOptions}, context::Context, fs, + metadata::PackageId, regex_vec::{RegexVec, RegexVecBuilder}, term, }; @@ -56,7 +56,7 @@ pub(crate) fn clean_partial(cx: &Context) -> Result<()> { .workspace_members .included .iter() - .flat_map(|id| ["--package", &cx.ws.metadata[id].name]) + .flat_map(|id| ["--package", &cx.ws.metadata.packages[id].name]) .collect(); let mut cmd = cx.cargo(); cmd.arg("clean").args(package_args); @@ -77,7 +77,7 @@ fn clean_ws( clean_ws_inner(ws, pkg_ids, verbose != 0)?; let package_args: Vec<_> = - pkg_ids.iter().flat_map(|id| ["--package", &ws.metadata[id].name]).collect(); + pkg_ids.iter().flat_map(|id| ["--package", &ws.metadata.packages[id].name]).collect(); let mut args_set = vec![vec![]]; if ws.target_dir.join("release").exists() { args_set.push(vec!["--release"]); @@ -127,7 +127,7 @@ fn clean_ws_inner(ws: &Workspace, pkg_ids: &[PackageId], verbose: bool) -> Resul fn pkg_hash_re(ws: &Workspace, pkg_ids: &[PackageId]) -> RegexVec { let mut re = RegexVecBuilder::new("^(lib)?(", ")(-[0-9a-f]{7,})?$"); for id in pkg_ids { - re.or(&ws.metadata[id].name.replace('-', "(-|_)")); + re.or(&ws.metadata.packages[id].name.replace('-', "(-|_)")); } re.build().unwrap() } diff --git a/src/context.rs b/src/context.rs index 4fd5d7bb..1485b2ae 100644 --- a/src/context.rs +++ b/src/context.rs @@ -8,12 +8,12 @@ use std::{ use anyhow::{bail, Result}; use camino::Utf8PathBuf; -use cargo_metadata::PackageId; use crate::{ cargo::Workspace, cli::{self, Args, Subcommand}, env, + metadata::{Metadata, PackageId}, process::ProcessBuilder, regex_vec::{RegexVec, RegexVecBuilder}, term, @@ -221,7 +221,7 @@ impl Context { fn pkg_hash_re(ws: &Workspace, pkg_ids: &[PackageId]) -> RegexVec { let mut re = RegexVecBuilder::new("^(", ")-[0-9a-f]+$"); for id in pkg_ids { - re.or(&ws.metadata[id].name); + re.or(&ws.metadata.packages[id].name); } re.build().unwrap() } @@ -232,18 +232,14 @@ pub(crate) struct WorkspaceMembers { } impl WorkspaceMembers { - fn new( - exclude: &[String], - exclude_from_report: &[String], - metadata: &cargo_metadata::Metadata, - ) -> Self { + fn new(exclude: &[String], exclude_from_report: &[String], metadata: &Metadata) -> Self { let mut excluded = vec![]; let mut included = vec![]; if !exclude.is_empty() || !exclude_from_report.is_empty() { for id in &metadata.workspace_members { // --exclude flag doesn't handle `name:version` format - if exclude.contains(&metadata[id].name) - || exclude_from_report.contains(&metadata[id].name) + if exclude.contains(&metadata.packages[id].name) + || exclude_from_report.contains(&metadata.packages[id].name) { excluded.push(id.clone()); } else { diff --git a/src/main.rs b/src/main.rs index 6d648d6b..0e2b0203 100644 --- a/src/main.rs +++ b/src/main.rs @@ -28,6 +28,7 @@ use crate::{ cargo::Workspace, cli::{Args, ShowEnvOptions, Subcommand}, context::Context, + metadata::Metadata, process::ProcessBuilder, regex_vec::{RegexVec, RegexVecBuilder}, term::Coloring, @@ -46,6 +47,7 @@ mod context; mod demangle; mod env; mod fs; +mod metadata; mod regex_vec; fn main() { @@ -720,8 +722,8 @@ fn object_files(cx: &Context) -> Result> { trybuild_target_dir.push("debug"); if trybuild_target_dir.is_dir() { let mut trybuild_targets = vec![]; - for metadata in trybuild_metadata(&cx.ws.metadata.target_directory)? { - for package in metadata.packages { + for metadata in trybuild_metadata(&cx.ws, &cx.ws.metadata.target_directory)? { + for package in metadata.packages.into_values() { for target in package.targets { trybuild_targets.push(target.name); } @@ -769,7 +771,7 @@ impl Targets { let mut packages = BTreeSet::new(); let mut targets = BTreeSet::new(); for id in &ws.metadata.workspace_members { - let pkg = &ws.metadata[id]; + let pkg = &ws.metadata.packages[id]; packages.insert(pkg.name.clone()); for t in &pkg.targets { targets.insert(t.name.clone()); @@ -792,7 +794,7 @@ impl Targets { /// Collects metadata for packages generated by trybuild. If the trybuild test /// directory is not found, it returns an empty vector. -fn trybuild_metadata(target_dir: &Utf8Path) -> Result> { +fn trybuild_metadata(ws: &Workspace, target_dir: &Utf8Path) -> Result> { // https://github.com/dtolnay/trybuild/pull/219 let mut trybuild_dir = target_dir.join("tests").join("trybuild"); if !trybuild_dir.is_dir() { @@ -803,13 +805,11 @@ fn trybuild_metadata(target_dir: &Utf8Path) -> Result Vec { .workspace_members .excluded .iter() - .map(|id| cx.ws.metadata[id].manifest_path.parent().unwrap()) + .map(|id| cx.ws.metadata.packages[id].manifest_path.parent().unwrap()) .collect(); let included = cx .workspace_members .included .iter() - .map(|id| cx.ws.metadata[id].manifest_path.parent().unwrap()); + .map(|id| cx.ws.metadata.packages[id].manifest_path.parent().unwrap()); let mut excluded_path = vec![]; let mut contains: HashMap<&Utf8Path, Vec<_>> = HashMap::new(); for included in included { diff --git a/src/metadata.rs b/src/metadata.rs new file mode 100644 index 00000000..162c89af --- /dev/null +++ b/src/metadata.rs @@ -0,0 +1,170 @@ +// SPDX-License-Identifier: Apache-2.0 OR MIT + +// Adapted from https://github.com/taiki-e/cargo-hack + +use std::{collections::HashMap, ffi::OsStr, path::Path}; + +use anyhow::{format_err, Context as _, Result}; +use camino::Utf8PathBuf; +use serde_json::{Map, Value}; + +type Object = Map; +type ParseResult = Result; + +/// An opaque unique identifier for referring to the package. +#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub(crate) struct PackageId { + repr: String, +} + +impl From for PackageId { + fn from(repr: String) -> Self { + Self { repr } + } +} + +pub(crate) struct Metadata { + /// List of all packages in the workspace and all feature-enabled dependencies. + // + /// This doesn't contain dependencies if cargo-metadata is run with --no-deps. + pub(crate) packages: HashMap, + /// List of members of the workspace. + pub(crate) workspace_members: Vec, + /// The absolute path to the root of the workspace. + pub(crate) workspace_root: Utf8PathBuf, + pub(crate) target_directory: Utf8PathBuf, +} + +impl Metadata { + pub(crate) fn new(manifest_path: &Path, cargo: &OsStr) -> Result { + let mut cmd = cmd!( + cargo, + "metadata", + "--format-version=1", + "--no-deps", + "--manifest-path", + manifest_path + ); + let json = cmd.read()?; + + let map = serde_json::from_str(&json) + .with_context(|| format!("failed to parse output from {cmd}"))?; + Self::from_obj(map).map_err(|s| format_err!("failed to parse `{s}` field from metadata")) + } + + fn from_obj(mut map: Object) -> ParseResult { + let workspace_members: Vec<_> = map + .remove_array("workspace_members")? + .into_iter() + .map(|v| into_string(v).ok_or("workspace_members")) + .collect::>()?; + Ok(Self { + packages: map + .remove_array("packages")? + .into_iter() + .map(Package::from_value) + .collect::>()?, + workspace_members, + workspace_root: map.remove_string("workspace_root")?, + target_directory: map.remove_string("target_directory")?, + }) + } +} + +pub(crate) struct Package { + /// The name of the package. + pub(crate) name: String, + pub(crate) targets: Vec, + /// Absolute path to this package's manifest. + pub(crate) manifest_path: Utf8PathBuf, +} + +impl Package { + fn from_value(mut value: Value) -> 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")?, + targets: map + .remove_array("targets")? + .into_iter() + .map(Target::from_value) + .collect::>()?, + manifest_path: map.remove_string("manifest_path")?, + })) + } +} + +pub(crate) struct Target { + pub(crate) name: String, +} + +impl Target { + fn from_value(mut value: Value) -> ParseResult { + let map = value.as_object_mut().ok_or("targets")?; + + Ok(Self { name: map.remove_string("name")? }) + } +} + +#[allow(clippy::option_option)] +fn allow_null(value: Value, f: impl FnOnce(Value) -> Option) -> Option> { + if value.is_null() { + Some(None) + } else { + f(value).map(Some) + } +} + +fn into_string>(value: Value) -> Option { + if let Value::String(string) = value { + Some(string.into()) + } else { + None + } +} +fn into_array(value: Value) -> Option> { + if let Value::Array(array) = value { + Some(array) + } else { + None + } +} +fn into_object(value: Value) -> Option { + if let Value::Object(object) = value { + Some(object) + } else { + None + } +} + +trait ObjectExt { + fn remove_string>(&mut self, key: &'static str) -> ParseResult; + fn remove_array(&mut self, key: &'static str) -> ParseResult>; + fn remove_object(&mut self, key: &'static str) -> ParseResult; + fn remove_nullable( + &mut self, + key: &'static str, + f: impl FnOnce(Value) -> Option, + ) -> ParseResult>; +} + +impl ObjectExt for Object { + fn remove_string>(&mut self, key: &'static str) -> ParseResult { + self.remove(key).and_then(into_string).ok_or(key) + } + fn remove_array(&mut self, key: &'static str) -> ParseResult> { + self.remove(key).and_then(into_array).ok_or(key) + } + fn remove_object(&mut self, key: &'static str) -> ParseResult { + self.remove(key).and_then(into_object).ok_or(key) + } + fn remove_nullable( + &mut self, + key: &'static str, + f: impl FnOnce(Value) -> Option, + ) -> ParseResult> { + self.remove(key).and_then(|v| allow_null(v, f)).ok_or(key) + } +}