blob: 401eeb742af0768f5047288b284d5aa4d2890af6 [file] [log] [blame]
/*
* 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<PathBuf>,
}
/// "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<T: serde::de::DeserializeOwned>(path: &Path) -> Result<T> {
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<AidlInstance> {
// 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<Vec<AidlInterfaceMetadata>> = 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<InstalledFile> = read_json_file(&args.installed_files_json)?;
let instances: Vec<AidlInstance> = 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();
}
}