blob: 54e09d9ce7d6db376557a8d178313b9087967764 [file] [log] [blame]
// 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::crates::{self, Epoch};
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 {
/// Package ID in a particular set of dependencies.
pub id: PackageId,
/// 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>,
/// 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 a member of the cargo workspace the metadata
/// came from, as opposed to a third-party dependency.
pub is_workspace_member: bool,
}
impl Package {
pub fn third_party_crate_id(&self) -> crates::ChromiumVendoredCrate {
crates::ChromiumVendoredCrate {
name: self.package_name.clone(),
epoch: Epoch::from_version(&self.version),
}
}
}
/// 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 third_party_crate_id(&self) -> crates::ChromiumVendoredCrate {
crates::ChromiumVendoredCrate {
name: self.package_name.clone(),
epoch: Epoch::from_version(&self.version),
}
}
}
/// 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 resovled 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>>,
) -> 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());
}
// `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.id = package.id.clone();
dep.package_name = package.name.clone();
dep.description = package.description.clone();
dep.authors = package.authors.clone();
dep.edition = package.edition.clone();
// TODO(crbug.com/1291994): 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 (_, mut 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/1291994): 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);
}
}
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 => {
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!(),
}
}
// Make sure the package comes from our vendored source. If not, report
// the error for later.
dep.is_local = package.source == None;
// Determine whether it's a workspace member or third-party dependency.
dep.is_workspace_member = dep_graph.workspace_members.contains(&package.id);
}
// Return a flat list of dependencies.
dependencies.into_iter().map(|(_, v)| v).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 {
id: PackageId { repr: String::new() },
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,
is_local: false,
is_workspace_member: 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()
.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(),
})
})
})
.flatten()
}
/// 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>,
workspace_members: HashSet<&'a cargo_metadata::PackageId>,
roots: Vec<&'a cargo_metadata::PackageId>,
}
/// Convert the flat lists in `metadata` to maps indexable by PackageId.
fn build_graph<'a>(metadata: &'a cargo_metadata::Metadata) -> MetadataGraph<'a> {
// `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,
workspace_members: metadata.workspace_members.iter().collect(),
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"),
}
}
}