blob: b490d65653d8b0bf5ce46abdb23f1bd8c876f83d [file] [log] [blame]
// Copyright (C) 2023 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! Types for parsing cargo.metadata JSON files.
use super::{Crate, CrateType, Extern, ExternType};
use crate::config::VariantConfig;
use anyhow::{bail, Context, Result};
use serde::Deserialize;
use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
/// `cfg` strings for dependencies which should be considered enabled. It would be better to parse
/// them properly, but this is good enough in practice so far.
const ENABLED_CFGS: [&str; 6] = [
r#"unix"#,
r#"not(windows)"#,
r#"any(unix, target_os = "wasi")"#,
r#"not(all(target_family = "wasm", target_os = "unknown"))"#,
r#"not(target_family = "wasm")"#,
r#"any(target_os = "linux", target_os = "android")"#,
];
/// `cargo metadata` output.
#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
pub struct WorkspaceMetadata {
pub packages: Vec<PackageMetadata>,
pub workspace_members: Vec<String>,
}
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq)]
pub struct PackageMetadata {
pub name: String,
pub version: String,
pub edition: String,
pub manifest_path: String,
pub dependencies: Vec<DependencyMetadata>,
pub features: BTreeMap<String, Vec<String>>,
pub id: String,
pub targets: Vec<TargetMetadata>,
pub license: Option<String>,
pub license_file: Option<String>,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
pub struct DependencyMetadata {
pub name: String,
pub kind: Option<String>,
pub optional: bool,
pub target: Option<String>,
pub rename: Option<String>,
}
impl DependencyMetadata {
/// Returns whether the dependency should be included when the given features are enabled.
fn enabled(&self, features: &[String], cfgs: &[String]) -> bool {
if let Some(target) = &self.target {
if target.starts_with("cfg(") && target.ends_with(')') {
let target_cfg = &target[4..target.len() - 1];
if !ENABLED_CFGS.contains(&target_cfg) && !cfgs.contains(&target_cfg.to_string()) {
return false;
}
}
}
let name = self.rename.as_ref().unwrap_or(&self.name);
!self.optional || features.contains(&format!("dep:{}", name))
}
}
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq)]
#[allow(dead_code)]
pub struct TargetMetadata {
pub crate_types: Vec<CrateType>,
pub doc: bool,
pub doctest: bool,
pub edition: String,
pub kind: Vec<TargetKind>,
pub name: String,
pub src_path: PathBuf,
pub test: bool,
}
#[derive(Copy, Clone, Debug, Deserialize, Eq, PartialEq)]
#[serde[rename_all = "kebab-case"]]
pub enum TargetKind {
Bin,
CustomBuild,
Bench,
Example,
Lib,
Rlib,
Staticlib,
Cdylib,
ProcMacro,
Test,
}
pub fn parse_cargo_metadata_str(cargo_metadata: &str, cfg: &VariantConfig) -> Result<Vec<Crate>> {
let metadata =
serde_json::from_str(cargo_metadata).context("failed to parse cargo metadata")?;
parse_cargo_metadata(
&metadata,
&cfg.features,
&cfg.extra_cfg,
cfg.tests,
&cfg.workspace_excludes,
)
}
fn parse_cargo_metadata(
metadata: &WorkspaceMetadata,
features: &Option<Vec<String>>,
cfgs: &[String],
include_tests: bool,
workspace_excludes: &[String],
) -> Result<Vec<Crate>> {
let mut crates = Vec::new();
for package in &metadata.packages {
if !metadata.workspace_members.contains(&package.id)
|| workspace_excludes.contains(&package.name)
{
continue;
}
let features = resolve_features(features, &package.features, &package.dependencies);
let features_without_deps: Vec<String> =
features.clone().into_iter().filter(|feature| !feature.starts_with("dep:")).collect();
let package_dir = package_dir_from_id(&package.id)?;
for target in &package.targets {
let target_kinds = target
.kind
.clone()
.into_iter()
.filter(|kind| {
[
TargetKind::Bin,
TargetKind::Cdylib,
TargetKind::Lib,
TargetKind::ProcMacro,
TargetKind::Rlib,
TargetKind::Staticlib,
TargetKind::Test,
]
.contains(kind)
})
.collect::<Vec<_>>();
if target_kinds.is_empty() {
// Only binaries, libraries and integration tests are supported.
continue;
}
let main_src = split_src_path(&target.src_path, &package_dir);
// Hypens are not allowed in crate names. See
// https://github.com/rust-lang/rfcs/blob/master/text/0940-hyphens-considered-harmful.md
// for background.
let target_name = target.name.replace('-', "_");
let target_triple = if target_kinds == [TargetKind::ProcMacro] {
None
} else {
Some("x86_64-unknown-linux-gnu".to_string())
};
// Don't generate an entry for integration tests, they will be covered by the test case
// below.
if target_kinds != [TargetKind::Test] {
crates.push(Crate {
name: target_name.clone(),
package_name: package.name.to_owned(),
version: Some(package.version.to_owned()),
types: target.crate_types.clone(),
features: features_without_deps.clone(),
edition: package.edition.to_owned(),
license: package.license.clone(),
license_file: package.license_file.clone(),
package_dir: package_dir.clone(),
main_src: main_src.to_owned(),
target: target_triple.clone(),
externs: get_externs(
package,
&metadata.packages,
&features,
cfgs,
&target_kinds,
false,
)?,
cfgs: cfgs.to_owned(),
..Default::default()
});
}
// This includes both unit tests and integration tests.
if target.test && include_tests {
crates.push(Crate {
name: target_name,
package_name: package.name.to_owned(),
version: Some(package.version.to_owned()),
types: vec![CrateType::Test],
features: features_without_deps.clone(),
edition: package.edition.to_owned(),
license: package.license.clone(),
license_file: package.license_file.clone(),
package_dir: package_dir.clone(),
main_src: main_src.to_owned(),
target: target_triple.clone(),
externs: get_externs(
package,
&metadata.packages,
&features,
cfgs,
&target_kinds,
true,
)?,
cfgs: cfgs.to_owned(),
..Default::default()
});
}
}
}
Ok(crates)
}
fn get_externs(
package: &PackageMetadata,
packages: &[PackageMetadata],
features: &[String],
cfgs: &[String],
target_kinds: &[TargetKind],
test: bool,
) -> Result<Vec<Extern>> {
let mut externs = package
.dependencies
.iter()
.filter_map(|dependency| {
// Kind is None for normal dependencies, as opposed to dev dependencies.
if dependency.enabled(features, cfgs)
&& dependency.kind.as_deref() != Some("build")
&& (dependency.kind.is_none() || test)
{
Some(make_extern(packages, dependency))
} else {
None
}
})
.collect::<Result<Vec<Extern>>>()?;
// If there is a library target and this is a binary or integration test, add the library as an
// extern.
if matches!(target_kinds, [TargetKind::Bin] | [TargetKind::Test]) {
for target in &package.targets {
if target.kind.contains(&TargetKind::Lib) {
let lib_name = target.name.replace('-', "_");
externs.push(Extern {
name: lib_name.clone(),
lib_name,
raw_name: target.name.clone(),
extern_type: ExternType::Rust,
});
}
}
}
externs.sort();
externs.dedup();
Ok(externs)
}
fn make_extern(packages: &[PackageMetadata], dependency: &DependencyMetadata) -> Result<Extern> {
let Some(package) = packages.iter().find(|package| package.name == dependency.name) else {
bail!("package {} not found in metadata", dependency.name);
};
let Some(target) = package.targets.iter().find(|target| {
target.kind.contains(&TargetKind::Lib) || target.kind.contains(&TargetKind::ProcMacro)
}) else {
bail!("Package {} didn't have any library or proc-macro targets", dependency.name);
};
let lib_name = target.name.replace('-', "_");
// This is ugly but looking at the source path is the easiest way to tell if the raw
// crate name uses a hyphen instead of an underscore. It won't work if it uses both.
let raw_name = target.name.replace('_', "-");
let src_path = target.src_path.to_str().expect("failed to convert src_path to string");
let raw_name = if src_path.contains(&raw_name) { raw_name } else { lib_name.clone() };
let name =
if let Some(rename) = &dependency.rename { rename.clone() } else { lib_name.clone() };
// Check whether the package is a proc macro.
let extern_type =
if package.targets.iter().any(|target| target.kind.contains(&TargetKind::ProcMacro)) {
ExternType::ProcMacro
} else {
ExternType::Rust
};
Ok(Extern { name, lib_name, raw_name, extern_type })
}
/// Given a Cargo package ID, returns the path.
///
/// Extracts `"/path/to/crate"` from
/// `"path+file:///path/to/crate#1.2.3"`. See
/// https://doc.rust-lang.org/cargo/reference/pkgid-spec.html for
/// information on Cargo package ID specifications.
fn package_dir_from_id(id: &str) -> Result<PathBuf> {
const PREFIX: &str = "path+file://";
const SEPARATOR: char = '#';
let Some(stripped) = id.strip_prefix(PREFIX) else {
bail!("Invalid package ID {id:?}, expected it to start with {PREFIX:?}");
};
let Some(idx) = stripped.rfind(SEPARATOR) else {
bail!("Invalid package ID {id:?}, expected it to contain {SEPARATOR:?}");
};
Ok(PathBuf::from(stripped[..idx].to_string()))
}
fn split_src_path<'a>(src_path: &'a Path, package_dir: &Path) -> &'a Path {
if let Ok(main_src) = src_path.strip_prefix(package_dir) {
main_src
} else {
src_path
}
}
/// Given a set of chosen features, and the feature dependencies from a package's metadata, returns
/// the full set of features which should be enabled.
fn resolve_features(
chosen_features: &Option<Vec<String>>,
package_features: &BTreeMap<String, Vec<String>>,
dependencies: &[DependencyMetadata],
) -> Vec<String> {
let mut package_features = package_features.to_owned();
// Add implicit features for optional dependencies.
for dependency in dependencies {
if dependency.optional && !package_features.contains_key(&dependency.name) {
package_features
.insert(dependency.name.to_owned(), vec![format!("dep:{}", dependency.name)]);
}
}
let mut features = Vec::new();
if let Some(chosen_features) = chosen_features {
for feature in chosen_features {
add_feature_and_dependencies(&mut features, feature, &package_features);
}
} else {
// If there are no chosen features, then enable the default feature.
add_feature_and_dependencies(&mut features, "default", &package_features);
}
features.sort();
features.dedup();
features
}
/// Adds the given feature and all features it depends on to the given list of features.
///
/// Ignores features of other packages, and features which don't exist.
fn add_feature_and_dependencies(
features: &mut Vec<String>,
feature: &str,
package_features: &BTreeMap<String, Vec<String>>,
) {
if package_features.contains_key(feature) || feature.starts_with("dep:") {
features.push(feature.to_owned());
}
if let Some(dependencies) = package_features.get(feature) {
for dependency in dependencies {
if let Some((dependency_package, _)) = dependency.split_once('/') {
add_feature_and_dependencies(features, dependency_package, package_features);
} else {
add_feature_and_dependencies(features, dependency, package_features);
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::Config;
use crate::tests::testdata_directories;
use googletest::matchers::eq;
use googletest::prelude::assert_that;
use std::fs::{read_to_string, File};
#[test]
fn extract_package_dir_from_id() -> Result<()> {
assert_eq!(
package_dir_from_id("path+file:///path/to/crate#1.2.3")?,
PathBuf::from("/path/to/crate")
);
Ok(())
}
#[test]
fn resolve_multi_level_feature_dependencies() {
let chosen = vec!["default".to_string(), "extra".to_string(), "on_by_default".to_string()];
let package_features = [
(
"default".to_string(),
vec!["std".to_string(), "other".to_string(), "on_by_default".to_string()],
),
("std".to_string(), vec!["alloc".to_string()]),
("not_enabled".to_string(), vec![]),
("on_by_default".to_string(), vec![]),
("other".to_string(), vec![]),
("extra".to_string(), vec![]),
("alloc".to_string(), vec![]),
]
.into_iter()
.collect();
assert_eq!(
resolve_features(&Some(chosen), &package_features, &[]),
vec![
"alloc".to_string(),
"default".to_string(),
"extra".to_string(),
"on_by_default".to_string(),
"other".to_string(),
"std".to_string(),
]
);
}
#[test]
fn resolve_dep_features() {
let package_features = [(
"default".to_string(),
vec![
"optionaldep/feature".to_string(),
"requireddep/feature".to_string(),
"optionaldep2?/feature".to_string(),
],
)]
.into_iter()
.collect();
let dependencies = vec![
DependencyMetadata {
name: "optionaldep".to_string(),
kind: None,
optional: true,
target: None,
rename: None,
},
DependencyMetadata {
name: "optionaldep2".to_string(),
kind: None,
optional: true,
target: None,
rename: None,
},
DependencyMetadata {
name: "requireddep".to_string(),
kind: None,
optional: false,
target: None,
rename: None,
},
];
assert_eq!(
resolve_features(&None, &package_features, &dependencies),
vec!["default".to_string(), "dep:optionaldep".to_string(), "optionaldep".to_string()]
);
}
#[test]
fn get_externs_cfg() {
let package = PackageMetadata {
name: "test_package".to_string(),
dependencies: vec![
DependencyMetadata {
name: "alwayslib".to_string(),
kind: None,
optional: false,
target: None,
rename: None,
},
DependencyMetadata {
name: "unixlib".to_string(),
kind: None,
optional: false,
target: Some("cfg(unix)".to_string()),
rename: None,
},
DependencyMetadata {
name: "windowslib".to_string(),
kind: None,
optional: false,
target: Some("cfg(windows)".to_string()),
rename: None,
},
],
features: [].into_iter().collect(),
targets: vec![],
..Default::default()
};
let packages = vec![
package.clone(),
PackageMetadata {
name: "alwayslib".to_string(),
targets: vec![TargetMetadata {
name: "alwayslib".to_string(),
kind: vec![TargetKind::Lib],
..Default::default()
}],
..Default::default()
},
PackageMetadata {
name: "unixlib".to_string(),
targets: vec![TargetMetadata {
name: "unixlib".to_string(),
kind: vec![TargetKind::Lib],
..Default::default()
}],
..Default::default()
},
PackageMetadata {
name: "windowslib".to_string(),
targets: vec![TargetMetadata {
name: "windowslib".to_string(),
kind: vec![TargetKind::Lib],
..Default::default()
}],
..Default::default()
},
];
assert_eq!(
get_externs(&package, &packages, &[], &[], &[], false).unwrap(),
vec![
Extern {
name: "alwayslib".to_string(),
lib_name: "alwayslib".to_string(),
raw_name: "alwayslib".to_string(),
extern_type: ExternType::Rust
},
Extern {
name: "unixlib".to_string(),
lib_name: "unixlib".to_string(),
raw_name: "unixlib".to_string(),
extern_type: ExternType::Rust
},
]
);
}
#[test]
fn get_externs_extra_cfg() {
let package = PackageMetadata {
name: "test_package".to_string(),
dependencies: vec![
DependencyMetadata {
name: "foolib".to_string(),
kind: None,
optional: false,
target: Some("cfg(foo)".to_string()),
rename: None,
},
DependencyMetadata {
name: "barlib".to_string(),
kind: None,
optional: false,
target: Some("cfg(bar)".to_string()),
rename: None,
},
],
features: [].into_iter().collect(),
targets: vec![],
..Default::default()
};
let packages = vec![
package.clone(),
PackageMetadata {
name: "foolib".to_string(),
targets: vec![TargetMetadata {
name: "foolib".to_string(),
kind: vec![TargetKind::Lib],
..Default::default()
}],
..Default::default()
},
PackageMetadata {
name: "barlib".to_string(),
targets: vec![TargetMetadata {
name: "barlib".to_string(),
kind: vec![TargetKind::Lib],
..Default::default()
}],
..Default::default()
},
];
assert_eq!(
get_externs(&package, &packages, &[], &["foo".to_string()], &[], false).unwrap(),
vec![Extern {
name: "foolib".to_string(),
lib_name: "foolib".to_string(),
raw_name: "foolib".to_string(),
extern_type: ExternType::Rust
},]
);
}
#[test]
fn get_externs_rename() {
let package = PackageMetadata {
name: "test_package".to_string(),
dependencies: vec![
DependencyMetadata {
name: "foo".to_string(),
kind: None,
optional: false,
target: None,
rename: Some("foo2".to_string()),
},
DependencyMetadata {
name: "bar".to_string(),
kind: None,
optional: true,
target: None,
rename: None,
},
DependencyMetadata {
name: "bar".to_string(),
kind: None,
optional: true,
target: None,
rename: Some("baz".to_string()),
},
],
..Default::default()
};
let packages = vec![
package.clone(),
PackageMetadata {
name: "foo".to_string(),
targets: vec![TargetMetadata {
name: "foo".to_string(),
kind: vec![TargetKind::Lib],
..Default::default()
}],
..Default::default()
},
PackageMetadata {
name: "bar".to_string(),
targets: vec![TargetMetadata {
name: "bar".to_string(),
kind: vec![TargetKind::Lib],
..Default::default()
}],
..Default::default()
},
];
assert_eq!(
get_externs(&package, &packages, &["dep:bar".to_string()], &[], &[], false).unwrap(),
vec![
Extern {
name: "bar".to_string(),
lib_name: "bar".to_string(),
raw_name: "bar".to_string(),
extern_type: ExternType::Rust
},
Extern {
name: "foo2".to_string(),
lib_name: "foo".to_string(),
raw_name: "foo".to_string(),
extern_type: ExternType::Rust
},
]
);
assert_eq!(
get_externs(&package, &packages, &["dep:baz".to_string()], &[], &[], false).unwrap(),
vec![
Extern {
name: "baz".to_string(),
lib_name: "bar".to_string(),
raw_name: "bar".to_string(),
extern_type: ExternType::Rust
},
Extern {
name: "foo2".to_string(),
lib_name: "foo".to_string(),
raw_name: "foo".to_string(),
extern_type: ExternType::Rust
},
]
);
}
#[test]
fn parse_metadata() {
/// Remove anything before "external/rust/crates/" from the
/// `package_dir` field. This makes the test robust since you
/// can use `cargo metadata` to regenerate the test files and
/// you don't have to care about where your AOSP checkout
/// lives.
fn normalize_package_dir(mut c: Crate) -> Crate {
const EXTERNAL_RUST_CRATES: &str = "external/rust/crates/";
let package_dir = c.package_dir.to_str().unwrap();
if let Some(idx) = package_dir.find(EXTERNAL_RUST_CRATES) {
c.package_dir = PathBuf::from(format!(".../{}", &package_dir[idx..]));
}
c
}
for testdata_directory_path in testdata_directories() {
let cfg = Config::from_json_str(
&read_to_string(testdata_directory_path.join("cargo_embargo.json"))
.with_context(|| {
format!(
"Failed to open {:?}",
testdata_directory_path.join("cargo_embargo.json")
)
})
.unwrap(),
)
.unwrap();
let cargo_metadata_path = testdata_directory_path.join("cargo.metadata");
let expected_crates: Vec<Vec<Crate>> = serde_json::from_reader::<_, Vec<Vec<Crate>>>(
File::open(testdata_directory_path.join("crates.json")).unwrap(),
)
.unwrap()
.into_iter()
.map(|crates: Vec<Crate>| crates.into_iter().map(normalize_package_dir).collect())
.collect();
let crates = cfg
.variants
.iter()
.map(|variant_cfg| {
parse_cargo_metadata_str(
&read_to_string(&cargo_metadata_path)
.with_context(|| format!("Failed to open {:?}", cargo_metadata_path))
.unwrap(),
variant_cfg,
)
.unwrap()
.into_iter()
.map(normalize_package_dir)
.collect::<Vec<Crate>>()
})
.collect::<Vec<Vec<Crate>>>();
assert_that!(format!("{crates:#?}"), eq(&format!("{expected_crates:#?}")));
}
}
}