1 /*
2 * Copyright (C) 2022, The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17 //! Reports redundant AIDL libraries included in a partition.
18
19 use anyhow::{Context, Result};
20 use clap::Parser;
21 use std::collections::BTreeMap;
22 use std::fs::File;
23 use std::io::BufReader;
24 use std::path::{Path, PathBuf};
25
26 #[derive(Parser, Debug)]
27 #[structopt()]
28 struct Opt {
29 /// JSON file with list of files installed in a partition, e.g. "$OUT/installed-files.json".
30 #[clap(long)]
31 installed_files_json: PathBuf,
32
33 /// JSON file with metadata for AIDL interfaces. Optional, but fewer checks are performed when
34 /// unset.
35 #[clap(long)]
36 aidl_metadata_json: Option<PathBuf>,
37 }
38
39 /// "aidl_metadata.json" entry.
40 #[derive(Debug, serde::Deserialize)]
41 struct AidlInterfaceMetadata {
42 /// Name of module defining package.
43 name: String,
44 }
45
46 /// "installed-files.json" entry.
47 #[derive(Debug, serde::Deserialize)]
48 struct InstalledFile {
49 /// Full file path.
50 #[serde(rename = "Name")]
51 name: String,
52 /// File size.
53 #[serde(rename = "Size")]
54 size: u64,
55 }
56
57 #[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
58 enum LibDir {
59 Lib,
60 Lib64,
61 }
62
63 /// An instance of an AIDL interface lib.
64 #[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
65 struct AidlInstance {
66 installed_path: String,
67 size: u64,
68 name: String,
69 variant: String, // e.g. "ndk" or "cpp"
70 version: usize,
71 lib_dir: LibDir,
72 }
73
74 /// Deserializes a JSON file at `path` into an object of type `T`.
read_json_file<T: serde::de::DeserializeOwned>(path: &Path) -> Result<T>75 fn read_json_file<T: serde::de::DeserializeOwned>(path: &Path) -> Result<T> {
76 let file = File::open(path).with_context(|| format!("failed to open: {}", path.display()))?;
77 serde_json::from_reader(BufReader::new(file))
78 .with_context(|| format!("failed to read: {}", path.display()))
79 }
80
81 /// Extracts AIDL lib info an `InstalledFile`, mainly by parsing the file path. Returns `None` if
82 /// it doesn't look like an AIDL lib.
extract_aidl_instance(installed_file: &InstalledFile) -> Option<AidlInstance>83 fn extract_aidl_instance(installed_file: &InstalledFile) -> Option<AidlInstance> {
84 // example: android.hardware.security.keymint-V2-ndk.so
85 let lib_regex = regex::Regex::new(r".*/(lib|lib64)/([^-]*)-V(\d+)-([^.]+)\.")
86 .expect("failed to parse regex");
87 let captures = lib_regex.captures(&installed_file.name)?;
88 let (dir, name, version, variant) = (&captures[1], &captures[2], &captures[3], &captures[4]);
89 Some(AidlInstance {
90 installed_path: installed_file.name.clone(),
91 size: installed_file.size,
92 name: name.to_string(),
93 variant: variant.to_string(),
94 version: version.parse().unwrap(),
95 lib_dir: if dir == "lib64" { LibDir::Lib64 } else { LibDir::Lib },
96 })
97 }
98
main() -> Result<()>99 fn main() -> Result<()> {
100 let args = Opt::parse();
101
102 // Read the metadata file if available.
103 let metadata_list: Option<Vec<AidlInterfaceMetadata>> = match &args.aidl_metadata_json {
104 Some(aidl_metadata_json) => read_json_file(aidl_metadata_json)?,
105 None => None,
106 };
107 let is_valid_aidl_lib = |name: &str| match &metadata_list {
108 Some(x) => x.iter().any(|metadata| metadata.name == name),
109 None => true,
110 };
111
112 // Read the "installed-files.json" and create a list of AidlInstance.
113 let installed_files: Vec<InstalledFile> = read_json_file(&args.installed_files_json)?;
114 let instances: Vec<AidlInstance> = installed_files
115 .iter()
116 .filter_map(extract_aidl_instance)
117 .filter(|instance| {
118 if !is_valid_aidl_lib(&instance.name) {
119 eprintln!(
120 "WARNING: {} looks like an AIDL lib, but has no metadata",
121 &instance.installed_path
122 );
123 return false;
124 }
125 true
126 })
127 .collect();
128
129 // Group redundant AIDL lib instances together.
130 let groups: BTreeMap<(String, LibDir), Vec<&AidlInstance>> =
131 instances.iter().fold(BTreeMap::new(), |mut acc, x| {
132 let key = (x.name.clone(), x.lib_dir);
133 acc.entry(key).or_default().push(x);
134 acc
135 });
136 let mut total_wasted_bytes = 0;
137 for (group_key, mut instances) in groups {
138 if instances.len() > 1 {
139 instances.sort();
140 // Prefer the highest version, break ties favoring ndk.
141 let preferred_instance = instances
142 .iter()
143 .max_by_key(|x| (x.version, i32::from(x.variant == "ndk")))
144 .unwrap();
145 let wasted_bytes: u64 =
146 instances.iter().filter(|x| *x != preferred_instance).map(|x| x.size).sum();
147 println!("Found redundant AIDL instances for {:?}", group_key);
148 for instance in instances.iter() {
149 println!(
150 "\t{}\t({:.2} KiB){}",
151 instance.installed_path,
152 instance.size as f64 / 1024.0,
153 if instance == preferred_instance { " <- preferred" } else { "" }
154 );
155 }
156 total_wasted_bytes += wasted_bytes;
157 println!("\t(potential savings: {:.2} KiB)", wasted_bytes as f64 / 1024.0);
158 println!();
159 }
160 }
161 println!("total potential savings: {:.2} KiB", total_wasted_bytes as f64 / 1024.0);
162
163 Ok(())
164 }
165
166 #[cfg(test)]
167 mod tests {
168 use super::*;
169 use clap::CommandFactory;
170
171 #[test]
verify_opt()172 fn verify_opt() {
173 Opt::command().debug_assert();
174 }
175 }
176