// Copyright (C) 2022 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.
//! Converts a cargo project to Soong. //! //! Forked from development/scripts/cargo2android.py. Missing many of its features. Adds various //! features to make it easier to work with projects containing many crates. //! //! At a high level, this is done by //! //! 1. Running `cargo build -v` and saving the output to a "cargo.out" file. //! 2. Parsing the "cargo.out" file to find invocations of compilers, e.g. `rustc` and `cc`. //! 3. For each compiler invocation, generating a equivalent Soong module, e.g. a "rust_library". //! //! The last step often involves messy, project specific business logic, so many options are //! available to tweak it via a config file.
mod bp; mod cargo; mod config;
usecrate::config::Config; usecrate::config::PackageConfig; usecrate::config::PackageVariantConfig; usecrate::config::VariantConfig; use anyhow::anyhow; use anyhow::bail; use anyhow::Context; use anyhow::Result; use bp::*; use cargo::{
cargo_out::parse_cargo_out, metadata::parse_cargo_metadata_str, Crate, CrateType, ExternType,
}; use clap::Parser; use clap::Subcommand; use log::debug; #[cfg(not(target_os = "macos"))] use nix::{fcntl::OFlag, unistd::pipe2}; #[cfg(target_os = "macos")] use nix::{
fcntl::{fcntl, FdFlag},
unistd::pipe,
}; use std::collections::BTreeMap; use std::collections::VecDeque; use std::env; use std::fs::{read_to_string, write, File}; use std::io::{Read, Write}; #[cfg(target_os = "macos")] use std::os::fd::AsRawFd; use std::path::Path; use std::path::PathBuf; use std::process::{Command, Stdio}; use std::sync::LazyLock; use tempfile::tempdir;
// Major TODOs // * handle errors, esp. in cargo.out parsing. they should fail the program with an error code // * handle warnings. put them in comments in the android.bp, some kind of report section
/// This map tracks Rust crates that have special rules.mk modules that were not /// generated automatically by this script. Examples include compiler builtins /// and other foundational libraries. It also tracks the location of rules.mk /// build files for crates that are not under external/rust/crates. pubstatic RULESMK_RENAME_MAP: LazyLock<BTreeMap<&str, &str>> = LazyLock::new(|| {
[
("liballoc", "trusty/user/base/lib/liballoc-rust"),
("libcompiler_builtins", "trusty/user/base/lib/libcompiler_builtins-rust"),
("libcore", "trusty/user/base/lib/libcore-rust"),
("libhashbrown", "trusty/user/base/lib/libhashbrown-rust"),
("libpanic_abort", "trusty/user/base/lib/libpanic_abort-rust"),
("libstd", "trusty/user/base/lib/libstd-rust"),
("libstd_detect", "trusty/user/base/lib/libstd_detect-rust"),
("libunwind", "trusty/user/base/lib/libunwind-rust"),
]
.into_iter()
.collect()
});
/// Given a proposed module name, returns `None` if it is blocked by the given config, or /// else apply any name overrides and returns the name to use. fn override_module_name(
module_name: &str,
blocklist: &[String],
module_name_overrides: &BTreeMap<String, String>,
rename_map: &BTreeMap<&str, &str>,
) -> Option<String> { if blocklist.iter().any(|blocked_name| blocked_name == module_name) {
None
} elseiflet Some(overridden_name) = module_name_overrides.get(module_name) {
Some(overridden_name.to_string())
} elseiflet Some(renamed) = rename_map.get(module_name) {
Some(renamed.to_string())
} else {
Some(module_name.to_string())
}
}
/// Command-line parameters for `cargo_embargo`. #[derive(Parser, Debug)] struct Args { /// Use the cargo binary in the `cargo_bin` directory. Defaults to using the Android prebuilt. #[clap(long)]
cargo_bin: Option<PathBuf>, /// Store `cargo build` output in this directory. If not set, a temporary directory is created and used. #[clap(long)]
cargo_out_dir: Option<PathBuf>, /// Skip the `cargo build` commands and reuse the "cargo.out" file from a previous run if /// available. Requires setting --cargo_out_dir. #[clap(long)]
reuse_cargo_out: bool, #[command(subcommand)]
mode: Mode,
}
#[derive(Clone, Debug, Subcommand)] enum Mode { /// Generates `Android.bp` files for the crates under the current directory using the given /// config file.
Generate { /// `cargo_embargo.json` config file to use.
config: PathBuf,
}, /// Dumps information about the crates to the given JSON file.
DumpCrates { /// `cargo_embargo.json` config file to use.
config: PathBuf, /// Path to `crates.json` to output.
crates: PathBuf,
}, /// Tries to automatically generate a suitable `cargo_embargo.json` config file for the package /// in the current directory.
Autoconfig { /// `cargo_embargo.json` config file to create.
config: PathBuf,
},
}
fn main() -> Result<()> {
env_logger::init(); let args = Args::parse();
if args.reuse_cargo_out && args.cargo_out_dir.is_none() { return Err(anyhow!("Must specify --cargo_out_dir with --reuse_cargo_out"));
} let tempdir = tempdir()?; let intermediates_dir = args.cargo_out_dir.as_deref().unwrap_or(tempdir.path());
/// Runs cargo_embargo with the given JSON configuration string, but dumps the crate data to the /// given `crates.json` file rather than generating an `Android.bp`. fn dump_crates(
args: &Args,
config_filename: &Path,
crates_filename: &Path,
intermediates_dir: &Path,
) -> Result<()> { let cfg = Config::from_file(config_filename)?; let crates = make_all_crates(args, &cfg, intermediates_dir)?;
serde_json::to_writer(
File::create(crates_filename)
.with_context(|| format!("Failed to create {crates_filename:?}"))?,
&crates,
)?;
Ok(())
}
/// Tries to automatically generate a suitable `cargo_embargo.json` for the package in the current /// directory. fn autoconfig(args: &Args, config_filename: &Path, intermediates_dir: &Path) -> Result<()> {
println!("Trying default config with tests..."); letmut config_with_build = Config {
variants: vec![VariantConfig { tests: true, ..Default::default() }],
package: Default::default(),
}; letmut crates_with_build = make_all_crates(args, &config_with_build, intermediates_dir)?;
let has_tests =
crates_with_build[0].iter().any(|c| c.types.contains(&CrateType::Test) && !c.empty_test); if !has_tests {
println!("No tests, removing from config.");
config_with_build =
Config { variants: vec![Default::default()], package: Default::default() };
crates_with_build = make_all_crates(args, &config_with_build, intermediates_dir)?;
}
println!("Trying without cargo build..."); let config_no_build = Config {
variants: vec![VariantConfig { run_cargo: false, tests: has_tests, ..Default::default() }],
package: Default::default(),
}; let crates_without_build = make_all_crates(args, &config_no_build, intermediates_dir)?;
let config = if crates_with_build == crates_without_build {
println!("Output without build was the same, using that.");
config_no_build
} else {
println!("Output without build was different. Need to run cargo build.");
println!("With build: {}", serde_json::to_string_pretty(&crates_with_build)?);
println!("Without build: {}", serde_json::to_string_pretty(&crates_without_build)?);
config_with_build
};
write(config_filename, format!("{}\n", config.to_json_string()?))?;
println!( "Wrote config to {0}. Run `cargo_embargo generate {0}` to use it.",
config_filename.to_string_lossy()
);
Ok(())
}
/// Finds the path to the directory containing the Android prebuilt Rust toolchain. fn find_android_rust_toolchain() -> Result<PathBuf> { let platform_rustfmt = if cfg!(all(target_arch = "x86_64", target_os = "linux")) { "linux-x86/stable/rustfmt"
} elseif cfg!(all(target_arch = "x86_64", target_os = "macos")) { "darwin-x86/stable/rustfmt"
} elseif cfg!(all(target_arch = "x86_64", target_os = "windows")) { "windows-x86/stable/rustfmt.exe"
} else {
bail!("No prebuilt Rust toolchain available for this platform.");
};
let android_top = env::var("ANDROID_BUILD_TOP")
.context("ANDROID_BUILD_TOP was not set. Did you forget to run envsetup.sh?")?; let stable_rustfmt = [android_top.as_str(), "prebuilts", "rust-toolchain", platform_rustfmt]
.into_iter()
.collect::<PathBuf>(); let canonical_rustfmt = stable_rustfmt.canonicalize()?;
Ok(canonical_rustfmt.parent().unwrap().to_owned())
}
/// Adds the given path to the start of the `PATH` environment variable. fn add_to_path(extra_path: PathBuf) -> Result<()> { let path = env::var_os("PATH").unwrap(); letmut paths = env::split_paths(&path).collect::<VecDeque<_>>();
paths.push_front(extra_path); let new_path = env::join_paths(paths)?;
debug!("Set PATH to {new_path:?}");
std::env::set_var("PATH", new_path);
Ok(())
}
/// Calls make_crates for each variant in the given config. fn make_all_crates(args: &Args, cfg: &Config, intermediates_dir: &Path) -> Result<Vec<Vec<Crate>>> {
cfg.variants.iter().map(|variant| make_crates(args, variant, intermediates_dir)).collect()
}
fn make_crates(args: &Args, cfg: &VariantConfig, intermediates_dir: &Path) -> Result<Vec<Crate>> { if !Path::new("Cargo.toml").try_exists().context("when checking Cargo.toml")? {
bail!("Cargo.toml missing. Run in a directory with a Cargo.toml file.");
}
// Add the custom cargo to PATH. // NOTE: If the directory with cargo has more binaries, this could have some unpredictable side // effects. That is partly intended though, because we want to use that cargo binary's // associated rustc. let cargo_bin = iflet Some(cargo_bin) = &args.cargo_bin {
cargo_bin.to_owned()
} else { // Find the Android prebuilt.
find_android_rust_toolchain()?
};
add_to_path(cargo_bin)?;
let cargo_out_path = intermediates_dir.join("cargo.out"); let cargo_metadata_path = intermediates_dir.join("cargo.metadata"); let cargo_output = if args.reuse_cargo_out && cargo_out_path.exists() {
CargoOutput {
cargo_out: read_to_string(cargo_out_path)?,
cargo_metadata: read_to_string(cargo_metadata_path)?,
}
} else { let cargo_output =
generate_cargo_out(cfg, intermediates_dir).context("generate_cargo_out failed")?; if cfg.run_cargo {
write(cargo_out_path, &cargo_output.cargo_out)?;
}
write(cargo_metadata_path, &cargo_output.cargo_metadata)?;
cargo_output
};
/// Runs cargo_embargo with the given JSON configuration file. fn run_embargo(args: &Args, config_filename: &Path, intermediates_dir: &Path) -> Result<()> { let intermediates_glob = intermediates_dir
.to_str()
.ok_or(anyhow!("Failed to convert intermediate dir path to string"))?
.to_string()
+ "/target.tmp/**/build/*/out/*";
let cfg = Config::from_file(config_filename)?; let crates = make_all_crates(args, &cfg, intermediates_dir)?;
// TODO: Use different directories for different variants. // Find out files. // Example: target.tmp/x86_64-unknown-linux-gnu/debug/build/metrics-d2dd799cebf1888d/out/event_details.rs let num_variants = cfg.variants.len(); letmut package_out_files: BTreeMap<String, Vec<Vec<PathBuf>>> = BTreeMap::new(); for (variant_index, variant_cfg) in cfg.variants.iter().enumerate() { if variant_cfg.package.iter().any(|(_, v)| v.copy_out) { for entry in glob::glob(&intermediates_glob)? { match entry {
Ok(path) => { let package_name = || -> Option<_> { let dir_name = path.parent()?.parent()?.file_name()?.to_str()?;
Some(dir_name.rsplit_once('-')?.0)
}()
.unwrap_or_else(|| panic!("failed to parse out file path: {path:?}"));
package_out_files
.entry(package_name.to_string())
.or_insert_with(|| vec![vec![]; num_variants])[variant_index]
.push(path.clone());
}
Err(e) => eprintln!("failed to check for out files: {e}"),
}
}
}
}
// If we were configured to run cargo, check whether we could have got away without it. if cfg.variants.iter().any(|variant| variant.run_cargo) && package_out_files.is_empty() { letmut cfg_no_cargo = cfg.clone(); for variant in &mut cfg_no_cargo.variants {
variant.run_cargo = false;
} let crates_no_cargo = make_all_crates(args, &cfg_no_cargo, intermediates_dir)?; if crates_no_cargo == crates {
eprintln!("Running cargo appears to be unnecessary for this crate, consider adding `\"run_cargo\": false` to your cargo_embargo.json.");
}
}
/// Input is indexed by variant, then all crates for that variant. /// Output is a map from package directory to a list of variants, with all crates for that package /// and variant. fn group_by_package(crates: Vec<Vec<Crate>>) -> BTreeMap<PathBuf, Vec<Vec<Crate>>> { letmut module_by_package: BTreeMap<PathBuf, Vec<Vec<Crate>>> = BTreeMap::new();
let num_variants = crates.len(); for (i, variant_crates) in crates.into_iter().enumerate() { for c in variant_crates { let package_variants = module_by_package
.entry(c.package_dir.clone())
.or_insert_with(|| vec![vec![]; num_variants]);
package_variants[i].push(c);
}
}
module_by_package
}
fn write_all_build_files(
cfg: &Config,
crates: Vec<Vec<Crate>>,
package_out_files: &BTreeMap<String, Vec<Vec<PathBuf>>>,
) -> Result<()> { // Group by package. let module_by_package = group_by_package(crates);
let num_variants = cfg.variants.len(); let empty_package_out_files = vec![vec![]; num_variants]; letmut has_error = false; // Write a build file per package. for (package_dir, crates) in module_by_package { let package_name = &crates.iter().flatten().next().unwrap().package_name; iflet Err(e) = write_build_files(
cfg,
package_name,
package_dir,
&crates,
package_out_files.get(package_name).unwrap_or(&empty_package_out_files),
) { // print the error, but continue to accumulate all of the errors
eprintln!("ERROR: {e:#}");
has_error = true;
}
} if has_error {
panic!("Encountered fatal errors that must be fixed.");
}
Ok(())
}
/// Runs the given command, and returns its standard output and (optionally) standard error as a string. fn run_cargo(cmd: &mut Command, include_stderr: bool) -> Result<String> { #[cfg(target_os = "macos")] let (pipe_read, pipe_write) = { let (pipe_read, pipe_write) = pipe()?;
fcntl(pipe_read.as_raw_fd(), nix::fcntl::F_SETFD(FdFlag::FD_CLOEXEC))?;
fcntl(pipe_write.as_raw_fd(), nix::fcntl::F_SETFD(FdFlag::FD_CLOEXEC))?;
(pipe_read, pipe_write)
}; #[cfg(not(target_os = "macos"))] let (pipe_read, pipe_write) = pipe2(OFlag::O_CLOEXEC)?; if include_stderr {
cmd.stderr(pipe_write.try_clone()?);
}
cmd.stdout(pipe_write).stdin(Stdio::null());
debug!("Running: {cmd:?}\n"); letmut child = cmd.spawn()?;
// Unset the stdout and stderr for the command so that they are dropped in this process. // Otherwise the `read_to_string` below will block forever as there is still an open write file // descriptor for the pipe even after the child finishes.
cmd.stderr(Stdio::null()).stdout(Stdio::null());
letmut output = String::new();
File::from(pipe_read).read_to_string(&mut output)?; let status = child.wait()?; if !status.success() {
bail!( "cargo command `{:?}` failed with exit status: {:?}.\nOutput: \n------\n{}\n------",
cmd,
status,
output
);
}
Ok(output)
}
/// The raw output from running `cargo metadata`, `cargo build` and other commands. #[derive(Clone, Debug, Eq, PartialEq)] pubstruct CargoOutput {
cargo_metadata: String,
cargo_out: String,
}
/// Run various cargo commands and returns the output. fn generate_cargo_out(cfg: &VariantConfig, intermediates_dir: &Path) -> Result<CargoOutput> { let verbose_args = ["-v"]; let target_dir = intermediates_dir.join("target.tmp");
let default_target = "x86_64-unknown-linux-gnu"; let feature_args = iflet Some(features) = &cfg.features { if features.is_empty() {
vec!["--no-default-features".to_string()]
} else {
vec!["--no-default-features".to_string(), "--features".to_string(), features.join(",")]
}
} else {
vec![]
};
let workspace_args = if cfg.workspace { letmut v = vec!["--workspace".to_string()]; if !cfg.workspace_excludes.is_empty() { for x in cfg.workspace_excludes.iter() {
v.push("--exclude".to_string());
v.push(x.clone());
}
}
v
} else {
vec![]
};
/// Read and return license and other header lines from a build file. /// /// Skips initial comment lines, then returns all lines before the first line /// starting with `rust_`, `genrule {`, or `LOCAL_DIR`. /// /// If `path` could not be read and a license is required, return a /// placeholder license TODO line. fn read_license_header(path: &Path, require_license: bool) -> Result<String> { // Keep the old license header. match std::fs::read_to_string(path) {
Ok(s) => Ok(s
.lines()
.skip_while(|l| l.starts_with("//") || l.starts_with('#'))
.take_while(|l| {
!l.starts_with("rust_")
&& !l.starts_with("genrule {")
&& !l.starts_with("LOCAL_DIR")
})
.collect::<Vec<&str>>()
.join("\n")),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => { let placeholder = if require_license { "// DO NOT SUBMIT: Add license before submitting.\n"
} else { ""
};
Ok(placeholder.to_string())
}
Err(e) => Err(anyhow!("error when reading {path:?}: {e}")),
}
}
/// Create the build file for `package_dir`. /// /// `crates` and `out_files` are both indexed by variant. fn write_build_files(
cfg: &Config,
package_name: &str,
package_dir: PathBuf,
crates: &[Vec<Crate>],
out_files: &[Vec<PathBuf>],
) -> Result<()> {
assert_eq!(crates.len(), out_files.len());
letmut bp_modules = Vec::new(); letmut mk_contents = String::new(); for (variant_index, variant_config) in cfg.variants.iter().enumerate() { let variant_crates = &crates[variant_index]; let def = PackageVariantConfig::default(); let package_variant_cfg = variant_config.package.get(package_name).unwrap_or(&def);
// If `copy_out` is enabled and there are any generated out files for the package, copy them to // the appropriate directory. if package_variant_cfg.copy_out && !out_files[variant_index].is_empty() { let out_dir = package_dir.join("out"); if !out_dir.exists() {
std::fs::create_dir(&out_dir).expect("failed to create out dir");
}
for f in out_files[variant_index].iter() { let dest = out_dir.join(f.file_name().unwrap());
std::fs::copy(f, dest).expect("failed to copy out file");
}
}
if variant_config.generate_androidbp {
generate_bp_modules(
variant_config,
package_variant_cfg,
package_name,
variant_crates,
&out_files[variant_index],
&mut bp_modules,
)?;
} if variant_config.generate_rulesmk {
mk_contents += &generate_rules_mk(
variant_config,
package_variant_cfg,
package_name,
variant_crates,
&out_files[variant_index],
)?;
}
} letmut bp_contents = generate_android_bp(bp_modules)?; let main_module_name_overrides = &cfg.variants.first().unwrap().module_name_overrides; if !mk_contents.is_empty() { // If rules.mk is generated, then make it accessible via dirgroup.
bp_contents += &generate_android_bp_for_rules_mk(package_name, main_module_name_overrides)?;
}
let def = PackageConfig::default(); let package_cfg = cfg.package.get(package_name).unwrap_or(&def); iflet Some(path) = &package_cfg.add_toplevel_block {
bp_contents +=
&std::fs::read_to_string(path).with_context(|| format!("failed to read {path:?}"))?;
bp_contents += "\n";
} if !bp_contents.is_empty() { let output_path = package_dir.join("Android.bp"); let package_header = generate_android_bp_package_header(
package_name,
package_cfg,
read_license_header(&output_path, true)?.trim(),
crates,
main_module_name_overrides,
)?; let bp_contents = package_header + &bp_contents;
write_format_android_bp(&output_path, &bp_contents, package_cfg.patch.as_deref())?;
} if !mk_contents.is_empty() { let output_path = package_dir.join("rules.mk"); let mk_contents = "# This file is generated by cargo_embargo.\n".to_owned()
+ "# Do not modify this file after the LOCAL_DIR line\n"
+ "# because the changes will be overridden on upgrade.\n"
+ "# Content before the first line starting with LOCAL_DIR is preserved.\n"
+ read_license_header(&output_path, false)?.trim()
+ "\n"
+ &mk_contents;
File::create(&output_path)?.write_all(mk_contents.as_bytes())?; iflet Some(patch) = package_cfg.rulesmk_patch.as_deref() {
apply_patch_file(&output_path, patch)?;
}
}
letmut bp_contents = "// This file is generated by cargo_embargo.\n".to_owned()
+ "// Do not modify this file because the changes will be overridden on upgrade.\n\n"; for m in modules {
m.write(&mut bp_contents)?;
bp_contents += "\n";
} return Ok(bp_contents);
} else {
eprintln!("Crates have different licenses.");
}
}
}
Ok("// This file is generated by cargo_embargo.\n".to_owned()
+ "// Do not modify this file after the first \"rust_*\" or \"genrule\" module\n"
+ "// because the changes will be overridden on upgrade.\n"
+ "// Content before the first \"rust_*\" or \"genrule\" module is preserved.\n\n"
+ license_header
+ "\n")
}
/// Given an SPDX license expression that may offer a choice between several licenses, choose one or /// more to use. fn choose_licenses(license: &str) -> Result<Vec<&str>> {
Ok(match license { // Variations on "MIT OR Apache-2.0" "MIT OR Apache-2.0" => vec!["Apache-2.0"], "Apache-2.0 OR MIT" => vec!["Apache-2.0"], "MIT/Apache-2.0" => vec!["Apache-2.0"], "Apache-2.0/MIT" => vec!["Apache-2.0"], "Apache-2.0 / MIT" => vec!["Apache-2.0"],
// Variations on "BSD-* OR Apache-2.0" "Apache-2.0 OR BSD-3-Clause" => vec!["Apache-2.0"], "Apache-2.0 or BSD-3-Clause" => vec!["Apache-2.0"], "BSD-3-Clause OR Apache-2.0" => vec!["Apache-2.0"],
// Variations on "BSD-* OR MIT OR Apache-2.0" "BSD-3-Clause OR MIT OR Apache-2.0" => vec!["Apache-2.0"], "BSD-2-Clause OR Apache-2.0 OR MIT" => vec!["Apache-2.0"],
// Variations on "Zlib OR MIT OR Apache-2.0" "Zlib OR Apache-2.0 OR MIT" => vec!["Apache-2.0"], "MIT OR Apache-2.0 OR Zlib" => vec!["Apache-2.0"], "MIT OR Zlib OR Apache-2.0" => vec!["Apache-2.0"],
// Variations on "Apache-2.0 OR *" "Apache-2.0 OR BSL-1.0" => vec!["Apache-2.0"], "Apache-2.0 WITH LLVM-exception OR Apache-2.0 OR MIT" => vec!["Apache-2.0"],
// Variations on "Unlicense OR MIT" "Unlicense OR MIT" => vec!["MIT"], "Unlicense/MIT" => vec!["MIT"],
// Multiple licenses. "(Apache-2.0 OR MIT) AND BSD-3-Clause" => vec!["Apache-2.0", "BSD-3-Clause"], "(MIT OR Apache-2.0) AND Unicode-DFS-2016" => vec!["Apache-2.0", "Unicode-DFS-2016"], "MIT AND BSD-3-Clause" => vec!["BSD-3-Clause", "MIT"], // Usually we interpret "/" as "OR", but in the case of libfuzzer-sys, closer // inspection of the terms indicates the correct interpretation is "(MIT OR APACHE) AND NCSA". "MIT/Apache-2.0/NCSA" => vec!["Apache-2.0", "NCSA"], "(MIT OR Apache-2.0) AND Unicode-3.0" => vec!["Apache-2.0", "Unicode-3.0"], "(MIT OR Apache-2.0) AND NCSA" => vec!["Apache-2.0", "NCSA"], // Variations on "Apache-2.0 AND BSD-*" "Apache-2.0 AND BSD-3-Clause" => vec!["Apache-2.0", "BSD-3-Clause"],
// Other cases. "MIT OR BSD-3-Clause" => vec!["MIT"], "MIT OR LGPL-3.0-or-later" => vec!["MIT"], "MIT/BSD-3-Clause" => vec!["MIT"], "MIT AND (MIT OR Apache-2.0)" => vec!["MIT"], "0BSD OR MIT OR Apache-2.0" => vec!["Apache-2.0"],
"LGPL-2.1-only OR BSD-2-Clause" => vec!["BSD-2-Clause"],
"ISC AND (Apache-2.0 OR ISC)" => vec!["ISC"], "Apache-2.0 OR ISC OR MIT" => vec!["Apache-2.0"],
_ => { // If there is whitespace, it is probably an SPDX expression. if license.contains(char::is_whitespace) {
bail!("Unrecognized license: {license}");
}
vec![license]
}
})
}
/// Generates and returns a Soong Blueprint for the given set of crates, for a single variant of a /// package. fn generate_bp_modules(
cfg: &VariantConfig,
package_cfg: &PackageVariantConfig,
package_name: &str,
crates: &[Crate],
out_files: &[PathBuf],
modules: &mut Vec<BpModule>,
) -> Result<()> { let extra_srcs = if package_cfg.copy_out && !out_files.is_empty() { let outs: Vec<String> = out_files
.iter()
.map(|f| f.file_name().unwrap().to_str().unwrap().to_string())
.collect();
for c in crates {
modules.extend(crate_to_bp_modules(c, cfg, package_cfg, &extra_srcs).with_context(
|| {
format!( "failed to generate bp module for crate \"{}\" with package name \"{}\"",
c.name, c.package_name
)
},
)?);
}
Ok(())
}
fn generate_android_bp(modules: Vec<BpModule>) -> Result<String> { use std::collections::btree_map::Entry;
letmut module_map: BTreeMap<String, BpModule> = BTreeMap::new(); letmut bp_contents = String::new(); for module in modules { match module_map.entry(module.props.get_string("name").unwrap().to_string()) {
Entry::Vacant(v) => {
v.insert(module);
}
Entry::Occupied(mut o) => o.get_mut().merge(module)?,
}
} for m in module_map.values() {
m.write(&mut bp_contents)?;
bp_contents += "\n";
}
Ok(bp_contents)
}
/// Generates and returns a Trusty rules.mk file for the given set of crates. fn generate_rules_mk(
cfg: &VariantConfig,
package_cfg: &PackageVariantConfig,
package_name: &str,
crates: &[Crate],
out_files: &[PathBuf],
) -> Result<String> { let out_files = if package_cfg.copy_out && !out_files.is_empty() {
out_files.iter().map(|f| f.file_name().unwrap().to_str().unwrap().to_string()).collect()
} else {
vec![]
};
let crates: Vec<_> = crates
.iter()
.filter(|c| { if c.types.contains(&CrateType::Bin) {
eprintln!("WARNING: skipped generation of rules.mk for binary crate: {}", c.name); false
} elseif c.types.iter().any(|t| t.is_test()) { // Test build file generation is not yet implemented
eprintln!("WARNING: skipped generation of rules.mk for test crate: {}", c.name); false
} else { true
}
})
.collect(); let [crate_] = &crates[..] else {
bail!( "Expected exactly one library crate for package {package_name} when generating \
rules.mk, found: {crates:?}"
);
};
crate_to_rulesmk(crate_, cfg, package_cfg, &out_files).with_context(|| {
format!( "failed to generate rules.mk for crate \"{}\" with package name \"{}\"",
crate_.name, crate_.package_name
)
})
}
/// Generates and returns a Soong Blueprint for a Trusty rules.mk fn generate_android_bp_for_rules_mk(
package_name: &str,
module_name_overrides: &BTreeMap<String, String>,
) -> Result<String> { letmut bp_contents = String::new();
letmut m = BpModule::new("dirgroup".to_string());
let default_dirgroup_name = format!("trusty_dirgroup_external_rust_crates_{package_name}"); let dirgroup_name =
override_module_name(&default_dirgroup_name, &[], module_name_overrides, &RENAME_MAP)
.unwrap_or(default_dirgroup_name);
m.props.set("name", dirgroup_name);
m.props.set("dirs", vec!["."]);
m.props.set("visibility", vec!["//trusty/vendor/google/aosp/scripts"]);
m.write(&mut bp_contents)?;
bp_contents += "\n";
Ok(bp_contents)
}
/// Apply patch from `patch_path` to file `output_path`. /// /// Warns but still returns ok if the patch did not cleanly apply, fn apply_patch_file(output_path: &Path, patch_path: &Path) -> Result<()> { let patch_output = Command::new("patch")
.arg("-s")
.arg("--no-backup-if-mismatch")
.arg(output_path)
.arg(patch_path)
.output()
.context("Running patch")?; if !patch_output.status.success() { let stdout = String::from_utf8(patch_output.stdout)?; let stderr = String::from_utf8(patch_output.stderr)?; // These errors will cause the cargo_embargo command to fail, but not yet!
bail!("failed to apply patch {patch_path:?}:\n\nout:\n{stdout}\n\nerr:\n{stderr}");
}
Ok(())
}
/// Writes the given contents to the given `Android.bp` file, formats it with `bpfmt`, and applies /// the patch if there is one. fn write_format_android_bp(
bp_path: &Path,
bp_contents: &str,
patch_path: Option<&Path>,
) -> Result<()> {
File::create(bp_path)?.write_all(bp_contents.as_bytes())?;
let bpfmt_output =
Command::new("bpfmt").arg("-w").arg(bp_path).output().context("Running bpfmt")?; if !bpfmt_output.status.success() {
eprintln!( "WARNING: bpfmt -w {:?} failed before patch: {}",
bp_path,
String::from_utf8_lossy(&bpfmt_output.stderr)
);
}
iflet Some(patch_path) = patch_path {
apply_patch_file(bp_path, patch_path)?; // Re-run bpfmt after the patch so let bpfmt_output = Command::new("bpfmt")
.arg("-w")
.arg(bp_path)
.output()
.context("Running bpfmt after patch")?; if !bpfmt_output.status.success() {
eprintln!( "WARNING: bpfmt -w {:?} failed after patch: {}",
bp_path,
String::from_utf8_lossy(&bpfmt_output.stderr)
);
}
}
Ok(())
}
/// Convert a `Crate` into `BpModule`s. /// /// If messy business logic is necessary, prefer putting it here. fn crate_to_bp_modules(
crate_: &Crate,
cfg: &VariantConfig,
package_cfg: &PackageVariantConfig,
extra_srcs: &[String],
) -> Result<Vec<BpModule>> { letmut modules = Vec::new(); for crate_type in &crate_.types { let host = if package_cfg.device_supported { "" } else { "_host" }; let rlib = if package_cfg.force_rlib { "_rlib" } else { "" }; let (module_type, module_name) = match crate_type {
CrateType::Bin => ("rust_binary".to_string() + host, crate_.name.clone()),
CrateType::Lib | CrateType::RLib => { let stem = "lib".to_string() + &crate_.name;
("rust_library".to_string() + host + rlib, stem)
}
CrateType::DyLib => { let stem = "lib".to_string() + &crate_.name;
("rust_library".to_string() + host + "_dylib", stem + "_dylib")
}
CrateType::CDyLib => { let stem = "lib".to_string() + &crate_.name;
("rust_ffi".to_string() + host + "_shared", stem + "_shared")
}
CrateType::StaticLib => { let stem = "lib".to_string() + &crate_.name;
("rust_ffi".to_string() + host + "_static", stem + "_static")
}
CrateType::ProcMacro => { let stem = "lib".to_string() + &crate_.name;
("rust_proc_macro".to_string(), stem)
}
CrateType::Test | CrateType::TestNoHarness => { let suffix = crate_.main_src.to_string_lossy().into_owned(); let suffix = suffix.replace('/', "_").replace(".rs", ""); let stem = crate_.package_name.clone() + "_test_" + &suffix; if crate_.empty_test { return Ok(Vec::new());
} if crate_type == &CrateType::TestNoHarness {
eprintln!( "WARNING: ignoring test \"{stem}\" with harness=false. not supported yet"
); return Ok(Vec::new());
}
("rust_test".to_string() + host, stem)
}
};
letmut m = BpModule::new(module_type.clone()); let Some(module_name) = override_module_name(
&module_name,
&cfg.module_blocklist,
&cfg.module_name_overrides,
&RENAME_MAP,
) else { continue;
}; if matches!(
crate_type,
CrateType::Lib
| CrateType::RLib
| CrateType::DyLib
| CrateType::CDyLib
| CrateType::StaticLib
) && !module_name.starts_with(&format!("lib{}", crate_.name))
{
bail!("Module name must start with lib{} but was {}", crate_.name, module_name);
}
m.props.set("name", module_name.clone());
if !package_cfg.enabled {
m.props.set("enabled", false);
}
// crate dependencies without lib- prefix. Since paths to trusty modules may // contain hyphens, we generate the module path using the raw name output by // cargo metadata or cargo build. letmut library_deps: Vec<_> = crate_.externs.iter().map(|dep| dep.raw_name.clone()).collect(); if package_cfg.no_std {
contents += "MODULE_ADD_IMPLICIT_DEPS := false\n";
library_deps.push("compiler_builtins".to_string());
library_deps.push("core".to_string()); if package_cfg.alloc {
library_deps.push("alloc".to_string());
}
}
#[cfg(test)] mod tests { usesuper::*; use googletest::matchers::eq; use googletest::prelude::assert_that; use std::env::{current_dir, set_current_dir}; use std::fs::{self, read_to_string}; use std::path::PathBuf;
const TESTDATA_PATH: &str = "testdata";
#[test] fn choose_licenses_test() {
assert_eq!(choose_licenses("Apache-2.0").unwrap(), vec!["Apache-2.0"]);
assert_eq!(choose_licenses("MIT OR Apache-2.0").unwrap(), vec!["Apache-2.0"]);
assert_eq!(
choose_licenses("(Apache-2.0 OR MIT) AND BSD-3-Clause").unwrap(),
vec!["Apache-2.0", "BSD-3-Clause"]
);
}
#[test] fn generate_bp() { for testdata_directory_path in testdata_directories() { let cfg = Config::from_json_str(
&read_to_string(testdata_directory_path.join("cargo_embargo.json"))
.expect("Failed to open cargo_embargo.json"),
)
.unwrap(); let crates: Vec<Vec<Crate>> = serde_json::from_reader(
File::open(testdata_directory_path.join("crates.json"))
.expect("Failed to open crates.json"),
)
.unwrap(); let expected_output =
read_to_string(testdata_directory_path.join("expected_Android.bp")).unwrap();
let old_current_dir = current_dir().unwrap();
set_current_dir(&testdata_directory_path).unwrap();
let module_by_package = group_by_package(crates);
assert_eq!(module_by_package.len(), 1); let crates = module_by_package.into_values().next().unwrap();
let package_name = &crates[0][0].package_name; let def = PackageConfig::default(); let package_cfg = cfg.package.get(package_name).unwrap_or(&def); letmut output = generate_android_bp_package_header(
package_name,
package_cfg, "",
&crates,
&cfg.variants.first().unwrap().module_name_overrides,
)
.unwrap(); letmut modules = Vec::new(); for (variant_index, variant_cfg) in cfg.variants.iter().enumerate() { let variant_crates = &crates[variant_index]; let package_name = &variant_crates[0].package_name; let def = PackageVariantConfig::default(); let package_variant_cfg = variant_cfg.package.get(package_name).unwrap_or(&def);
#[test] fn generate_rules() { for testdata_directory_path in testdata_directories() { let cfg_path = testdata_directory_path.join("cargo_embargo.json"); let crates_path = testdata_directory_path.join("crates.json"); let expected_rules_path = testdata_directory_path.join("expected_rules.mk");
let cfg = Config::from_file(&cfg_path).unwrap(); let crates_string = read_to_string(&crates_path).expect("Failed to open crates.json"); let crates = serde_json::from_str::<Vec<Vec<Crate>>>(&crates_string).unwrap();
let old_current_dir = current_dir().unwrap();
set_current_dir(&testdata_directory_path).unwrap();
let module_by_package = group_by_package(crates);
assert_eq!(module_by_package.len(), 1); let crates = module_by_package.into_values().next().unwrap();
letmut rules = String::new(); for (variant_index, variant_cfg) in cfg.variants.iter().enumerate() { let variant_crates = &crates[variant_index]; let package_name = &variant_crates[0].package_name; let def = PackageVariantConfig::default(); let package_variant_cfg = variant_cfg.package.get(package_name).unwrap_or(&def);
/// Returns a list of directories containing test data. /// /// Each directory under `testdata/` contains a single test case. pubfn testdata_directories() -> Vec<PathBuf> {
fs::read_dir(TESTDATA_PATH)
.expect("Failed to read testdata directory")
.filter_map(|entry| { let entry = entry.expect("Error reading testdata directory entry"); if entry
.file_type()
.expect("Error getting metadata for testdata subdirectory")
.is_dir()
{
Some(entry.path())
} else {
None
}
})
.collect()
}
}
Messung V0.5 in Prozent
¤ Dauer der Verarbeitung: 0.11 Sekunden
(vorverarbeitet am 2026-06-26)
¤
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.