1 // Copyright (C) 2022 The Android Open Source Project
2 //
3 // Licensed under the Apache License, Version 2.0 (the "License");
4 // you may not use this file except in compliance with the License.
5 // You may obtain a copy of the License at
6 //
7 // http://www.apache.org/licenses/LICENSE-2.0
8 //
9 // Unless required by applicable law or agreed to in writing, software
10 // distributed under the License is distributed on an "AS IS" BASIS,
11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 // See the License for the specific language governing permissions and
13 // limitations under the License.
14
15 //! Converts a cargo project to Soong.
16 //!
17 //! Forked from development/scripts/cargo2android.py. Missing many of its features. Adds various
18 //! features to make it easier to work with projects containing many crates.
19 //!
20 //! At a high level, this is done by
21 //!
22 //! 1. Running `cargo build -v` and saving the output to a "cargo.out" file.
23 //! 2. Parsing the "cargo.out" file to find invocations of compilers, e.g. `rustc` and `cc`.
24 //! 3. For each compiler invocation, generating a equivalent Soong module, e.g. a "rust_library".
25 //!
26 //! The last step often involves messy, project specific business logic, so many options are
27 //! available to tweak it via a config file.
28
29 mod bp;
30 mod cargo_out;
31
32 use anyhow::bail;
33 use anyhow::Context;
34 use anyhow::Result;
35 use bp::*;
36 use cargo_out::*;
37 use clap::Parser;
38 use once_cell::sync::Lazy;
39 use regex::Regex;
40 use std::collections::BTreeMap;
41 use std::collections::VecDeque;
42 use std::fs::File;
43 use std::io::Write;
44 use std::path::Path;
45 use std::path::PathBuf;
46 use std::process::Command;
47
48 // Major TODOs
49 // * handle errors, esp. in cargo.out parsing. they should fail the program with an error code
50 // * handle warnings. put them in comments in the android.bp, some kind of report section
51
52 #[derive(Parser, Debug)]
53 #[clap()]
54 struct Args {
55 /// Use the cargo binary in the `cargo_bin` directory. Defaults to cargo in $PATH.
56 ///
57 /// TODO: Should default to android prebuilts.
58 #[clap(long)]
59 cargo_bin: Option<PathBuf>,
60 /// Config file.
61 #[clap(long)]
62 cfg: PathBuf,
63 /// Skip the `cargo build` commands and reuse the "cargo.out" file from a previous run if
64 /// available.
65 #[clap(long)]
66 reuse_cargo_out: bool,
67 }
68
default_apex_available() -> Vec<String>69 fn default_apex_available() -> Vec<String> {
70 vec!["//apex_available:platform".to_string(), "//apex_available:anyapex".to_string()]
71 }
72
73 /// Options that apply to everything.
74 #[derive(serde::Deserialize)]
75 #[serde(deny_unknown_fields)]
76 struct Config {
77 /// Whether to output "rust_test" modules.
78 tests: bool,
79 /// Set of features to enable. If non-empty, disables the default crate features.
80 #[serde(default)]
81 features: Vec<String>,
82 /// Whether to build with --workspace.
83 #[serde(default)]
84 workspace: bool,
85 /// When workspace is enabled, list of --exclude crates.
86 #[serde(default)]
87 workspace_excludes: Vec<String>,
88 /// Value to use for every generated module's "defaults" field.
89 global_defaults: Option<String>,
90 /// Value to use for every generated library module's "apex_available" field.
91 #[serde(default = "default_apex_available")]
92 apex_available: Vec<String>,
93 /// Map of renames for modules. For example, if a "libfoo" would be generated and there is an
94 /// entry ("libfoo", "libbar"), the generated module will be called "libbar" instead.
95 ///
96 /// Also, affects references to dependencies (e.g. in a "static_libs" list), even those outside
97 /// the project being processed.
98 #[serde(default)]
99 module_name_overrides: BTreeMap<String, String>,
100 /// Package specific config options.
101 #[serde(default)]
102 package: BTreeMap<String, PackageConfig>,
103 /// Modules in this list will not be generated.
104 #[serde(default)]
105 module_blocklist: Vec<String>,
106 /// Modules name => Soong "visibility" property.
107 #[serde(default)]
108 module_visibility: BTreeMap<String, Vec<String>>,
109 }
110
111 /// Options that apply to everything in a package (i.e. everything associated with a particular
112 /// Cargo.toml file).
113 #[derive(serde::Deserialize, Default)]
114 #[serde(deny_unknown_fields)]
115 struct PackageConfig {
116 /// Whether to compile for device. Defaults to true.
117 #[serde(default)]
118 device_supported: Option<bool>,
119 /// Whether to compile for host. Defaults to true.
120 #[serde(default)]
121 host_supported: Option<bool>,
122 /// Generate "rust_library_rlib" instead of "rust_library".
123 #[serde(default)]
124 force_rlib: bool,
125 /// Whether to disable "unit_test" for "rust_test" modules.
126 // TODO: Should probably be a list of modules or crates. A package might have a mix of unit and
127 // integration tests.
128 #[serde(default)]
129 no_presubmit: bool,
130 /// File with content to append to the end of the generated Android.bp.
131 add_toplevel_block: Option<PathBuf>,
132 /// File with content to append to the end of each generated module.
133 add_module_block: Option<PathBuf>,
134 /// Modules in this list will not be added as dependencies of generated modules.
135 #[serde(default)]
136 dep_blocklist: Vec<String>,
137 /// Patch file to apply after Android.bp is generated.
138 patch: Option<PathBuf>,
139 /// Copy build.rs output to ./out/* and add a genrule to copy ./out/* to genrule output.
140 /// For crates with code pattern:
141 /// include!(concat!(env!("OUT_DIR"), "/<some_file>.rs"))
142 #[serde(default)]
143 copy_out: bool,
144 }
145
main() -> Result<()>146 fn main() -> Result<()> {
147 let args = Args::parse();
148
149 let json_str = std::fs::read_to_string(&args.cfg)
150 .with_context(|| format!("failed to read file: {:?}", args.cfg))?;
151 // Add some basic support for comments to JSON.
152 let json_str: String = json_str.lines().filter(|l| !l.trim_start().starts_with("//")).collect();
153 let cfg: Config = serde_json::from_str(&json_str).context("failed to parse config")?;
154
155 if !Path::new("Cargo.toml").try_exists().context("when checking Cargo.toml")? {
156 bail!("Cargo.toml missing. Run in a directory with a Cargo.toml file.");
157 }
158
159 // Add the custom cargo to PATH.
160 // NOTE: If the directory with cargo has more binaries, this could have some unpredictable side
161 // effects. That is partly intended though, because we want to use that cargo binary's
162 // associated rustc.
163 if let Some(cargo_bin) = args.cargo_bin {
164 let path = std::env::var_os("PATH").unwrap();
165 let mut paths = std::env::split_paths(&path).collect::<VecDeque<_>>();
166 paths.push_front(cargo_bin);
167 let new_path = std::env::join_paths(paths)?;
168 std::env::set_var("PATH", new_path);
169 }
170
171 let cargo_out_path = "cargo.out";
172 let cargo_metadata_path = "cargo.metadata";
173 if !args.reuse_cargo_out || !Path::new(cargo_out_path).exists() {
174 generate_cargo_out(&cfg, cargo_out_path, cargo_metadata_path)
175 .context("generate_cargo_out failed")?;
176 }
177
178 let crates =
179 parse_cargo_out(cargo_out_path, cargo_metadata_path).context("parse_cargo_out failed")?;
180
181 // Find out files.
182 // Example: target.tmp/x86_64-unknown-linux-gnu/debug/build/metrics-d2dd799cebf1888d/out/event_details.rs
183 let mut package_out_files: BTreeMap<String, Vec<PathBuf>> = BTreeMap::new();
184 if cfg.package.iter().any(|(_, v)| v.copy_out) {
185 for entry in glob::glob("target.tmp/**/build/*/out/*")? {
186 match entry {
187 Ok(path) => {
188 let package_name = || -> Option<_> {
189 let dir_name = path.parent()?.parent()?.file_name()?.to_str()?;
190 Some(dir_name.rsplit_once('-')?.0)
191 }()
192 .unwrap_or_else(|| panic!("failed to parse out file path: {:?}", path));
193 package_out_files
194 .entry(package_name.to_string())
195 .or_default()
196 .push(path.clone());
197 }
198 Err(e) => eprintln!("failed to check for out files: {}", e),
199 }
200 }
201 }
202
203 // Group by package.
204 let mut module_by_package: BTreeMap<PathBuf, Vec<Crate>> = BTreeMap::new();
205 for c in crates {
206 module_by_package.entry(c.package_dir.clone()).or_default().push(c);
207 }
208 // Write an Android.bp file per package.
209 for (package_dir, crates) in module_by_package {
210 write_android_bp(
211 &cfg,
212 package_dir,
213 &crates,
214 package_out_files.get(&crates[0].package_name),
215 )?;
216 }
217
218 Ok(())
219 }
220
run_cargo(cargo_out: &mut File, cmd: &mut Command) -> Result<()>221 fn run_cargo(cargo_out: &mut File, cmd: &mut Command) -> Result<()> {
222 use std::os::unix::io::OwnedFd;
223 use std::process::Stdio;
224 let fd: OwnedFd = cargo_out.try_clone()?.into();
225 // eprintln!("Running: {:?}\n", cmd);
226 let output = cmd.stdout(Stdio::from(fd.try_clone()?)).stderr(Stdio::from(fd)).output()?;
227 if !output.status.success() {
228 bail!("cargo command failed with exit status: {:?}", output.status);
229 }
230 Ok(())
231 }
232
233 /// Run various cargo commands and save the output to `cargo_out_path`.
generate_cargo_out(cfg: &Config, cargo_out_path: &str, cargo_metadata_path: &str) -> Result<()>234 fn generate_cargo_out(cfg: &Config, cargo_out_path: &str, cargo_metadata_path: &str) -> Result<()> {
235 let mut cargo_out_file = std::fs::File::create(cargo_out_path)?;
236 let mut cargo_metadata_file = std::fs::File::create(cargo_metadata_path)?;
237
238 let verbose_args = ["-v"];
239 let target_dir_args = ["--target-dir", "target.tmp"];
240
241 // cargo clean
242 run_cargo(&mut cargo_out_file, Command::new("cargo").arg("clean").args(target_dir_args))?;
243
244 let default_target = "x86_64-unknown-linux-gnu";
245 let feature_args = if cfg.features.is_empty() {
246 vec![]
247 } else {
248 vec!["--no-default-features".to_string(), "--features".to_string(), cfg.features.join(",")]
249 };
250
251 let workspace_args = if cfg.workspace {
252 let mut v = vec!["--workspace".to_string()];
253 if !cfg.workspace_excludes.is_empty() {
254 for x in cfg.workspace_excludes.iter() {
255 v.push("--exclude".to_string());
256 v.push(x.clone());
257 }
258 }
259 v
260 } else {
261 vec![]
262 };
263
264 // cargo metadata
265 run_cargo(
266 &mut cargo_metadata_file,
267 Command::new("cargo")
268 .arg("metadata")
269 .arg("-q") // don't output warnings to stderr
270 .arg("--format-version")
271 .arg("1")
272 .args(&feature_args),
273 )?;
274
275 // cargo build
276 run_cargo(
277 &mut cargo_out_file,
278 Command::new("cargo")
279 .args(["build", "--target", default_target])
280 .args(verbose_args)
281 .args(target_dir_args)
282 .args(&workspace_args)
283 .args(&feature_args),
284 )?;
285
286 if cfg.tests {
287 // cargo build --tests
288 run_cargo(
289 &mut cargo_out_file,
290 Command::new("cargo")
291 .args(["build", "--target", default_target, "--tests"])
292 .args(verbose_args)
293 .args(target_dir_args)
294 .args(&workspace_args)
295 .args(&feature_args),
296 )?;
297 }
298
299 Ok(())
300 }
301
302 /// Create the Android.bp file for `package_dir`.
write_android_bp( cfg: &Config, package_dir: PathBuf, crates: &[Crate], out_files: Option<&Vec<PathBuf>>, ) -> Result<()>303 fn write_android_bp(
304 cfg: &Config,
305 package_dir: PathBuf,
306 crates: &[Crate],
307 out_files: Option<&Vec<PathBuf>>,
308 ) -> Result<()> {
309 let bp_path = package_dir.join("Android.bp");
310
311 let package_name = crates[0].package_name.clone();
312 let def = PackageConfig::default();
313 let package_cfg = cfg.package.get(&package_name).unwrap_or(&def);
314
315 // Keep the old license header.
316 let license_section = match std::fs::read_to_string(&bp_path) {
317 Ok(s) => s
318 .lines()
319 .skip_while(|l| l.starts_with("//"))
320 .take_while(|l| !l.starts_with("rust_") && !l.starts_with("genrule {"))
321 .collect::<Vec<&str>>()
322 .join("\n"),
323 Err(e) if e.kind() == std::io::ErrorKind::NotFound => "// TODO: Add license.\n".to_string(),
324 Err(e) => bail!("error when reading {bp_path:?}: {e}"),
325 };
326
327 let mut bp_contents = String::new();
328 bp_contents += "// This file is generated by cargo_embargo.\n";
329 bp_contents += "// Do not modify this file as changes will be overridden on upgrade.\n\n";
330 bp_contents += license_section.trim();
331 bp_contents += "\n";
332
333 let mut modules = Vec::new();
334
335 let extra_srcs = match (package_cfg.copy_out, out_files) {
336 (true, Some(out_files)) => {
337 let out_dir = package_dir.join("out");
338 if !out_dir.exists() {
339 std::fs::create_dir(&out_dir).expect("failed to create out dir");
340 }
341
342 let mut outs: Vec<String> = Vec::new();
343 for f in out_files.iter() {
344 let dest = out_dir.join(f.file_name().unwrap());
345 std::fs::copy(f, dest).expect("failed to copy out file");
346 outs.push(f.file_name().unwrap().to_str().unwrap().to_string());
347 }
348
349 let mut m = BpModule::new("genrule".to_string());
350 let module_name = format!("copy_{}_build_out", package_name);
351 m.props.set("name", module_name.clone());
352 m.props.set("srcs", vec!["out/*"]);
353 m.props.set("cmd", "cp $(in) $(genDir)");
354 m.props.set("out", outs);
355 modules.push(m);
356
357 vec![":".to_string() + &module_name]
358 }
359 _ => vec![],
360 };
361
362 for c in crates {
363 modules.extend(crate_to_bp_modules(c, cfg, package_cfg, &extra_srcs).with_context(
364 || {
365 format!(
366 "failed to generate bp module for crate \"{}\" with package name \"{}\"",
367 c.name, c.package_name
368 )
369 },
370 )?);
371 }
372 if modules.is_empty() {
373 return Ok(());
374 }
375
376 modules.sort_by_key(|m| m.props.get_string("name").to_string());
377 for m in modules {
378 m.write(&mut bp_contents)?;
379 bp_contents += "\n";
380 }
381 if let Some(path) = &package_cfg.add_toplevel_block {
382 bp_contents +=
383 &std::fs::read_to_string(path).with_context(|| format!("failed to read {path:?}"))?;
384 bp_contents += "\n";
385 }
386 File::create(&bp_path)?.write_all(bp_contents.as_bytes())?;
387
388 let bpfmt_output = Command::new("bpfmt").arg("-w").arg(&bp_path).output()?;
389 if !bpfmt_output.status.success() {
390 eprintln!(
391 "WARNING: bpfmt -w {:?} failed before patch: {}",
392 bp_path,
393 String::from_utf8_lossy(&bpfmt_output.stderr)
394 );
395 }
396
397 if let Some(patch_path) = &package_cfg.patch {
398 let patch_output =
399 Command::new("patch").arg("-s").arg(&bp_path).arg(patch_path).output()?;
400 if !patch_output.status.success() {
401 eprintln!("WARNING: failed to apply patch {:?}", patch_path);
402 }
403 // Re-run bpfmt after the patch so
404 let bpfmt_output = Command::new("bpfmt").arg("-w").arg(&bp_path).output()?;
405 if !bpfmt_output.status.success() {
406 eprintln!(
407 "WARNING: bpfmt -w {:?} failed after patch: {}",
408 bp_path,
409 String::from_utf8_lossy(&bpfmt_output.stderr)
410 );
411 }
412 }
413
414 Ok(())
415 }
416
417 /// Convert a `Crate` into `BpModule`s.
418 ///
419 /// If messy business logic is necessary, prefer putting it here.
crate_to_bp_modules( crate_: &Crate, cfg: &Config, package_cfg: &PackageConfig, extra_srcs: &[String], ) -> Result<Vec<BpModule>>420 fn crate_to_bp_modules(
421 crate_: &Crate,
422 cfg: &Config,
423 package_cfg: &PackageConfig,
424 extra_srcs: &[String],
425 ) -> Result<Vec<BpModule>> {
426 let mut modules = Vec::new();
427 for crate_type in &crate_.types {
428 let host = if package_cfg.device_supported.unwrap_or(true) { "" } else { "_host" };
429 let rlib = if package_cfg.force_rlib { "_rlib" } else { "" };
430 let (module_type, module_name, stem) = match crate_type {
431 CrateType::Bin => {
432 ("rust_binary".to_string() + host, crate_.name.clone(), crate_.name.clone())
433 }
434 CrateType::Lib | CrateType::RLib => {
435 let stem = "lib".to_string() + &crate_.name;
436 ("rust_library".to_string() + rlib + host, stem.clone(), stem)
437 }
438 CrateType::DyLib => {
439 let stem = "lib".to_string() + &crate_.name;
440 ("rust_library".to_string() + host + "_dylib", stem.clone() + "_dylib", stem)
441 }
442 CrateType::CDyLib => {
443 let stem = "lib".to_string() + &crate_.name;
444 ("rust_ffi".to_string() + host + "_shared", stem.clone() + "_shared", stem)
445 }
446 CrateType::StaticLib => {
447 let stem = "lib".to_string() + &crate_.name;
448 ("rust_ffi".to_string() + host + "_static", stem.clone() + "_static", stem)
449 }
450 CrateType::ProcMacro => {
451 let stem = "lib".to_string() + &crate_.name;
452 ("rust_proc_macro".to_string(), stem.clone(), stem)
453 }
454 CrateType::Test | CrateType::TestNoHarness => {
455 let suffix = crate_.main_src.to_string_lossy().into_owned();
456 let suffix = suffix.replace('/', "_").replace(".rs", "");
457 let stem = crate_.package_name.clone() + "_test_" + &suffix;
458 if crate_type == &CrateType::TestNoHarness {
459 eprintln!(
460 "WARNING: ignoring test \"{}\" with harness=false. not supported yet",
461 stem
462 );
463 return Ok(Vec::new());
464 }
465 ("rust_test".to_string() + host, stem.clone(), stem)
466 }
467 };
468
469 let mut m = BpModule::new(module_type.clone());
470 let module_name = cfg.module_name_overrides.get(&module_name).unwrap_or(&module_name);
471 if cfg.module_blocklist.contains(module_name) {
472 continue;
473 }
474 m.props.set("name", module_name.clone());
475 if &stem != module_name {
476 m.props.set("stem", stem);
477 }
478
479 if let Some(defaults) = &cfg.global_defaults {
480 m.props.set("defaults", vec![defaults.clone()]);
481 }
482
483 if package_cfg.host_supported.unwrap_or(true)
484 && package_cfg.device_supported.unwrap_or(true)
485 && module_type != "rust_proc_macro"
486 {
487 m.props.set("host_supported", true);
488 }
489
490 m.props.set("crate_name", crate_.name.clone());
491 m.props.set("cargo_env_compat", true);
492
493 if let Some(version) = &crate_.version {
494 m.props.set("cargo_pkg_version", version.clone());
495 }
496
497 if crate_.types.contains(&CrateType::Test) {
498 m.props.set("test_suites", vec!["general-tests"]);
499 m.props.set("auto_gen_config", true);
500 if package_cfg.host_supported.unwrap_or(true) {
501 m.props.object("test_options").set("unit_test", !package_cfg.no_presubmit);
502 }
503 }
504
505 let mut srcs = vec![crate_.main_src.to_string_lossy().to_string()];
506 srcs.extend(extra_srcs.iter().cloned());
507 m.props.set("srcs", srcs);
508
509 m.props.set("edition", crate_.edition.clone());
510 if !crate_.features.is_empty() {
511 m.props.set("features", crate_.features.clone());
512 }
513 if !crate_.cfgs.is_empty() {
514 m.props.set("cfgs", crate_.cfgs.clone());
515 }
516
517 let mut flags = Vec::new();
518 if !crate_.cap_lints.is_empty() {
519 flags.push(crate_.cap_lints.clone());
520 }
521 flags.extend(crate_.codegens.clone());
522 if !flags.is_empty() {
523 m.props.set("flags", flags);
524 }
525
526 let mut rust_libs = Vec::new();
527 let mut proc_macro_libs = Vec::new();
528 for (extern_name, filename) in &crate_.externs {
529 if extern_name == "proc_macro" {
530 continue;
531 }
532 let filename =
533 filename.as_ref().unwrap_or_else(|| panic!("no filename for {}", extern_name));
534 // Example filename: "libgetrandom-fd8800939535fc59.rmeta"
535 static REGEX: Lazy<Regex> =
536 Lazy::new(|| Regex::new(r"^lib(.*)-[0-9a-f]*.(rlib|so|rmeta)$").unwrap());
537 let lib_name = if let Some(x) = REGEX.captures(filename).and_then(|x| x.get(1)) {
538 x
539 } else {
540 bail!("bad filename for extern {}: {}", extern_name, filename);
541 };
542 if filename.ends_with(".rlib") || filename.ends_with(".rmeta") {
543 rust_libs.push(lib_name.as_str().to_string());
544 } else if filename.ends_with(".so") {
545 // Assume .so files are always proc_macros. May not always be right.
546 proc_macro_libs.push(lib_name.as_str().to_string());
547 } else {
548 unreachable!();
549 }
550 }
551
552 // Add "lib" prefix and apply name overrides.
553 let process_lib_deps = |libs: Vec<String>| -> Vec<String> {
554 let mut result = Vec::new();
555 for x in libs {
556 let module_name = "lib".to_string() + x.as_str();
557 let module_name =
558 cfg.module_name_overrides.get(&module_name).unwrap_or(&module_name);
559 if package_cfg.dep_blocklist.contains(module_name) {
560 continue;
561 }
562 result.push(module_name.to_string());
563 }
564 result.sort();
565 result
566 };
567 if !rust_libs.is_empty() {
568 m.props.set("rustlibs", process_lib_deps(rust_libs));
569 }
570 if !proc_macro_libs.is_empty() {
571 m.props.set("proc_macros", process_lib_deps(proc_macro_libs));
572 }
573 if !crate_.static_libs.is_empty() {
574 m.props.set("static_libs", process_lib_deps(crate_.static_libs.clone()));
575 }
576 if !crate_.shared_libs.is_empty() {
577 m.props.set("shared_libs", process_lib_deps(crate_.shared_libs.clone()));
578 }
579
580 if !cfg.apex_available.is_empty()
581 && [
582 CrateType::Lib,
583 CrateType::RLib,
584 CrateType::DyLib,
585 CrateType::CDyLib,
586 CrateType::StaticLib,
587 ]
588 .contains(crate_type)
589 {
590 m.props.set("apex_available", cfg.apex_available.clone());
591 }
592
593 if let Some(visibility) = cfg.module_visibility.get(module_name) {
594 m.props.set("visibility", visibility.clone());
595 }
596
597 if let Some(path) = &package_cfg.add_module_block {
598 let content = std::fs::read_to_string(path)
599 .with_context(|| format!("failed to read {path:?}"))?;
600 m.props.raw_block = Some(content);
601 }
602
603 modules.push(m);
604 }
605 Ok(modules)
606 }
607