/* * 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. */ //! Reports redundant AIDL libraries included in a partition. use anyhow::{Context, Result}; use clap::Parser; use std::collections::BTreeMap; use std::fs::File; use std::io::BufReader; use std::path::{Path, PathBuf}; #[derive(Parser, Debug)] #[structopt()] struct Opt { /// JSON file with list of files installed in a partition, e.g. "$OUT/installed-files.json". #[clap(long)] installed_files_json: PathBuf, /// JSON file with metadata for AIDL interfaces. Optional, but fewer checks are performed when /// unset. #[clap(long)] aidl_metadata_json: Option, } /// "aidl_metadata.json" entry. #[derive(Debug, serde::Deserialize)] struct AidlInterfaceMetadata { /// Name of module defining package. name: String, } /// "installed-files.json" entry. #[derive(Debug, serde::Deserialize)] struct InstalledFile { /// Full file path. #[serde(rename = "Name")] name: String, /// File size. #[serde(rename = "Size")] size: u64, } #[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] enum LibDir { Lib, Lib64, } /// An instance of an AIDL interface lib. #[derive(Debug, PartialEq, Eq, PartialOrd, Ord)] struct AidlInstance { installed_path: String, size: u64, name: String, variant: String, // e.g. "ndk" or "cpp" version: usize, lib_dir: LibDir, } /// Deserializes a JSON file at `path` into an object of type `T`. fn read_json_file(path: &Path) -> Result { let file = File::open(path).with_context(|| format!("failed to open: {}", path.display()))?; serde_json::from_reader(BufReader::new(file)) .with_context(|| format!("failed to read: {}", path.display())) } /// Extracts AIDL lib info an `InstalledFile`, mainly by parsing the file path. Returns `None` if /// it doesn't look like an AIDL lib. fn extract_aidl_instance(installed_file: &InstalledFile) -> Option { // example: android.hardware.security.keymint-V2-ndk.so let lib_regex = regex::Regex::new(r".*/(lib|lib64)/([^-]*)-V(\d+)-([^.]+)\.") .expect("failed to parse regex"); let captures = lib_regex.captures(&installed_file.name)?; let (dir, name, version, variant) = (&captures[1], &captures[2], &captures[3], &captures[4]); Some(AidlInstance { installed_path: installed_file.name.clone(), size: installed_file.size, name: name.to_string(), variant: variant.to_string(), version: version.parse().unwrap(), lib_dir: if dir == "lib64" { LibDir::Lib64 } else { LibDir::Lib }, }) } fn main() -> Result<()> { let args = Opt::parse(); // Read the metadata file if available. let metadata_list: Option> = match &args.aidl_metadata_json { Some(aidl_metadata_json) => read_json_file(aidl_metadata_json)?, None => None, }; let is_valid_aidl_lib = |name: &str| match &metadata_list { Some(x) => x.iter().any(|metadata| metadata.name == name), None => true, }; // Read the "installed-files.json" and create a list of AidlInstance. let installed_files: Vec = read_json_file(&args.installed_files_json)?; let instances: Vec = installed_files .iter() .filter_map(extract_aidl_instance) .filter(|instance| { if !is_valid_aidl_lib(&instance.name) { eprintln!( "WARNING: {} looks like an AIDL lib, but has no metadata", &instance.installed_path ); return false; } true }) .collect(); // Group redundant AIDL lib instances together. let groups: BTreeMap<(String, LibDir), Vec<&AidlInstance>> = instances.iter().fold(BTreeMap::new(), |mut acc, x| { let key = (x.name.clone(), x.lib_dir); acc.entry(key).or_default().push(x); acc }); let mut total_wasted_bytes = 0; for (group_key, mut instances) in groups { if instances.len() > 1 { instances.sort(); // Prefer the highest version, break ties favoring ndk. let preferred_instance = instances .iter() .max_by_key(|x| (x.version, i32::from(x.variant == "ndk"))) .unwrap(); let wasted_bytes: u64 = instances.iter().filter(|x| *x != preferred_instance).map(|x| x.size).sum(); println!("Found redundant AIDL instances for {:?}", group_key); for instance in instances.iter() { println!( "\t{}\t({:.2} KiB){}", instance.installed_path, instance.size as f64 / 1024.0, if instance == preferred_instance { " <- preferred" } else { "" } ); } total_wasted_bytes += wasted_bytes; println!("\t(potential savings: {:.2} KiB)", wasted_bytes as f64 / 1024.0); println!(); } } println!("total potential savings: {:.2} KiB", total_wasted_bytes as f64 / 1024.0); Ok(()) } #[cfg(test)] mod tests { use super::*; use clap::CommandFactory; #[test] fn verify_opt() { Opt::command().debug_assert(); } }