diff --git a/tools/external_crates/Cargo.toml b/tools/external_crates/Cargo.toml index b724739e96..4ee0bfd765 100644 --- a/tools/external_crates/Cargo.toml +++ b/tools/external_crates/Cargo.toml @@ -7,6 +7,7 @@ members = [ "license_checker", "name_and_version", "name_and_version_proc_macros", + "repo_config", "rooted_path", "test_mapping", ] diff --git a/tools/external_crates/cargo_embargo.json b/tools/external_crates/cargo_embargo.json index 71d7baf485..b515b5f64c 100644 --- a/tools/external_crates/cargo_embargo.json +++ b/tools/external_crates/cargo_embargo.json @@ -19,6 +19,9 @@ }, "name_and_version_proc_macros": { "device_supported": false + }, + "repo_config": { + "device_supported": false } }, "tests": true, diff --git a/tools/external_crates/crate_config/Android.bp b/tools/external_crates/crate_config/Android.bp index fd59df1338..604197ccb4 100644 --- a/tools/external_crates/crate_config/Android.bp +++ b/tools/external_crates/crate_config/Android.bp @@ -8,6 +8,26 @@ package { default_applicable_licenses: ["Android-Apache-2.0"], } +rust_test_host { + name: "crate_config_test_src_lib", + crate_name: "crate_config", + cargo_env_compat: true, + cargo_pkg_version: "0.1.0", + crate_root: "src/lib.rs", + test_suites: ["general-tests"], + auto_gen_config: true, + test_options: { + unit_test: true, + }, + edition: "2021", + rustlibs: [ + "libserde", + "libtempfile", + "libthiserror", + "libtoml", + ], +} + rust_library_host { name: "libcrate_config", crate_name: "crate_config", diff --git a/tools/external_crates/crate_config/Cargo.toml b/tools/external_crates/crate_config/Cargo.toml index 4c297eb4d0..7ea5f5dbff 100644 --- a/tools/external_crates/crate_config/Cargo.toml +++ b/tools/external_crates/crate_config/Cargo.toml @@ -6,4 +6,7 @@ edition = "2021" [dependencies] serde = { version = "=1.0.210", features = ["derive"] } toml = "0.8" -thiserror = "1.0" \ No newline at end of file +thiserror = "1.0" + +[dev-dependencies] +tempfile = "3" \ No newline at end of file diff --git a/tools/external_crates/crate_config/src/lib.rs b/tools/external_crates/crate_config/src/lib.rs index 6608be3ad8..b6c978b6cc 100644 --- a/tools/external_crates/crate_config/src/lib.rs +++ b/tools/external_crates/crate_config/src/lib.rs @@ -18,7 +18,7 @@ use std::{fs::read_to_string, io, path::Path}; use serde::Deserialize; -/// A parsed android_config.json file +/// A parsed android_config.toml file #[derive(Deserialize, Debug, Default, Clone)] pub struct CrateConfig { #[serde(default, skip_serializing_if = "Vec::is_empty")] @@ -34,10 +34,14 @@ pub enum Error { IoError(#[from] io::Error), } +/// The crate config file name. +pub static CONFIG_FILE_NAME: &str = "android_config.toml"; + impl CrateConfig { - /// Read the android_config.toml file in the specified directory. - pub fn read(config_file: impl AsRef) -> Result { - let config_file = config_file.as_ref(); + /// Read the android_config.toml file in the specified directory. If not present, + /// a default version is returned. + pub fn read(crate_dir: impl AsRef) -> Result { + let config_file = crate_dir.as_ref().join(CONFIG_FILE_NAME); if !config_file.exists() { return Ok(CrateConfig::default()); } @@ -49,3 +53,33 @@ impl CrateConfig { self.deletions.iter().map(|d| d.as_str()) } } + +#[cfg(test)] +mod tests { + use std::fs::write; + + use super::*; + + #[test] + fn basic() { + let dir = tempfile::tempdir().expect("Failed to create tempdir"); + write(dir.path().join(CONFIG_FILE_NAME), r#"deletions = ["foo"]"#) + .expect("Failed to write to tempdir"); + let config = CrateConfig::read(dir.path()).expect("Failed to read config file"); + assert_eq!(config.deletions, ["foo"]); + } + + #[test] + fn default() { + let dir = tempfile::tempdir().expect("Failed to create tempdir"); + let config = CrateConfig::read(dir.path()).expect("Failed to get default config"); + assert!(config.deletions.is_empty()); + } + + #[test] + fn parse_error() { + let dir = tempfile::tempdir().expect("Failed to create tempdir"); + write(dir.path().join(CONFIG_FILE_NAME), r#"blah"#).expect("Failed to write to tempdir"); + assert!(matches!(CrateConfig::read(dir.path()), Err(Error::TomlParseError(_)))); + } +} diff --git a/tools/external_crates/crate_tool/Cargo.toml b/tools/external_crates/crate_tool/Cargo.toml index 66cce0fb5e..d49dfd080f 100644 --- a/tools/external_crates/crate_tool/Cargo.toml +++ b/tools/external_crates/crate_tool/Cargo.toml @@ -30,6 +30,7 @@ google_metadata = { path = "../google_metadata"} license_checker = { path = "../license_checker", features = ["fuzzy_content_match"] } name_and_version = { path = "../name_and_version" } name_and_version_proc_macros = { path = "../name_and_version_proc_macros" } +repo_config = { path = "../repo_config" } rooted_path = { path = "../rooted_path" } test_mapping = { path = "../test_mapping" } diff --git a/tools/external_crates/crate_tool/src/managed_crate.rs b/tools/external_crates/crate_tool/src/managed_crate.rs index d84d484c33..c63d08980e 100644 --- a/tools/external_crates/crate_tool/src/managed_crate.rs +++ b/tools/external_crates/crate_tool/src/managed_crate.rs @@ -94,9 +94,14 @@ impl ManagedCrate { } pub fn config(&self) -> &CrateConfig { self.config.get_or_init(|| { - let config_file = self.android_crate_path().join("android_config.toml").unwrap(); - CrateConfig::read(&config_file) - .unwrap_or_else(|e| panic!("Failed to read {config_file}/android_config.toml: {e}")) + CrateConfig::read(self.android_crate_path().abs()).unwrap_or_else(|e| { + panic!( + "Failed to read crate config {}/{}: {}", + self.android_crate_path(), + crate_config::CONFIG_FILE_NAME, + e + ) + }) }) } pub fn android_bp(&self) -> RootedPath { diff --git a/tools/external_crates/crate_tool/src/managed_repo.rs b/tools/external_crates/crate_tool/src/managed_repo.rs index 07f40da3a4..d0c9e0bbfc 100644 --- a/tools/external_crates/crate_tool/src/managed_repo.rs +++ b/tools/external_crates/crate_tool/src/managed_repo.rs @@ -13,6 +13,7 @@ // limitations under the License. use std::{ + cell::OnceCell, collections::{BTreeMap, BTreeSet}, env, fs::{create_dir, create_dir_all, read_dir, remove_file, write}, @@ -20,7 +21,6 @@ use std::{ path::Path, process::Command, str::from_utf8, - sync::LazyLock, }; use anyhow::{anyhow, bail, Context, Result}; @@ -30,6 +30,7 @@ use google_metadata::GoogleMetadata; use itertools::Itertools; use license_checker::find_licenses; use name_and_version::{NameAndVersionMap, NameAndVersionRef, NamedAndVersioned}; +use repo_config::RepoConfig; use rooted_path::RootedPath; use semver::Version; use serde::Serialize; @@ -48,37 +49,6 @@ use crate::{ SuccessOrError, }; -// TODO: Store this as a data file in the monorepo. -static IMPORT_DENYLIST: LazyLock> = LazyLock::new(|| { - BTreeSet::from([ - "instant", // Not maintained. - "bumpalo", // Unsound - "allocator-api2", // Unsound - // Uniffi crates. - // Per mmaurer: "considered too difficult to verify and stopped being used for the original use case". - "oneshot-uniffi", - "uniffi", - "uniffi_checksum_derive", - "uniffi_core", - "uniffi_macros", - "uniffi_meta", - // From go/android-restricted-rust-crates - "ctor", // Runs before main, can cause linking errors - "inventory", // Depends on ctor - "ouroboros", // Sketchy unsafe, code smell - "sled", // There is too much unsafe. - "proc-macro-error", // Unmaintained and depends on syn 1 - "derive-getters", // Unmaintained and depends on syn 1 - "android_system_properties", // Duplicates internal functionality - "x509-parser", // Duplicates x509-cert and BoringSSL - "der-parser", // Duplicates der - "oid-registry", - "asn1-rs", - "android-tzdata", // Relies on unsupported API - "rustls", // Duplicates BoringSSL - ]) -}); - #[derive(Serialize, Default, Debug)] struct UpdateSuggestions { updates: Vec, @@ -94,6 +64,7 @@ struct UpdateSuggestion { pub struct ManagedRepo { path: RootedPath, + config: OnceCell, crates_io: CratesIoIndex, } @@ -101,9 +72,22 @@ impl ManagedRepo { pub fn new(path: RootedPath, offline: bool) -> Result { Ok(ManagedRepo { path, + config: OnceCell::new(), crates_io: if offline { CratesIoIndex::new_offline()? } else { CratesIoIndex::new()? }, }) } + pub fn config(&self) -> &RepoConfig { + self.config.get_or_init(|| { + RepoConfig::read(self.path.abs()).unwrap_or_else(|e| { + panic!( + "Failed to read crate config {}/{}: {}", + self.path, + repo_config::CONFIG_FILE_NAME, + e + ) + }) + }) + } fn pseudo_crate(&self) -> PseudoCrate { PseudoCrate::new(self.path.join("pseudo_crate").unwrap()) } @@ -428,7 +412,7 @@ impl ManagedRepo { return Ok(()); } - if IMPORT_DENYLIST.contains(crate_name) { + if !self.config().is_allowed(crate_name) { println!("Crate {crate_name} is on the import denylist"); return Ok(()); } @@ -456,7 +440,7 @@ impl ManagedRepo { dep.crate_name(), dep.requirement() ); - if IMPORT_DENYLIST.contains(dep.crate_name()) { + if !self.config().is_allowed(dep.crate_name()) { println!(" And {} is on the import denylist", dep.crate_name()); } // This is a no-op because our dependency code only considers normal deps anyway. @@ -516,7 +500,7 @@ impl ManagedRepo { if legacy_dir.abs().exists() { bail!("Legacy crate already imported at {}", legacy_dir); } - if IMPORT_DENYLIST.contains(crate_name) { + if !self.config().is_allowed(crate_name) { bail!("Crate {crate_name} is on the import denylist"); } diff --git a/tools/external_crates/repo_config/.gitignore b/tools/external_crates/repo_config/.gitignore new file mode 100644 index 0000000000..ea8c4bf7f3 --- /dev/null +++ b/tools/external_crates/repo_config/.gitignore @@ -0,0 +1 @@ +/target diff --git a/tools/external_crates/repo_config/Android.bp b/tools/external_crates/repo_config/Android.bp new file mode 100644 index 0000000000..37724d048b --- /dev/null +++ b/tools/external_crates/repo_config/Android.bp @@ -0,0 +1,43 @@ +// This file is generated by cargo_embargo. +// Do not modify this file after the first "rust_*" or "genrule" module +// because the changes will be overridden on upgrade. +// Content before the first "rust_*" or "genrule" module is preserved. + +package { + default_team: "trendy_team_android_rust", + default_applicable_licenses: ["Android-Apache-2.0"], +} + +rust_library_host { + name: "librepo_config", + crate_name: "repo_config", + cargo_env_compat: true, + cargo_pkg_version: "0.1.0", + crate_root: "src/lib.rs", + edition: "2021", + rustlibs: [ + "libserde", + "libthiserror", + "libtoml", + ], +} + +rust_test_host { + name: "repo_config_test_src_lib", + crate_name: "repo_config", + cargo_env_compat: true, + cargo_pkg_version: "0.1.0", + crate_root: "src/lib.rs", + test_suites: ["general-tests"], + auto_gen_config: true, + test_options: { + unit_test: true, + }, + edition: "2021", + rustlibs: [ + "libserde", + "libtempfile", + "libthiserror", + "libtoml", + ], +} diff --git a/tools/external_crates/repo_config/Cargo.toml b/tools/external_crates/repo_config/Cargo.toml new file mode 100644 index 0000000000..3e79469340 --- /dev/null +++ b/tools/external_crates/repo_config/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "repo_config" +version = "0.1.0" +edition = "2021" + +[dependencies] +serde = { version = "=1.0.210", features = ["derive"] } +toml = "0.8" +thiserror = "1.0" + +[dev-dependencies] +tempfile = "3" \ No newline at end of file diff --git a/tools/external_crates/repo_config/src/lib.rs b/tools/external_crates/repo_config/src/lib.rs new file mode 100644 index 0000000000..89d28ae9c5 --- /dev/null +++ b/tools/external_crates/repo_config/src/lib.rs @@ -0,0 +1,86 @@ +// Copyright (C) 2025 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. + +//! TOML file to store per-repo configuration. + +use std::{collections::BTreeSet, fs::read_to_string, io, path::Path}; + +use serde::Deserialize; + +/// A parsed android_repo_config.toml file +#[derive(Deserialize, Debug, Default, Clone)] +pub struct RepoConfig { + #[serde(default, skip_serializing_if = "Vec::is_empty")] + crate_denylist: BTreeSet, +} + +#[allow(missing_docs)] +#[derive(thiserror::Error, Debug)] +pub enum Error { + #[error("TOML parse error")] + TomlParseError(#[from] toml::de::Error), + #[error("IO error")] + IoError(#[from] io::Error), +} + +/// The repo config file name. +pub static CONFIG_FILE_NAME: &str = "android_repo_config.toml"; + +impl RepoConfig { + /// Read the android_repo_config.toml file in the specified directory. If not present, + /// a default version is returned. + pub fn read(managed_repo_dir: impl AsRef) -> Result { + let config_file = managed_repo_dir.as_ref().join(CONFIG_FILE_NAME); + if !config_file.exists() { + return Ok(RepoConfig::default()); + } + let toml: RepoConfig = toml::from_str(read_to_string(config_file)?.as_str())?; + Ok(toml) + } + /// Returns true if the crate is on the crate denylist. + pub fn is_allowed(&self, crate_name: impl AsRef) -> bool { + !self.crate_denylist.contains(crate_name.as_ref()) + } +} + +#[cfg(test)] +mod tests { + use std::fs::write; + + use super::*; + + #[test] + fn basic() { + let dir = tempfile::tempdir().expect("Failed to create tempdir"); + write(dir.path().join(CONFIG_FILE_NAME), r#"crate_denylist = ["foo"]"#) + .expect("Failed to write to tempdir"); + let config = RepoConfig::read(dir.path()).expect("Failed to read config file"); + assert!(!config.is_allowed("foo")); + assert!(config.is_allowed("bar")); + } + + #[test] + fn default() { + let dir = tempfile::tempdir().expect("Failed to create tempdir"); + let config = RepoConfig::read(dir.path()).expect("Failed to get default config"); + assert!(config.is_allowed("foo")); + } + + #[test] + fn parse_error() { + let dir = tempfile::tempdir().expect("Failed to create tempdir"); + write(dir.path().join(CONFIG_FILE_NAME), r#"blah"#).expect("Failed to write to tempdir"); + assert!(matches!(RepoConfig::read(dir.path()), Err(Error::TomlParseError(_)))); + } +}