// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
//! Utilities to process `cargo metadata` dependency graph.
use crate::config::BuildConfig;
use crate::crates;
use crate::group::Group;
use crate::inherit::find_inherited_privilege_group;
use crate::platforms::{self, Platform, PlatformSet};
use std::collections::{hash_map::Entry, HashMap, HashSet};
use std::iter;
use std::path::PathBuf;
pub use cargo_metadata::DependencyKind;
pub use semver::Version;
/// Uniquely identifies a `Package` in a particular set of dependencies. The
/// representation is an implementation detail and may not be unique between
/// different sets of metadata.
pub use cargo_metadata::PackageId;
/// A single transitive dependency of a root crate. Includes information needed
/// for generating build files later.
#[derive(Clone, Debug)]
pub struct Package {
/// The package name as used by cargo.
pub package_name: String,
/// The package version as used by cargo.
pub version: Version,
pub description: Option<String>,
pub authors: Vec<String>,
pub edition: String,
/// This package's dependencies. Each element cross-references another
/// `Package` by name and version.
pub dependencies: Vec<DepOfDep>,
/// Same as the above, but for build script deps.
pub build_dependencies: Vec<DepOfDep>,
/// Same as the above, but for test deps.
pub dev_dependencies: Vec<DepOfDep>,
/// A package can be depended upon in different ways: as a normal
/// dependency, just for build scripts, or just for tests. `kinds` contains
/// an entry for each way this package is depended on.
pub dependency_kinds: HashMap<DependencyKind, PerKindInfo>,
/// The package's lib target, or `None` if it doesn't have one.
pub lib_target: Option<LibTarget>,
/// List of binaries provided by the package.
pub bin_targets: Vec<BinTarget>,
/// The build script's absolute path, or `None` if the package does not use
/// one.
pub build_script: Option<PathBuf>,
/// The path in the dependency graph to this package. This is intended for
/// human consumption when debugging missing packages.
pub dependency_path: Vec<String>,
/// What privilege group the crate is a part of.
pub group: Group,
/// Whether the source is a local path. Is `false` if cargo resolved this
/// dependency from a registry (e.g. crates.io) or git. If `false` the
/// package may still be locally vendored through cargo configuration (see
/// https://doc.rust-lang.org/cargo/reference/source-replacement.html)
pub is_local: bool,
/// Whether this package is depended on directly by the root Cargo.toml or
/// it is a transitive dependency.
pub is_toplevel_dep: bool,
}
impl Package {
pub fn crate_id(&self) -> crates::VendoredCrate {
crates::VendoredCrate { name: self.package_name.clone(), version: self.version.clone() }
}
}
/// A dependency of a `Package`. Cross-references another `Package` entry in the
/// resolved list.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct DepOfDep {
/// This dependency's package name as used by cargo.
pub package_name: String,
/// The name of the lib crate as `use`d by the dependent. This may be the
/// same or different than `package_name`.
pub use_name: String,
/// The resolved version of this dependency.
pub version: Version,
/// A platform constraint for this dependency, or `None` if it's used on all
/// platforms.
pub platform: Option<Platform>,
}
impl DepOfDep {
pub fn crate_id(&self) -> crates::VendoredCrate {
crates::VendoredCrate { name: self.package_name.clone(), version: self.version.clone() }
}
}
/// Information specific to the dependency kind: for normal, build script, or
/// test dependencies.
#[derive(Clone, Debug)]
pub struct PerKindInfo {
/// The set of platforms this kind is needed on.
pub platforms: PlatformSet,
/// The resolved feature set for this kind.
pub features: Vec<String>,
}
/// Description of a package's lib target.
#[derive(Clone, Debug)]
pub struct LibTarget {
/// The absolute path of the lib target's `lib.rs`.
pub root: PathBuf,
/// The type of the lib target. This is "rlib" for normal dependencies and
/// "proc-macro" for proc macros.
pub lib_type: LibType,
}
/// A binary provided by a package.
#[derive(Clone, Debug)]
pub struct BinTarget {
/// The absolute path of the binary's root source file (e.g. `main.rs`).
pub root: PathBuf,
/// The binary name.
pub name: String,
}
/// The type of lib target. Only includes types supported by this tool.
#[derive(Clone, Copy, Debug)]
pub enum LibType {
/// A normal Rust rlib library.
Rlib,
/// A Rust dynamic library. See
/// https://doc.rust-lang.org/reference/linkage.html for details and the
/// distinction between dylib and cdylib.
Dylib,
/// A C-compatible dynamic library. See
/// https://doc.rust-lang.org/reference/linkage.html for details and the
/// distinction between dylib and cdylib.
Cdylib,
/// A procedural macro.
ProcMacro,
}
impl std::fmt::Display for LibType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match *self {
Self::Rlib => f.write_str("rlib"),
Self::Dylib => f.write_str("dylib"),
Self::Cdylib => f.write_str("cdylib"),
Self::ProcMacro => f.write_str("proc-macro"),
}
}
}
/// Process the dependency graph in `metadata` to a flat list of transitive
/// dependencies. Each element in the result corresponds to a cargo package. A
/// package may have multiple crates, each of which corresponds to a single
/// rustc invocation: e.g. a package may have a lib crate as well as multiple
/// binary crates.
///
/// `roots` optionally specifies from which packages to traverse the dependency
/// graph (likely the root packages to generate build files for). This overrides
/// the usual behavior, which traverses from all workspace members and the root
/// workspace package. The package names in `roots` should still only contain
/// workspace members.
///
/// `exclude` optionally lists packages to exclude from dependency resolution.
/// Listed packages will still be included in upstream dependency lists, but
/// downstream dependencies will not be explored. E.g. if `bar` is listed, and
/// `foo` -> `bar` -> `baz` is in the dependency graph, `foo` will have `bar` as
/// a `DepOfDep` entry, but neither `bar` nor `baz` will be included in the
/// output. The intended use-case is when build rules for certain packages must
/// be written manually.
pub fn collect_dependencies(
metadata: &cargo_metadata::Metadata,
roots: Option<Vec<String>>,
exclude: Option<Vec<String>>,
extra_config: &BuildConfig,
) -> Vec<Package> {
// The metadata is split into two parts:
// 1. A list of packages and associated info: targets (e.g. lib, bin, tests),
// source path, etc. This includes all workspace members and all transitive
// dependencies. Deps are not filtered based on platform or features: it is
// the maximal set of dependencies.
// 2. Resolved dependency graph. There is a node for each package pointing to
// its dependencies in each configuration (normal, build, dev), and the
// resolved feature set. This includes platform-specific info so one can
// filter based on target platform. Nodes include an ID that uniquely refers
// to a package in both (1) and (2).
//
// We need info from both parts. Traversing the graph tells us exactly which
// crates are needed for a given configuration and platform. In the process,
// we must collect package IDs then look up other data in (1).
//
// Note the difference between "packages" and "crates" as described in
// https://doc.rust-lang.org/book/ch07-01-packages-and-crates.html
// `metadata`'s structures are flattened into lists. Make it easy to index
// by package ID.
let dep_graph: MetadataGraph = build_graph(metadata);
// `cargo metadata`'s resolved dependency graph.
let resolved_graph: &cargo_metadata::Resolve = metadata.resolve.as_ref().unwrap();
// The ID of the fake root package. Do not include it in the dependency list
// since it is not actually built.
let fake_root: &cargo_metadata::PackageId = resolved_graph.root.as_ref().unwrap();
let exclude = match exclude {
Some(exclude) => metadata
.packages
.iter()
.filter_map(|pkg| if exclude.contains(&pkg.name) { Some(&pkg.id) } else { None })
.collect(),
None => HashSet::new(),
};
// `explore_node`, our recursive depth-first traversal function, needs to
// share state between stack frames. Construct the shared state.
let mut traversal_state = TraversalState {
dep_graph: &dep_graph,
root: fake_root,
exclude,
visited: HashSet::new(),
path: Vec::new(),
dependencies: HashMap::new(),
};
let traversal_roots: Vec<&cargo_metadata::PackageId> = match roots {
Some(roots) => metadata
.packages
.iter()
.filter_map(|pkg| if roots.contains(&pkg.name) { Some(&pkg.id) } else { None })
.collect(),
None => dep_graph.roots.clone(),
};
// Do a depth-first traversal of the graph to find all relevant
// dependencies. Start from each workspace package ("chromium" and
// additional binary members used in the build).
for root_id in traversal_roots.iter() {
let node_map: &HashMap<&cargo_metadata::PackageId, &cargo_metadata::Node> =
&dep_graph.nodes;
explore_node(&mut traversal_state, node_map.get(*root_id).unwrap());
}
// TODO(danakj): Throw an error if any `safe` crate depends on a `sandbox`
// crate.
// `traversal_state.dependencies` is the output of `explore_node`. Pull it
// out for processing.
let mut dependencies = traversal_state.dependencies;
// Fill in the per-package data for each dependency.
for (id, dep) in dependencies.iter_mut() {
let node: &cargo_metadata::Node = traversal_state.dep_graph.nodes.get(id).unwrap();
let package: &cargo_metadata::Package = traversal_state.dep_graph.packages.get(id).unwrap();
dep.package_name = package.name.clone();
dep.description = package.description.clone();
dep.authors = package.authors.clone();
dep.edition = package.edition.to_string();
// TODO(danakj): It would be nice to store the `manifest_dir` here and
// change all gnrt_config.toml relative paths to be relative to the
// manifest instead of relative to the crate root, to eliminate the
// chance for there being a different relative path from a lib root vs a
// bin root. It can be grabbed like:
//
// dep.manifest_dir = package
// .manifest_path
// .parent()
// .expect("manifest_path has no directory?")
// .to_path_buf()
// .into_std_path_buf();
// TODO(crbug.com/40212956): Resolve features independently per kind
// and platform. This may require using the unstable unit-graph feature:
// https://doc.rust-lang.org/cargo/reference/unstable.html#unit-graph
for (_, kind_info) in dep.dependency_kinds.iter_mut() {
kind_info.features = node.features.clone();
// Remove "default" feature to match behavior of crates.py. Note
// that this is technically not correct since a crate's code may
// choose to check "default" directly, but virtually none actually
// do this.
//
// TODO(crbug.com/40212956): Revisit this behavior and maybe keep
// "default" features.
if let Some(pos) = kind_info.features.iter().position(|x| x == "default") {
kind_info.features.remove(pos);
}
}
let allowed_bin_targets: HashSet<&str> =
extra_config.get_combined_set(&package.name, |crate_cfg| &crate_cfg.bin_targets);
for target in package.targets.iter() {
let src_root = target.src_path.clone().into_std_path_buf();
let target_type = match target.kind.iter().find_map(|s| TargetType::from_name(s)) {
Some(target_type) => target_type,
// Skip other targets, such as test, example, etc.
None => continue,
};
match target_type {
TargetType::Lib(lib_type) => {
// There can only be one lib target.
assert!(
dep.lib_target.is_none(),
"found duplicate lib target:\n{:?}\n{:?}",
dep.lib_target,
target
);
dep.lib_target = Some(LibTarget { root: src_root, lib_type });
}
TargetType::Bin => {
if allowed_bin_targets.contains(target.name.as_str()) {
dep.bin_targets
.push(BinTarget { root: src_root, name: target.name.clone() });
}
}
TargetType::BuildScript => {
assert_eq!(
dep.build_script, None,
"found duplicate build script target {target:?}"
);
dep.build_script = Some(src_root);
}
}
}
dep.version = package.version.clone();
// Collect this package's list of resolved dependencies which will be
// needed for build file generation later.
for node_dep in iter_node_deps(node) {
let dep_pkg = dep_graph.packages.get(node_dep.pkg).unwrap();
let mut platform = node_dep.target;
if let Some(p) = platform {
assert!(platforms::matches_supported_target(&p));
platform = platforms::filter_unsupported_platform_terms(p);
}
let dep_of_dep = DepOfDep {
package_name: dep_pkg.name.clone(),
use_name: node_dep.lib_name.to_string(),
version: dep_pkg.version.clone(),
platform,
};
match node_dep.kind {
DependencyKind::Normal => dep.dependencies.push(dep_of_dep),
DependencyKind::Build => dep.build_dependencies.push(dep_of_dep),
DependencyKind::Development => dep.dev_dependencies.push(dep_of_dep),
DependencyKind::Unknown => unreachable!(),
}
}
dep.group = find_inherited_privilege_group(
id,
&dep_graph.nodes.get(fake_root).unwrap().id,
&dep_graph.packages,
&dep_graph.nodes,
extra_config,
);
// Make sure the package comes from our vendored source. If not, report
// the error for later.
dep.is_local = package.source.is_none();
// Determine whether it's a direct or transitive dependency.
dep.is_toplevel_dep = {
let fake_root_node = dep_graph.nodes.get(fake_root).unwrap();
fake_root_node.dependencies.contains(id)
};
}
// Return a flat list of dependencies.
dependencies.into_values().collect()
}
/// Graph traversal state shared by recursive calls of `explore_node`.
struct TraversalState<'a> {
/// The graph from "cargo metadata", processed for indexing by package id.
dep_graph: &'a MetadataGraph<'a>,
/// The fake root package that we exclude from `dependencies`.
root: &'a cargo_metadata::PackageId,
/// Set of packages to exclude from traversal.
exclude: HashSet<&'a cargo_metadata::PackageId>,
/// Set of packages already visited by `explore_node`.
visited: HashSet<&'a cargo_metadata::PackageId>,
/// The path of package IDs to the current node. For human consumption.
path: Vec<String>,
/// The final set of dependencies.
dependencies: HashMap<&'a cargo_metadata::PackageId, Package>,
}
/// Recursively explore a particular node in the dependency graph. Fills data in
/// `state`. The final output is in `state.dependencies`.
fn explore_node<'a>(state: &mut TraversalState<'a>, node: &'a cargo_metadata::Node) {
// Mark the node as visited, or continue if it's already visited.
if !state.visited.insert(&node.id) {
return;
}
if state.exclude.contains(&node.id) {
return;
}
// Helper to insert a placeholder `Dependency` into a map. We fill in the
// fields later.
let init_dep = |path| Package {
package_name: String::new(),
version: Version::new(0, 0, 0),
description: None,
authors: Vec::new(),
edition: String::new(),
dependencies: Vec::new(),
build_dependencies: Vec::new(),
dev_dependencies: Vec::new(),
dependency_kinds: HashMap::new(),
lib_target: None,
bin_targets: Vec::new(),
build_script: None,
dependency_path: path,
group: Group::Safe,
is_local: false,
is_toplevel_dep: false,
};
state.path.push(node.id.repr.clone());
// Each node contains a list of enabled features plus a list of
// dependencies. Each dependency has a platform filter if applicable.
for dep_edge in iter_node_deps(node) {
// Explore the target of this edge next. Note that we may visit the same
// node multiple times, but this is OK since we'll skip it in the
// recursive call.
let target_node: &cargo_metadata::Node = state.dep_graph.nodes.get(&dep_edge.pkg).unwrap();
if state.exclude.contains(&target_node.id) {
continue;
}
explore_node(state, target_node);
// Merge this with the existing entry for the dep.
let dep: &mut Package =
state.dependencies.entry(dep_edge.pkg).or_insert_with(|| init_dep(state.path.clone()));
let info: &mut PerKindInfo = dep
.dependency_kinds
.entry(dep_edge.kind)
.or_insert(PerKindInfo { platforms: PlatformSet::empty(), features: Vec::new() });
info.platforms.add(dep_edge.target);
}
state.path.pop();
// Initialize the dependency entry for this node's package if it's not our
// fake root.
if &node.id != state.root {
state.dependencies.entry(&node.id).or_insert_with(|| init_dep(state.path.clone()));
}
}
struct DependencyEdge<'a> {
pkg: &'a cargo_metadata::PackageId,
lib_name: &'a str,
kind: DependencyKind,
target: Option<Platform>,
}
/// Iterates over the dependencies of `node`, filtering out platforms we don't
/// support.
fn iter_node_deps(node: &cargo_metadata::Node) -> impl Iterator<Item = DependencyEdge<'_>> + '_ {
node.deps.iter().flat_map(|node_dep| {
// Each NodeDep has information about the package depended on, as
// well as the kinds of dependence: as a normal, build script, or
// test dependency. For each kind there is an optional platform
// filter.
//
// Filter out kinds for unsupported platforms while mapping the
// dependency edges to our own type.
//
// Cargo may also have duplicates in the dep_kinds list, which may
// or may not be a Cargo bug, but we want to filter them out too.
// See crbug.com/1393600.
let mut seen = HashSet::new();
node_dep.dep_kinds.iter().filter_map(move |dep_kind_info| {
// Filter if it's for a platform we don't support.
match &dep_kind_info.target {
None => (),
Some(platform) => {
if !platforms::matches_supported_target(platform) {
return None;
}
}
};
if seen.contains(&(&dep_kind_info.kind, &dep_kind_info.target)) {
return None;
}
seen.insert((&dep_kind_info.kind, &dep_kind_info.target));
Some(DependencyEdge {
pkg: &node_dep.pkg,
lib_name: &node_dep.name,
kind: dep_kind_info.kind,
target: dep_kind_info.target.clone(),
})
})
})
}
/// Indexable representation of the `cargo_metadata::Metadata` fields we need.
struct MetadataGraph<'a> {
nodes: HashMap<&'a cargo_metadata::PackageId, &'a cargo_metadata::Node>,
packages: HashMap<&'a cargo_metadata::PackageId, &'a cargo_metadata::Package>,
roots: Vec<&'a cargo_metadata::PackageId>,
}
/// Convert the flat lists in `metadata` to maps indexable by PackageId.
fn build_graph(metadata: &cargo_metadata::Metadata) -> MetadataGraph<'_> {
// `metadata` always has `resolve` unless cargo was explicitly asked not to
// output the dependency graph.
let resolve = metadata.resolve.as_ref().unwrap();
let mut graph = HashMap::new();
for node in resolve.nodes.iter() {
match graph.entry(&node.id) {
Entry::Vacant(e) => e.insert(node),
Entry::Occupied(_) => panic!("duplicate entries in dependency graph"),
};
}
let packages = metadata.packages.iter().map(|p| (&p.id, p)).collect();
let roots = iter::once(resolve.root.as_ref().unwrap())
.chain(metadata.workspace_members.iter())
.collect();
MetadataGraph { nodes: graph, packages, roots }
}
/// A crate target type we support.
#[derive(Clone, Copy, Debug)]
enum TargetType {
Lib(LibType),
Bin,
BuildScript,
}
impl TargetType {
fn from_name(name: &str) -> Option<Self> {
match name {
"lib" | "rlib" => Some(Self::Lib(LibType::Rlib)),
"dylib" => Some(Self::Lib(LibType::Dylib)),
"cdylib" => Some(Self::Lib(LibType::Cdylib)),
"bin" => Some(Self::Bin),
"custom-build" => Some(Self::BuildScript),
"proc-macro" => Some(Self::Lib(LibType::ProcMacro)),
_ => None,
}
}
}
impl std::fmt::Display for TargetType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match *self {
Self::Lib(typ) => typ.fmt(f),
Self::Bin => f.write_str("bin"),
Self::BuildScript => f.write_str("custom-build"),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn collect_dependencies_on_sample_output() {
use std::str::FromStr;
let config = BuildConfig::default();
let metadata: cargo_metadata::Metadata =
serde_json::from_str(SAMPLE_CARGO_METADATA).unwrap();
let mut dependencies = collect_dependencies(&metadata, None, None, &config);
dependencies.sort_by(|left, right| {
left.package_name.cmp(&right.package_name).then(left.version.cmp(&right.version))
});
let empty_str_slice: &'static [&'static str] = &[];
assert_eq!(dependencies.len(), 17);
let mut i = 0;
assert_eq!(dependencies[i].package_name, "autocfg");
assert_eq!(dependencies[i].version, Version::new(1, 1, 0));
assert_eq!(
dependencies[i].dependency_kinds.get(&DependencyKind::Build).unwrap().features,
empty_str_slice
);
i += 1;
assert_eq!(dependencies[i].package_name, "bar");
assert_eq!(dependencies[i].version, Version::new(0, 1, 0));
assert_eq!(
dependencies[i].dependency_kinds.get(&DependencyKind::Normal).unwrap().features,
empty_str_slice
);
i += 1;
assert_eq!(dependencies[i].package_name, "cc");
assert_eq!(dependencies[i].version, Version::new(1, 0, 73));
assert_eq!(
dependencies[i].dependency_kinds.get(&DependencyKind::Build).unwrap().features,
empty_str_slice
);
i += 1;
assert_eq!(dependencies[i].package_name, "foo");
assert_eq!(dependencies[i].version, Version::new(0, 1, 0));
assert_eq!(
dependencies[i].dependency_kinds.get(&DependencyKind::Normal).unwrap().features,
empty_str_slice
);
assert_eq!(dependencies[i].dependencies.len(), 2);
assert_eq!(
dependencies[i].dependencies[0],
DepOfDep {
package_name: "bar".to_string(),
use_name: "baz".to_string(),
version: Version::new(0, 1, 0),
platform: None,
}
);
assert_eq!(
dependencies[i].dependencies[1],
DepOfDep {
package_name: "time".to_string(),
use_name: "time".to_string(),
version: Version::new(0, 3, 14),
platform: None,
}
);
i += 1;
assert_eq!(dependencies[i].package_name, "more-asserts");
assert_eq!(dependencies[i].version, Version::new(0, 3, 0));
assert_eq!(
dependencies[i].dependency_kinds.get(&DependencyKind::Development).unwrap().features,
empty_str_slice
);
i += 1;
assert_eq!(dependencies[i].package_name, "num-traits");
assert_eq!(dependencies[i].version, Version::new(0, 2, 15));
assert_eq!(
dependencies[i].dependency_kinds.get(&DependencyKind::Normal).unwrap().features,
&["std"]
);
assert_eq!(dependencies[i].build_dependencies.len(), 1);
assert_eq!(
dependencies[i].build_dependencies[0],
DepOfDep {
package_name: "autocfg".to_string(),
use_name: "autocfg".to_string(),
version: Version::new(1, 1, 0),
platform: None,
}
);
i += 1;
assert_eq!(dependencies[i].package_name, "once_cell");
assert_eq!(dependencies[i].version, Version::new(1, 13, 0));
assert_eq!(
dependencies[i].dependency_kinds.get(&DependencyKind::Normal).unwrap().features,
&["alloc", "race", "std"]
);
i += 1;
assert_eq!(dependencies[i].package_name, "proc-macro2");
assert_eq!(dependencies[i].version, Version::new(1, 0, 40));
assert_eq!(
dependencies[i].dependency_kinds.get(&DependencyKind::Normal).unwrap().features,
&["proc-macro"]
);
i += 1;
assert_eq!(dependencies[i].package_name, "quote");
assert_eq!(dependencies[i].version, Version::new(1, 0, 20));
assert_eq!(
dependencies[i].dependency_kinds.get(&DependencyKind::Normal).unwrap().features,
&["proc-macro"]
);
i += 1;
assert_eq!(dependencies[i].package_name, "serde");
assert_eq!(dependencies[i].version, Version::new(1, 0, 139));
assert_eq!(
dependencies[i].dependency_kinds.get(&DependencyKind::Normal).unwrap().features,
&["derive", "serde_derive", "std"]
);
assert_eq!(dependencies[i].dependencies.len(), 1);
assert_eq!(dependencies[i].build_dependencies.len(), 0);
assert_eq!(dependencies[i].dev_dependencies.len(), 0);
assert_eq!(
dependencies[i].dependencies[0],
DepOfDep {
package_name: "serde_derive".to_string(),
use_name: "serde_derive".to_string(),
version: Version::new(1, 0, 139),
platform: None,
}
);
i += 1;
assert_eq!(dependencies[i].package_name, "serde_derive");
assert_eq!(dependencies[i].version, Version::new(1, 0, 139));
assert_eq!(
dependencies[i].dependency_kinds.get(&DependencyKind::Normal).unwrap().features,
empty_str_slice
);
assert_eq!(dependencies[i].dependencies.len(), 3);
assert_eq!(dependencies[i].build_dependencies.len(), 0);
assert_eq!(dependencies[i].dev_dependencies.len(), 0);
assert_eq!(
dependencies[i].dependencies[0],
DepOfDep {
package_name: "proc-macro2".to_string(),
use_name: "proc_macro2".to_string(),
version: Version::new(1, 0, 40),
platform: None,
}
);
assert_eq!(
dependencies[i].dependencies[1],
DepOfDep {
package_name: "quote".to_string(),
use_name: "quote".to_string(),
version: Version::new(1, 0, 20),
platform: None,
}
);
assert_eq!(
dependencies[i].dependencies[2],
DepOfDep {
package_name: "syn".to_string(),
use_name: "syn".to_string(),
version: Version::new(1, 0, 98),
platform: None,
}
);
i += 1;
assert_eq!(dependencies[i].package_name, "syn");
assert_eq!(dependencies[i].version, Version::new(1, 0, 98));
assert_eq!(
dependencies[i].dependency_kinds.get(&DependencyKind::Normal).unwrap().features,
&["clone-impls", "derive", "parsing", "printing", "proc-macro", "quote"]
);
assert_eq!(dependencies[i].dependencies.len(), 3);
assert_eq!(dependencies[i].build_dependencies.len(), 0);
assert_eq!(dependencies[i].dev_dependencies.len(), 0);
assert_eq!(
dependencies[i].dependencies[0],
DepOfDep {
package_name: "proc-macro2".to_string(),
use_name: "proc_macro2".to_string(),
version: Version::new(1, 0, 40),
platform: None,
}
);
assert_eq!(
dependencies[i].dependencies[1],
DepOfDep {
package_name: "quote".to_string(),
use_name: "quote".to_string(),
version: Version::new(1, 0, 20),
platform: None,
}
);
assert_eq!(
dependencies[i].dependencies[2],
DepOfDep {
package_name: "unicode-ident".to_string(),
use_name: "unicode_ident".to_string(),
version: Version::new(1, 0, 1),
platform: None,
}
);
i += 1;
assert_eq!(dependencies[i].package_name, "termcolor");
assert_eq!(dependencies[i].version, Version::new(1, 1, 3));
assert_eq!(
dependencies[i].dependency_kinds.get(&DependencyKind::Normal).unwrap().features,
empty_str_slice
);
assert_eq!(dependencies[i].dependencies.len(), 1);
assert_eq!(dependencies[i].build_dependencies.len(), 0);
assert_eq!(dependencies[i].dev_dependencies.len(), 0);
assert_eq!(
dependencies[i].dependencies[0],
DepOfDep {
package_name: "winapi-util".to_string(),
use_name: "winapi_util".to_string(),
version: Version::new(0, 1, 5),
platform: Some(Platform::from_str("cfg(windows)").unwrap()),
}
);
i += 1;
assert_eq!(dependencies[i].package_name, "time");
assert_eq!(dependencies[i].version, Version::new(0, 3, 14));
assert_eq!(
dependencies[i].dependency_kinds.get(&DependencyKind::Normal).unwrap().features,
&["alloc", "std"]
);
i += 1;
assert_eq!(dependencies[i].package_name, "unicode-ident");
assert_eq!(dependencies[i].version, Version::new(1, 0, 1));
assert_eq!(
dependencies[i].dependency_kinds.get(&DependencyKind::Normal).unwrap().features,
empty_str_slice
);
i += 1;
assert_eq!(dependencies[i].package_name, "winapi");
assert_eq!(dependencies[i].version, Version::new(0, 3, 9));
assert_eq!(
dependencies[i].dependency_kinds.get(&DependencyKind::Normal).unwrap().features,
&[
"consoleapi",
"errhandlingapi",
"fileapi",
"minwindef",
"processenv",
"std",
"winbase",
"wincon",
"winerror",
"winnt"
]
);
assert_eq!(dependencies[i].dependencies.len(), 0);
assert_eq!(dependencies[i].build_dependencies.len(), 0);
assert_eq!(dependencies[i].dev_dependencies.len(), 0);
i += 1;
assert_eq!(dependencies[i].package_name, "winapi-util");
assert_eq!(dependencies[i].version, Version::new(0, 1, 5));
assert_eq!(
dependencies[i].dependency_kinds.get(&DependencyKind::Normal).unwrap().features,
empty_str_slice
);
assert_eq!(dependencies[i].dependencies.len(), 1);
assert_eq!(dependencies[i].build_dependencies.len(), 0);
assert_eq!(dependencies[i].dev_dependencies.len(), 0);
assert_eq!(
dependencies[i].dependencies[0],
DepOfDep {
package_name: "winapi".to_string(),
use_name: "winapi".to_string(),
version: Version::new(0, 3, 9),
platform: Some(Platform::from_str("cfg(windows)").unwrap()),
}
);
}
#[test]
fn dependencies_for_workspace_member() {
let config = BuildConfig::default();
let metadata: cargo_metadata::Metadata =
serde_json::from_str(SAMPLE_CARGO_METADATA).unwrap();
// Start from "foo" workspace member.
let mut dependencies =
collect_dependencies(&metadata, Some(vec!["foo".to_string()]), None, &config);
dependencies.sort_by(|left, right| {
left.package_name.cmp(&right.package_name).then(left.version.cmp(&right.version))
});
assert_eq!(dependencies.len(), 3);
let mut i = 0;
assert_eq!(dependencies[i].package_name, "bar");
assert_eq!(dependencies[i].version, Version::new(0, 1, 0));
i += 1;
assert_eq!(dependencies[i].package_name, "foo");
assert_eq!(dependencies[i].version, Version::new(0, 1, 0));
i += 1;
assert_eq!(dependencies[i].package_name, "time");
assert_eq!(dependencies[i].version, Version::new(0, 3, 14));
assert_eq!(
dependencies[i].dependency_kinds.get(&DependencyKind::Normal).unwrap().features,
&["alloc", "std"]
);
}
#[test]
fn exclude_dependency() {
let metadata: cargo_metadata::Metadata =
serde_json::from_str(SAMPLE_CARGO_METADATA).unwrap();
let config = BuildConfig::default();
let deps_with_exclude =
collect_dependencies(&metadata, None, Some(vec!["serde_derive".to_string()]), &config);
let deps_without_exclude = collect_dependencies(&metadata, None, None, &config);
let pkgs_with_exclude: HashSet<&str> =
deps_with_exclude.iter().map(|dep| dep.package_name.as_str()).collect();
let pkgs_without_exclude: HashSet<&str> =
deps_without_exclude.iter().map(|dep| dep.package_name.as_str()).collect();
let mut diff: Vec<&str> =
pkgs_without_exclude.difference(&pkgs_with_exclude).copied().collect();
diff.sort_unstable();
assert_eq!(diff, ["proc-macro2", "quote", "serde_derive", "syn", "unicode-ident",]);
}
// test_metadata.json contains the output of "cargo metadata" run in
// sample_package. The dependency graph is relatively simple but includes
// transitive deps and a workspace member.
static SAMPLE_CARGO_METADATA: &str = include_str!("test_metadata.json");
}