diff --git a/tools/cargo_embargo/src/cargo.rs b/tools/cargo_embargo/src/cargo.rs new file mode 100644 index 000000000..23e644765 --- /dev/null +++ b/tools/cargo_embargo/src/cargo.rs @@ -0,0 +1,86 @@ +// 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 and functions for parsing the output of cargo. + +pub mod cargo_out; +pub mod metadata; + +use serde::{Deserialize, Serialize}; +use std::path::PathBuf; + +/// Combined representation of --crate-type and --test flags. +#[derive(Copy, Clone, Debug, Deserialize, PartialEq, Eq, Serialize)] +#[serde[rename_all = "lowercase"]] +pub enum CrateType { + // --crate-type types + Bin, + Lib, + RLib, + DyLib, + CDyLib, + StaticLib, + #[serde(rename = "proc-macro")] + ProcMacro, + // --test + Test, + // "--cfg test" without --test. (Assume it is a test with the harness disabled. + TestNoHarness, +} + +impl CrateType { + fn from_str(s: &str) -> CrateType { + match s { + "bin" => CrateType::Bin, + "lib" => CrateType::Lib, + "rlib" => CrateType::RLib, + "dylib" => CrateType::DyLib, + "cdylib" => CrateType::CDyLib, + "staticlib" => CrateType::StaticLib, + "proc-macro" => CrateType::ProcMacro, + _ => panic!("unexpected --crate-type: {}", s), + } + } +} + +impl CrateType { + /// Returns whether the crate type is a kind of library. + pub fn is_library(self) -> bool { + matches!(self, Self::Lib | Self::RLib | Self::DyLib | Self::CDyLib | Self::StaticLib) + } +} + +/// Info extracted from `CargoOut` for a crate. +/// +/// Note that there is a 1-to-many relationship between a Cargo.toml file and these `Crate` +/// objects. For example, a Cargo.toml file might have a bin, a lib, and various tests. Each of +/// those will be a separate `Crate`. All of them will have the same `package_name`. +#[derive(Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +pub struct Crate { + pub name: String, + pub package_name: String, + pub version: Option, + pub types: Vec, + pub target: Option, // --target + pub features: Vec, // --cfg feature= + pub cfgs: Vec, // non-feature --cfg + pub externs: Vec<(String, Option)>, // name => rlib file + pub codegens: Vec, // -C + pub cap_lints: String, + pub static_libs: Vec, + pub shared_libs: Vec, + pub edition: String, + pub package_dir: PathBuf, // canonicalized + pub main_src: PathBuf, // relative to package_dir +} diff --git a/tools/cargo_embargo/src/cargo_out.rs b/tools/cargo_embargo/src/cargo/cargo_out.rs similarity index 74% rename from tools/cargo_embargo/src/cargo_out.rs rename to tools/cargo_embargo/src/cargo/cargo_out.rs index a8d5984b2..74acf5c04 100644 --- a/tools/cargo_embargo/src/cargo_out.rs +++ b/tools/cargo_embargo/src/cargo/cargo_out.rs @@ -12,91 +12,60 @@ // See the License for the specific language governing permissions and // limitations under the License. +use super::metadata::WorkspaceMetadata; +use super::{Crate, CrateType}; use anyhow::anyhow; use anyhow::bail; use anyhow::Context; use anyhow::Result; use once_cell::sync::Lazy; use regex::Regex; -use serde::{Deserialize, Serialize}; use std::collections::BTreeMap; +use std::env; +use std::fs::{read_to_string, File}; use std::path::Path; use std::path::PathBuf; -/// Combined representation of --crate-type and --test flags. -#[derive(Copy, Clone, Debug, Deserialize, PartialEq, Eq, Serialize)] -#[serde[rename_all = "lowercase"]] -pub enum CrateType { - // --crate-type types - Bin, - Lib, - RLib, - DyLib, - CDyLib, - StaticLib, - ProcMacro, - // --test - Test, - // "--cfg test" without --test. (Assume it is a test with the harness disabled. - TestNoHarness, -} - -impl CrateType { - /// Returns whether the crate type is a kind of library. - pub fn is_library(self) -> bool { - matches!(self, Self::Lib | Self::RLib | Self::DyLib | Self::CDyLib | Self::StaticLib) - } -} - -/// Info extracted from `CargoOut` for a crate. +/// Reads the given `cargo.out` and `cargo.metadata` files, and generates a list of crates based on +/// the rustc invocations. /// -/// Note that there is a 1-to-many relationship between a Cargo.toml file and these `Crate` -/// objects. For example, a Cargo.toml file might have a bin, a lib, and various tests. Each of -/// those will be a separate `Crate`. All of them will have the same `package_name`. -#[derive(Debug, Default, Deserialize, Eq, PartialEq, Serialize)] -pub struct Crate { - pub name: String, - pub package_name: String, - pub version: Option, - pub types: Vec, - pub target: Option, // --target - pub features: Vec, // --cfg feature= - pub cfgs: Vec, // non-feature --cfg - pub externs: Vec<(String, Option)>, // name => rlib file - pub codegens: Vec, // -C - pub cap_lints: String, - pub static_libs: Vec, - pub shared_libs: Vec, - pub edition: String, - pub package_dir: PathBuf, // canonicalized - pub main_src: PathBuf, // relative to package_dir -} - -pub fn parse_cargo_out(cargo_out_path: &str, cargo_metadata_path: &str) -> Result> { - let metadata: WorkspaceMetadata = serde_json::from_str( - &std::fs::read_to_string(cargo_metadata_path).context("failed to read cargo.metadata")?, +/// Ignores crates outside the current directory and build script crates. +pub fn parse_cargo_out( + cargo_out_path: impl AsRef, + cargo_metadata_path: impl AsRef, +) -> Result> { + let cargo_out = read_to_string(cargo_out_path).context("failed to read cargo.out")?; + let metadata = serde_json::from_reader( + File::open(cargo_metadata_path).context("failed to open cargo.metadata")?, ) .context("failed to parse cargo.metadata")?; + parse_cargo_out_str(&cargo_out, &metadata, env::current_dir().unwrap().canonicalize().unwrap()) +} - let cargo_out = CargoOut::parse( - &std::fs::read_to_string(cargo_out_path).context("failed to read cargo.out")?, - ) - .context("failed to parse cargo.out")?; +/// Parses the given `cargo.out` and `cargo.metadata` file contents and generates a list of crates +/// based on the rustc invocations. +/// +/// Ignores crates outside `base_directory` and build script crates. +fn parse_cargo_out_str( + cargo_out: &str, + metadata: &WorkspaceMetadata, + base_directory: impl AsRef, +) -> Result> { + let cargo_out = CargoOut::parse(cargo_out).context("failed to parse cargo.out")?; assert!(cargo_out.cc_invocations.is_empty(), "cc not supported yet"); assert!(cargo_out.ar_invocations.is_empty(), "ar not supported yet"); let mut crates = Vec::new(); for rustc in cargo_out.rustc_invocations.iter() { - let c = Crate::from_rustc_invocation(rustc, &metadata) + let c = Crate::from_rustc_invocation(rustc, metadata) .with_context(|| format!("failed to process rustc invocation: {rustc}"))?; // Ignore build.rs crates. if c.name.starts_with("build_script_") { continue; } - // Ignore crates outside the current directory. - let cwd = std::env::current_dir().unwrap().canonicalize().unwrap(); - if !c.package_dir.starts_with(cwd) { + // Ignore crates outside the base directory. + if !c.package_dir.starts_with(&base_directory) { continue; } crates.push(c); @@ -104,20 +73,6 @@ pub fn parse_cargo_out(cargo_out_path: &str, cargo_metadata_path: &str) -> Resul Ok(crates) } -/// `cargo metadata` output. -#[derive(Debug, Deserialize)] -struct WorkspaceMetadata { - packages: Vec, -} - -#[derive(Debug, Deserialize)] -struct PackageMetadata { - name: String, - version: String, - edition: String, - manifest_path: String, -} - /// Raw-ish data extracted from cargo.out file. #[derive(Debug, Default)] struct CargoOut { @@ -240,21 +195,6 @@ impl CargoOut { } } -impl CrateType { - fn from_str(s: &str) -> CrateType { - match s { - "bin" => CrateType::Bin, - "lib" => CrateType::Lib, - "rlib" => CrateType::RLib, - "dylib" => CrateType::DyLib, - "cdylib" => CrateType::CDyLib, - "staticlib" => CrateType::StaticLib, - "proc-macro" => CrateType::ProcMacro, - _ => panic!("unexpected --crate-type: {}", s), - } - } -} - impl Crate { fn from_rustc_invocation(rustc: &str, metadata: &WorkspaceMetadata) -> Result { let mut out = Crate::default(); @@ -339,27 +279,7 @@ impl Crate { } } _ if !arg.starts_with('-') => { - let src_path = Path::new(arg); - // Canonicalize the path because: - // - // 1. We don't consistently get relative or absolute paths elsewhere. If we - // canonicalize everything, it becomes easy to compare paths. - // - // 2. We don't want to consider symlinks to code outside the cwd as part of the - // project (e.g. AOSP's import of crosvm has symlinks from crosvm's own 3p - // directory to the android 3p directories). - let src_path = src_path - .canonicalize() - .unwrap_or_else(|e| panic!("failed to canonicalize {src_path:?}: {}", e)); - out.package_dir = src_path.parent().unwrap().to_path_buf(); - while !out.package_dir.join("Cargo.toml").try_exists()? { - if let Some(parent) = out.package_dir.parent() { - out.package_dir = parent.to_path_buf(); - } else { - bail!("No Cargo.toml found in parents of {:?}", src_path); - } - } - out.main_src = src_path.strip_prefix(&out.package_dir).unwrap().to_path_buf(); + (out.package_dir, out.main_src) = split_src_path(Path::new(arg))?; } // ignored flags @@ -426,3 +346,35 @@ impl Crate { Ok(out) } } + +/// Given a path to the main source file of some Rust crate, returns the canonical path to the +/// package directory, and the relative path to the source file within that directory. +fn split_src_path(src_path: &Path) -> Result<(PathBuf, PathBuf)> { + // Canonicalize the path because: + // + // 1. We don't consistently get relative or absolute paths elsewhere. If we + // canonicalize everything, it becomes easy to compare paths. + // + // 2. We don't want to consider symlinks to code outside the cwd as part of the + // project (e.g. AOSP's import of crosvm has symlinks from crosvm's own 3p + // directory to the android 3p directories). + let src_path = src_path + .canonicalize() + .unwrap_or_else(|e| panic!("failed to canonicalize {src_path:?}: {}", e)); + let package_dir = find_cargo_toml(&src_path)?; + let main_src = src_path.strip_prefix(&package_dir).unwrap().to_path_buf(); + + Ok((package_dir, main_src)) +} + +/// Given a path to a Rust source file, finds the closest ancestor directory containing a +/// `Cargo.toml` file. +fn find_cargo_toml(src_path: &Path) -> Result { + let mut package_dir = src_path.parent().unwrap(); + while !package_dir.join("Cargo.toml").try_exists()? { + package_dir = package_dir + .parent() + .ok_or_else(|| anyhow!("No Cargo.toml found in parents of {:?}", src_path))?; + } + Ok(package_dir.to_path_buf()) +} diff --git a/tools/cargo_embargo/src/cargo/metadata.rs b/tools/cargo_embargo/src/cargo/metadata.rs new file mode 100644 index 000000000..e5cfc7709 --- /dev/null +++ b/tools/cargo_embargo/src/cargo/metadata.rs @@ -0,0 +1,31 @@ +// 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 serde::Deserialize; + +/// `cargo metadata` output. +#[derive(Debug, Deserialize)] +pub struct WorkspaceMetadata { + pub packages: Vec, +} + +#[derive(Debug, Deserialize)] +pub struct PackageMetadata { + pub name: String, + pub version: String, + pub edition: String, + pub manifest_path: String, +} diff --git a/tools/cargo_embargo/src/main.rs b/tools/cargo_embargo/src/main.rs index f88ed9fc8..cbcac394d 100644 --- a/tools/cargo_embargo/src/main.rs +++ b/tools/cargo_embargo/src/main.rs @@ -27,7 +27,7 @@ //! available to tweak it via a config file. mod bp; -mod cargo_out; +mod cargo; mod config; use crate::config::Config; @@ -36,7 +36,7 @@ use anyhow::bail; use anyhow::Context; use anyhow::Result; use bp::*; -use cargo_out::{parse_cargo_out, Crate, CrateType}; +use cargo::{cargo_out::parse_cargo_out, Crate, CrateType}; use clap::Parser; use once_cell::sync::Lazy; use regex::Regex; @@ -52,6 +52,7 @@ use std::process::Command; // * 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 +/// Command-line parameters for `cargo_embargo`. #[derive(Parser, Debug)] #[clap()] struct Args {