// 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. //! Code for reading configuration json files, usually called `cargo_embargo.json`. //! //! A single configuration file may cover several Rust packages under its directory tree, and may //! have multiple "variants". A variant is a particular configuration for building a set of //! packages, such as what feature flags to enable. Multiple variants are most often used to build //! both a std variant of a library and a no_std variant. Each variant generates one or more modules //! for each package, usually distinguished by a suffix. //! //! The [`Config`] struct has a map of `PackageConfig`s keyed by package name (for options that //! apply across all variants of a package), and a vector of `VariantConfig`s. There must be at //! least one variant for the configuration to generate any output. Each `VariantConfig` has the //! options that apply to that variant across all packages, and then a map of //! `PackageVariantConfig`s for options specific to a particular package of the variant. use anyhow::{bail, Context, Result}; use serde::{Deserialize, Serialize}; use serde_json::{Map, Value}; use std::collections::BTreeMap; use std::path::{Path, PathBuf}; fn default_apex_available() -> Vec<String> { vec!["//apex_available:platform".to_string(), "//apex_available:anyapex".to_string()] } fn is_default_apex_available(apex_available: &[String]) -> bool { apex_available == default_apex_available() } fn default_true() -> bool { true } fn is_true(value: &bool) -> bool { *value } fn is_false(value: &bool) -> bool { !*value } /// Options that apply to everything. #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] #[serde(deny_unknown_fields)] pub struct Config { pub variants: Vec<VariantConfig>, /// Package specific config options across all variants. #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] pub package: BTreeMap<String, PackageConfig>, } /// Inserts entries from `defaults` into `variant` if neither it nor `ignored_fields` contain /// matching keys. fn add_defaults_to_variant( variant: &mut Map<String, Value>, defaults: &Map<String, Value>, ignored_fields: &[&str], ) { for (key, value) in defaults { if !ignored_fields.contains(&key.as_str()) && !variant.contains_key(key) { variant.insert(key.to_owned(), value.to_owned()); } } } impl Config { /// Names of all fields in [`Config`] other than `variants` (which is treated specially). const FIELD_NAMES: [&'static str; 1] = ["package"]; /// Parses an instance of this config from the given JSON file. pub fn from_file(filename: &Path) -> Result<Self> { let json_string = std::fs::read_to_string(filename) .with_context(|| format!("failed to read file: {:?}", filename))?; Self::from_json_str(&json_string) } /// Parses an instance of this config from a string of JSON. pub fn from_json_str(json_str: &str) -> Result<Self> { // Ignore comments. let json_str: String = json_str.lines().filter(|l| !l.trim_start().starts_with("//")).collect(); // First parse into untyped map. let mut config: Map<String, Value> = serde_json::from_str(&json_str).context("failed to parse config")?; // Flatten variants. First, get the variants from the config file. let mut variants = match config.remove("variants") { Some(Value::Array(v)) => v, Some(_) => bail!("Failed to parse config: variants is not an array"), None => { // There are no variants, so just put everything into a single variant. vec![Value::Object(Map::new())] } }; // Set default values in variants from top-level config. for variant in &mut variants { let variant = variant .as_object_mut() .context("Failed to parse config: variant is not an object")?; add_defaults_to_variant(variant, &config, &Config::FIELD_NAMES); if let Some(packages) = config.get("package") { // Copy package entries across. let variant_packages = variant .entry("package") .or_insert_with(|| Map::new().into()) .as_object_mut() .context("Failed to parse config: variant package is not an object")?; for (package_name, package_config) in packages .as_object() .context("Failed to parse config: package is not an object")? { let variant_package = variant_packages .entry(package_name) .or_insert_with(|| Map::new().into()) .as_object_mut() .context( "Failed to parse config: variant package config is not an object", )?; add_defaults_to_variant( variant_package, package_config .as_object() .context("Failed to parse config: package is not an object")?, &PackageConfig::FIELD_NAMES, ); } } } // Remove other entries from the top-level config, and put variants back. config.retain(|key, _| Self::FIELD_NAMES.contains(&key.as_str())); if let Some(package) = config.get_mut("package") { for value in package .as_object_mut() .context("Failed to parse config: package is not an object")? .values_mut() { let package_config = value .as_object_mut() .context("Failed to parse config: package is not an object")?; package_config.retain(|key, _| PackageConfig::FIELD_NAMES.contains(&key.as_str())) } } config.insert("variants".to_string(), Value::Array(variants)); // Parse into `Config` struct. serde_json::from_value(Value::Object(config)).context("failed to parse config") } /// Serializes an instance of this config to a string of pretty-printed JSON. pub fn to_json_string(&self) -> Result<String> { // First convert to an untyped map. let Value::Object(mut config) = serde_json::to_value(self)? else { panic!("Config wasn't a map."); }; // Factor out common options which are set for all variants. let Value::Array(mut variants) = config.remove("variants").unwrap() else { panic!("variants wasn't an array.") }; let mut packages = if let Some(Value::Object(packages)) = config.remove("package") { packages } else { Map::new() }; for (key, value) in variants[0].as_object().unwrap() { if key == "package" { for (package_name, package_config) in value.as_object().unwrap() { for (package_key, package_value) in package_config.as_object().unwrap() { // Check whether all other variants have the same entry for the same package. if variants[1..variants.len()].iter().all(|variant| { if let Some(Value::Object(variant_packages)) = variant.as_object().unwrap().get("package") { if let Some(Value::Object(variant_package_config)) = variant_packages.get(package_name) { return variant_package_config.get(package_key) == Some(package_value); } } false }) { packages .entry(package_name) .or_insert_with(|| Map::new().into()) .as_object_mut() .unwrap() .insert(package_key.to_owned(), package_value.to_owned()); } } } } else { // Check whether all the other variants have the same entry. if variants[1..variants.len()] .iter() .all(|variant| variant.as_object().unwrap().get(key) == Some(value)) { // Add it to the top-level config. config.insert(key.to_owned(), value.to_owned()); } } } // Remove factored out common options from all variants. for key in config.keys() { for variant in &mut variants { variant.as_object_mut().unwrap().remove(key); } } // Likewise, remove package options factored out from variants. for (package_name, package_config) in &packages { for package_key in package_config.as_object().unwrap().keys() { for variant in &mut variants { if let Some(Value::Object(variant_packages)) = variant.get_mut("package") { if let Some(Value::Object(variant_package_config)) = variant_packages.get_mut(package_name) { variant_package_config.remove(package_key); } } } } } // Remove any variant packages which are now empty. for variant in &mut variants { if let Some(Value::Object(variant_packages)) = variant.get_mut("package") { variant_packages .retain(|_, package_config| !package_config.as_object().unwrap().is_empty()); if variant_packages.is_empty() { variant.as_object_mut().unwrap().remove("package"); } } } // Put packages and variants back into the top-level config. if variants.len() > 1 || !variants[0].as_object().unwrap().is_empty() { config.insert("variants".to_string(), Value::Array(variants)); } if !packages.is_empty() { config.insert("package".to_string(), Value::Object(packages)); } // Serialise the map into a JSON string. serde_json::to_string_pretty(&config).context("failed to serialize config") } } #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] #[serde(deny_unknown_fields)] pub struct VariantConfig { /// Whether to output `rust_test` modules. #[serde(default, skip_serializing_if = "is_false")] pub tests: bool, /// Set of features to enable. If not set, uses the default crate features. #[serde(skip_serializing_if = "Option::is_none")] pub features: Option<Vec<String>>, /// Whether to build with `--workspace`. #[serde(default, skip_serializing_if = "is_false")] pub workspace: bool, /// When workspace is enabled, list of `--exclude` crates. #[serde(default, skip_serializing_if = "Vec::is_empty")] pub workspace_excludes: Vec<String>, /// Value to use for every generated module's `defaults` field. #[serde(skip_serializing_if = "Option::is_none")] pub global_defaults: Option<String>, /// Value to use for every generated library module's `apex_available` field. #[serde(default = "default_apex_available", skip_serializing_if = "is_default_apex_available")] pub apex_available: Vec<String>, /// Value to use for every generated library module's `native_bridge_supported` field. #[serde(default, skip_serializing_if = "is_false")] pub native_bridge_supported: bool, /// Value to use for every generated library module's `product_available` field. #[serde(default = "default_true", skip_serializing_if = "is_true")] pub product_available: bool, /// Value to use for every generated library module's `ramdisk_available` field. #[serde(default, skip_serializing_if = "is_false")] pub ramdisk_available: bool, /// Value to use for every generated library module's `recovery_available` field. #[serde(default, skip_serializing_if = "is_false")] pub recovery_available: bool, /// Value to use for every generated library module's `vendor_available` field. #[serde(default = "default_true", skip_serializing_if = "is_true")] pub vendor_available: bool, /// Value to use for every generated library module's `vendor_ramdisk_available` field. #[serde(default, skip_serializing_if = "is_false")] pub vendor_ramdisk_available: bool, /// Minimum SDK version. #[serde(skip_serializing_if = "Option::is_none")] pub min_sdk_version: Option<String>, /// Map of renames for modules. For example, if a "libfoo" would be generated and there is an /// entry ("libfoo", "libbar"), the generated module will be called "libbar" instead. /// /// Also, affects references to dependencies (e.g. in a "static_libs" list), even those outside /// the project being processed. #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] pub module_name_overrides: BTreeMap<String, String>, /// Package specific config options. #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] pub package: BTreeMap<String, PackageVariantConfig>, /// `cfg` flags in this list will not be included. #[serde(default, skip_serializing_if = "Vec::is_empty")] pub cfg_blocklist: Vec<String>, /// Extra `cfg` flags to enable in output modules. #[serde(default, skip_serializing_if = "Vec::is_empty")] pub extra_cfg: Vec<String>, /// Modules in this list will not be generated. #[serde(default, skip_serializing_if = "Vec::is_empty")] pub module_blocklist: Vec<String>, /// Modules name => Soong "visibility" property. #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] pub module_visibility: BTreeMap<String, Vec<String>>, /// Whether to run the cargo build and parse its output, rather than just figuring things out /// from the cargo metadata. #[serde(default = "default_true", skip_serializing_if = "is_true")] pub run_cargo: bool, /// Generate an Android.bp build file for this variant if true. #[serde(default = "default_true", skip_serializing_if = "is_true")] pub generate_androidbp: bool, /// Generate a rules.mk build file for this variant if true. #[serde(default, skip_serializing_if = "is_false")] pub generate_rulesmk: bool, } impl Default for VariantConfig { fn default() -> Self { Self { tests: false, features: Default::default(), workspace: false, workspace_excludes: Default::default(), global_defaults: None, apex_available: default_apex_available(), native_bridge_supported: false, product_available: true, ramdisk_available: false, recovery_available: false, vendor_available: true, vendor_ramdisk_available: false, min_sdk_version: None, module_name_overrides: Default::default(), package: Default::default(), cfg_blocklist: Default::default(), extra_cfg: Default::default(), module_blocklist: Default::default(), module_visibility: Default::default(), run_cargo: true, generate_androidbp: true, generate_rulesmk: false, } } } /// Options that apply to everything in a package (i.e. everything associated with a particular /// Cargo.toml file), for all variants. #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(deny_unknown_fields)] pub struct PackageConfig { /// File with content to append to the end of the generated Android.bp. #[serde(skip_serializing_if = "Option::is_none")] pub add_toplevel_block: Option<PathBuf>, /// Patch file to apply after Android.bp is generated. #[serde(skip_serializing_if = "Option::is_none")] pub patch: Option<PathBuf>, /// Patch file to apply after rules.mk is generated. #[serde(skip_serializing_if = "Option::is_none")] pub rulesmk_patch: Option<PathBuf>, } impl PackageConfig { /// Names of all the fields on `PackageConfig`. const FIELD_NAMES: [&'static str; 3] = ["add_toplevel_block", "patch", "rulesmk_patch"]; } /// Options that apply to everything in a package (i.e. everything associated with a particular /// Cargo.toml file), for a particular variant. #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] #[serde(deny_unknown_fields)] pub struct PackageVariantConfig { /// Link against `alloc`. Only valid if `no_std` is also true. #[serde(default, skip_serializing_if = "is_false")] pub alloc: bool, /// Whether to compile for device. Defaults to true. #[serde(default = "default_true", skip_serializing_if = "is_true")] pub device_supported: bool, /// Whether to compile for host. Defaults to true. #[serde(default = "default_true", skip_serializing_if = "is_true")] pub host_supported: bool, /// Add a `compile_multilib: "first"` property to host modules. #[serde(default, skip_serializing_if = "is_false")] pub host_first_multilib: bool, /// Generate "rust_library_rlib" instead of "rust_library". #[serde(default, skip_serializing_if = "is_false")] pub force_rlib: bool, /// Whether to disable "unit_test" for "rust_test" modules. // TODO: Should probably be a list of modules or crates. A package might have a mix of unit and // integration tests. #[serde(default, skip_serializing_if = "is_false")] pub no_presubmit: bool, /// File with content to append to the end of each generated module. #[serde(skip_serializing_if = "Option::is_none")] pub add_module_block: Option<PathBuf>, /// Modules in this list will not be added as dependencies of generated modules. #[serde(default, skip_serializing_if = "Vec::is_empty")] pub dep_blocklist: Vec<String>, /// Don't link against `std`, only `core`. #[serde(default, skip_serializing_if = "is_false")] pub no_std: bool, /// Copy build.rs output to ./out/* and add a genrule to copy ./out/* to genrule output. /// For crates with code pattern: /// include!(concat!(env!("OUT_DIR"), "/<some_file>.rs")) #[serde(default, skip_serializing_if = "is_false")] pub copy_out: bool, /// Add the given files to the given tests' `data` property. The key is the test source filename /// relative to the crate root. #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] pub test_data: BTreeMap<String, Vec<String>>, /// Static libraries in this list will instead be added as whole_static_libs. #[serde(default, skip_serializing_if = "Vec::is_empty")] pub whole_static_libs: Vec<String>, /// Directories with headers to export for C usage. #[serde(default, skip_serializing_if = "Vec::is_empty")] pub exported_c_header_dir: Vec<PathBuf>, } impl Default for PackageVariantConfig { fn default() -> Self { Self { alloc: false, device_supported: true, host_supported: true, host_first_multilib: false, force_rlib: false, no_presubmit: false, add_module_block: None, dep_blocklist: Default::default(), no_std: false, copy_out: false, test_data: Default::default(), whole_static_libs: Default::default(), exported_c_header_dir: Default::default(), } } } #[cfg(test)] mod tests { use super::*; #[test] fn variant_config() { let config = Config::from_json_str( r#"{ "tests": true, "package": { "argh": { "patch": "patches/Android.bp.patch" }, "another": { "add_toplevel_block": "block.bp", "device_supported": false, "force_rlib": true }, "rulesmk": { "rulesmk_patch": "patches/rules.mk.patch" } }, "variants": [ {}, { "generate_androidbp": false, "generate_rulesmk": true, "tests": false, "features": ["feature"], "vendor_available": false, "package": { "another": { "alloc": false, "force_rlib": false }, "variant_package": { "add_module_block": "variant_module_block.bp" } } } ] }"#, ) .unwrap(); assert_eq!( config, Config { variants: vec![ VariantConfig { generate_androidbp: true, generate_rulesmk: false, tests: true, features: None, vendor_available: true, package: [ ("argh".to_string(), PackageVariantConfig { ..Default::default() }), ( "another".to_string(), PackageVariantConfig { device_supported: false, force_rlib: true, ..Default::default() }, ), ("rulesmk".to_string(), PackageVariantConfig { ..Default::default() }), ] .into_iter() .collect(), ..Default::default() }, VariantConfig { generate_androidbp: false, generate_rulesmk: true, tests: false, features: Some(vec!["feature".to_string()]), vendor_available: false, package: [ ("argh".to_string(), PackageVariantConfig { ..Default::default() }), ( "another".to_string(), PackageVariantConfig { alloc: false, device_supported: false, force_rlib: false, ..Default::default() }, ), ("rulesmk".to_string(), PackageVariantConfig { ..Default::default() }), ( "variant_package".to_string(), PackageVariantConfig { add_module_block: Some("variant_module_block.bp".into()), ..Default::default() }, ), ] .into_iter() .collect(), ..Default::default() }, ], package: [ ( "argh".to_string(), PackageConfig { patch: Some("patches/Android.bp.patch".into()), ..Default::default() }, ), ( "another".to_string(), PackageConfig { add_toplevel_block: Some("block.bp".into()), ..Default::default() }, ), ( "rulesmk".to_string(), PackageConfig { rulesmk_patch: Some("patches/rules.mk.patch".into()), ..Default::default() }, ), ] .into_iter() .collect(), } ); } /// Tests that variant configuration options are factored out to the top level where possible. #[test] fn factor_variants() { let config = Config { variants: vec![ VariantConfig { features: Some(vec![]), tests: true, vendor_available: false, package: [( "argh".to_string(), PackageVariantConfig { dep_blocklist: vec!["bad_dep".to_string()], ..Default::default() }, )] .into_iter() .collect(), ..Default::default() }, VariantConfig { features: Some(vec![]), tests: true, product_available: false, module_name_overrides: [("argh".to_string(), "argh_nostd".to_string())] .into_iter() .collect(), vendor_available: false, package: [( "argh".to_string(), PackageVariantConfig { dep_blocklist: vec!["bad_dep".to_string()], no_std: true, ..Default::default() }, )] .into_iter() .collect(), ..Default::default() }, ], package: [( "argh".to_string(), PackageConfig { add_toplevel_block: Some("block.bp".into()), ..Default::default() }, )] .into_iter() .collect(), }; assert_eq!( config.to_json_string().unwrap(), r#"{ "features": [], "package": { "argh": { "add_toplevel_block": "block.bp", "dep_blocklist": [ "bad_dep" ] } }, "tests": true, "variants": [ {}, { "module_name_overrides": { "argh": "argh_nostd" }, "package": { "argh": { "no_std": true } }, "product_available": false } ], "vendor_available": false }"# ); } #[test] fn factor_trivial_variant() { let config = Config { variants: vec![VariantConfig { tests: true, package: [("argh".to_string(), Default::default())].into_iter().collect(), ..Default::default() }], package: Default::default(), }; assert_eq!( config.to_json_string().unwrap(), r#"{ "tests": true }"# ); } }