1 /// Module to keep track of which files should be pushed to a device.
2 /// Composed of:
3 /// 1) A tracking config that lets user specify modules to
4 /// augment a base image (droid).
5 /// 2) Integration with ninja to derive "installed" files from
6 /// this module set.
7 use anyhow::{bail, Context, Result};
8 use regex::Regex;
9 use serde::{Deserialize, Serialize};
10 use std::fs;
11 use std::io::BufReader;
12 use std::path::PathBuf;
13 use std::process;
14 use std::sync::LazyLock;
15 use tracing::{debug, warn};
16
17 #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
18 pub struct Config {
19 pub base: String,
20 pub modules: Vec<String>,
21 #[serde(default, skip_serializing, skip_deserializing)]
22 config_path: String,
23 }
24
25 /// Object representing the files that are _tracked_. These are files that the
26 /// build system indicates should be on the device. Sometimes stale files
27 /// get left in the Product Out tree or extra modules get built into the Product Out tree.
28 /// This tracking config helps us call ninja to distinguish declared depdencies for
29 /// `droid` and what has been built.
30 /// TODO(rbraunstein): Rewrite above clearer.
31 impl Config {
32 /// Load set of tracked modules from User's homedir or return a default one.
33 /// If the user passes a config path, use it. Otherwise use the
34 /// default path in their home dir.
load(config_path: &Option<String>) -> Result<Self>35 pub fn load(config_path: &Option<String>) -> Result<Self> {
36 match &config_path {
37 Some(path) => Self::from_json_file(path),
38 None => match std::env::var("HOME") {
39 Ok(home) if !home.is_empty() => Self::load(&Some(Self::default_path(&home)?)),
40 _ => Ok(Self::default()),
41 },
42 }
43 }
44
45 /// Load set of tracked modules from the given path or return a default one.
from_json_file(path: &String) -> Result<Self>46 fn from_json_file(path: &String) -> Result<Self> {
47 if let Ok(file) = fs::File::open(path) {
48 let mut config: Config = serde_json::from_reader(BufReader::new(file))
49 .context(format!("Parsing config {path:?}"))?;
50 config.config_path.clone_from(path);
51 return Ok(config);
52 }
53 // Lets not create a default config file until they actually track a module.
54 Ok(Config { base: "droid".to_string(), modules: Vec::new(), config_path: path.clone() })
55 }
56
default() -> Self57 fn default() -> Self {
58 Config { base: "droid".to_string(), modules: Vec::new(), config_path: String::new() }
59 }
60
print(&self)61 pub fn print(&self) {
62 debug!("Tracking base: `{}` and modules {:?}", self.base, self.modules);
63 }
64
65 /// Returns the full path to the serialized config file.
default_path(home: &str) -> Result<String>66 fn default_path(home: &str) -> Result<String> {
67 fs::create_dir_all(format!("{home}/.config/asuite"))?;
68 Ok(format!("{home}/.config/asuite/adevice-tracking.json"))
69 }
70
71 /// Adds the module name to the config and saves it.
track(&mut self, module_names: &[String]) -> Result<()>72 pub fn track(&mut self, module_names: &[String]) -> Result<()> {
73 // TODO(rbraunstein): Validate the module names and warn on bad names.
74 self.modules.extend_from_slice(module_names);
75 self.modules.sort();
76 self.modules.dedup();
77 self.print();
78 self.clear_cache();
79 Self::save(self)
80 }
81
82 /// Update the base module and saves it.
trackbase(&mut self, base: &str) -> Result<()>83 pub fn trackbase(&mut self, base: &str) -> Result<()> {
84 // TODO(rbraunstein): Validate the module names and warn on bad names.
85 self.base = base.to_string();
86 self.print();
87 self.clear_cache();
88 Self::save(self)
89 }
90
91 /// Removes the module name from the config and saves it.
untrack(&mut self, module_names: &[String]) -> Result<()>92 pub fn untrack(&mut self, module_names: &[String]) -> Result<()> {
93 // TODO(rbraunstein): Report if not found?
94 self.modules.retain(|m| !module_names.contains(m));
95 self.print();
96 self.clear_cache();
97 Self::save(self)
98 }
99
100 // Store the config as json at the config_path.
save(&self) -> Result<()>101 fn save(&self) -> Result<()> {
102 if self.config_path.is_empty() {
103 bail!("Can not save config file when HOME is not set and --config not set.")
104 }
105 let mut file = fs::File::create(&self.config_path)
106 .context(format!("Creating config file {:?}", self.config_path))?;
107 serde_json::to_writer_pretty(&mut file, &self).context("Writing config file")?;
108 debug!("Wrote config file {:?}", &self.config_path);
109 Ok(())
110 }
111
112 /// Return all files that are part of the tracked set under ANDROID_PRODUCT_OUT.
113 /// Implementation:
114 /// Runs `ninja` to get all transitive intermediate targets for `droid`.
115 /// These intermediate targets contain all the apks and .sos, etc that
116 /// that get packaged for flashing.
117 /// Filter all the inputs returned by ninja to just those under
118 /// ANDROID_PRODUCT_OUT and explicitly ask for modules in our tracking set.
119 /// Extra or stale files in ANDROID_PRODUCT_OUT from builds will not be part
120 /// of the result.
121 /// The combined.ninja file will be found under:
122 /// ${ANDROID_BUILD_TOP}/${OUT_DIR}/combined-${TARGET_PRODUCT}.ninja
123 /// Tracked files inside that file are relative to $OUT_DIR/target/product/*/
124 /// The final element of the path can be derived from the final element of ANDROID_PRODUCT_OUT,
125 /// but matching against */target/product/* is enough.
126 /// Store all ninja deps in the cache.
tracked_files(&self) -> Result<Vec<String>>127 pub fn tracked_files(&self) -> Result<Vec<String>> {
128 if let Ok(cache) = self.read_cache() {
129 Ok(cache)
130 } else {
131 let ninja_output = self.ninja_output(
132 &self.src_root()?,
133 &self.ninja_args(&self.target_product()?, &self.out_dir()),
134 )?;
135 if !ninja_output.status.success() {
136 let stderr = String::from_utf8(ninja_output.stderr.clone()).unwrap();
137 anyhow::bail!("{}", self.ninja_failure_msg(&stderr));
138 }
139 let unfiltered_tracked_files = tracked_files(&ninja_output)?;
140 self.write_cache(&unfiltered_tracked_files)
141 .unwrap_or_else(|e| warn!("Error writing tracked file cache: {e}"));
142 Ok(unfiltered_tracked_files)
143 }
144 }
145
src_root(&self) -> Result<String>146 pub fn src_root(&self) -> Result<String> {
147 std::env::var("ANDROID_BUILD_TOP")
148 .context("ANDROID_BUILD_TOP must be set. Be sure to run lunch.")
149 }
150
target_product(&self) -> Result<String>151 fn target_product(&self) -> Result<String> {
152 std::env::var("TARGET_PRODUCT").context("TARGET_PRODUCT must be set. Be sure to run lunch.")
153 }
154
out_dir(&self) -> String155 fn out_dir(&self) -> String {
156 std::env::var("OUT_DIR").unwrap_or("out".to_string())
157 }
158
159 // Prepare the ninja command line args, creating the right ninja file name and
160 // appending all the modules.
ninja_args(&self, target_product: &str, out_dir: &str) -> Vec<String>161 fn ninja_args(&self, target_product: &str, out_dir: &str) -> Vec<String> {
162 // Create `ninja -f combined.ninja -t input -i BASE MOD1 MOD2 ....`
163 // The `-i` for intermediary is what gives the PRODUCT_OUT files.
164 let mut args = vec![
165 "-f".to_string(),
166 format!("{out_dir}/combined-{target_product}.ninja"),
167 "-t".to_string(),
168 "inputs".to_string(),
169 "-i".to_string(),
170 self.base.clone(),
171 ];
172 for module in self.modules.clone() {
173 args.push(module);
174 }
175 args
176 }
177
178 // Call ninja.
ninja_output(&self, src_root: &str, args: &[String]) -> Result<process::Output>179 fn ninja_output(&self, src_root: &str, args: &[String]) -> Result<process::Output> {
180 // TODO(rbraunstein): Deal with non-linux-x86.
181 let path = "prebuilts/build-tools/linux-x86/bin/ninja";
182 debug!("Running {path} {args:?}");
183 process::Command::new(path)
184 .current_dir(src_root)
185 .args(args)
186 .output()
187 .context("Running ninja to get base files")
188 }
189
190 /// Check to see if the output from running ninja mentions a module we are tracking.
191 /// If a user tracks a module, but then removes it from the codebase, they should be notified.
192 /// Return origina ninja error and possibly a statement suggesting they `untrack` a module.
ninja_failure_msg(&self, stderr: &str) -> String193 fn ninja_failure_msg(&self, stderr: &str) -> String {
194 // A stale tracked target will look something like this:
195 // unknown target 'SomeStaleModule'
196 let mut msg = String::new();
197 for tracked_module in &self.modules {
198 if stderr.contains(tracked_module) {
199 msg = format!("You may need to `adevice untrack {}`", tracked_module);
200 }
201 }
202 if stderr.contains(&self.base) {
203 msg = format!(
204 "You may need to `adevice track-base` something other than `{}`",
205 &self.base
206 );
207 }
208 format!("{}{}", stderr, msg)
209 }
210
clear_cache(&self)211 pub fn clear_cache(&self) {
212 let path = self.cache_path();
213 if path.is_err() {
214 warn!("Error getting the cache path {:?}", path.err().unwrap());
215 return;
216 }
217 match std::fs::remove_file(path.unwrap()) {
218 Ok(_) => (),
219 Err(e) => {
220 // Probably the cache has already been cleared and we can't remove it again.
221 debug!("Error clearing the cache {e}");
222 }
223 }
224 }
225
226 // If our cache (in the out_dir) is newer than the ninja file, then use it rather
227 // than rerun ninja. Saves about 2 secs.
228 // Returns Err if cache not found or if cache is stale.
229 // Otherwise returns the stdout from the ninja command.
230 // TODO(rbraunstein): I don't think the cache is effective. I think the combined
231 // ninja file gets touched after every `m`. Either use the subninja or just turn off caching.
read_cache(&self) -> Result<Vec<String>>232 fn read_cache(&self) -> Result<Vec<String>> {
233 let cache_path = self.cache_path()?;
234 let ninja_file_path = PathBuf::from(&self.src_root()?)
235 .join(self.out_dir())
236 .join(format!("combined-{}.ninja", self.target_product()?));
237 // cache file is too old.
238 // TODO(rbraunstein): Need integration tests for this.
239 // Adding and removing tracked modules affects the cache too.
240 debug!("Reading cache {cache_path}");
241 let cache_time = fs::metadata(&cache_path)?.modified()?;
242 debug!("Reading ninja {ninja_file_path:?}");
243 let ninja_file_time = fs::metadata(ninja_file_path)?.modified()?;
244 if cache_time.lt(&ninja_file_time) {
245 debug!("Cache is too old: {cache_time:?}, ninja file time {ninja_file_time:?}");
246 anyhow::bail!("cache is stale");
247 }
248 debug!("Using ninja file cache");
249 Ok(fs::read_to_string(&cache_path)?.split('\n').map(|s| s.to_string()).collect())
250 }
251
cache_path(&self) -> Result<String>252 fn cache_path(&self) -> Result<String> {
253 Ok([
254 self.src_root()?,
255 self.out_dir(),
256 format!("adevice-ninja-deps-{}.cache", self.target_product()?),
257 ]
258 // TODO(rbraunstein): Fix OS separator.
259 .join("/"))
260 }
261
262 // Unconditionally write the given byte stream to the cache file
263 // overwriting whatever is there.
write_cache(&self, data: &[String]) -> Result<()>264 fn write_cache(&self, data: &[String]) -> Result<()> {
265 let cache_path = self.cache_path()?;
266 debug!("Wrote cache file: {cache_path:?}");
267 fs::write(cache_path, data.join("\n"))?;
268 Ok(())
269 }
270 }
271
272 /// Iterate through the `ninja -t input -i MOD...` output
273 /// to find files in the PRODUCT_OUT directory.
tracked_files(output: &process::Output) -> Result<Vec<String>>274 fn tracked_files(output: &process::Output) -> Result<Vec<String>> {
275 let stdout = &output.stdout;
276 let stderr = &output.stderr;
277 debug!("NINJA calculated deps: {}", stdout.len());
278 if output.status.code().unwrap() > 0 || !stderr.is_empty() {
279 warn!("code: {} {:?}", output.status, String::from_utf8(stderr.to_owned()));
280 }
281 Ok(String::from_utf8(stdout.to_owned())?
282 .lines()
283 .filter_map(|line| {
284 if let Some(device_path) = strip_product_prefix(line) {
285 return Some(device_path);
286 }
287 None
288 })
289 .collect())
290 }
291
292 // The ninja output for the files we are interested in will look like this:
293 // % OUT_DIR=innie m nothing
294 // % (cd $ANDROID_BUILD_TOP;prebuilts/build-tools/linux-x86/bin/ninja -f innie/combined-aosp_cf_x86_64_phone.ninja -t inputs -i droid | grep innie/target/product/vsoc_x86_64/system) | grep apk | head
295 // innie/target/product/vsoc_x86_64/system/app/BasicDreams/BasicDreams.apk
296 // innie/target/product/vsoc_x86_64/system/app/BluetoothMidiService/BluetoothMidiService.apk
297 // innie/target/product/vsoc_x86_64/system/app/BookmarkProvider/BookmarkProvider.apk
298 // innie/target/product/vsoc_x86_64/system/app/CameraExtensionsProxy/CameraExtensionsProxy.apk
299 // Match any files with target/product as the second and third dir paths and capture
300 // everything from 5th path element to the end.
301 static NINJA_OUT_PATH_MATCHER: LazyLock<Regex> = LazyLock::new(|| {
302 Regex::new(r"^[^/]+/target/product/[^/]+/(.+)$").expect("regex does not compile")
303 });
304
strip_product_prefix(path: &str) -> Option<String>305 fn strip_product_prefix(path: &str) -> Option<String> {
306 NINJA_OUT_PATH_MATCHER.captures(path).map(|x| x[1].to_string())
307 }
308
309 #[cfg(test)]
310 mod tests {
311 use super::*;
312 use tempfile::TempDir;
313
314 #[test]
load_creates_new_config_with_droid() -> Result<()>315 fn load_creates_new_config_with_droid() -> Result<()> {
316 let home_dir = TempDir::new()?;
317 let config_path = home_dir.path().join("config.json").display().to_string();
318 let config = Config::load(&Some(config_path));
319 assert_eq!("droid", config?.base);
320 Ok(())
321 }
322
323 #[test]
track_updates_config_file() -> Result<()>324 fn track_updates_config_file() -> Result<()> {
325 let home_dir = TempDir::new()?;
326 let config_path = home_dir.path().join("config.json").display().to_string();
327 let mut config = Config::load(&Some(config_path.clone()))?;
328 config.track(&["supermod".to_string()])?;
329 config.track(&["another".to_string()])?;
330 // Updates in-memory version, which gets sorted and deduped.
331 assert_eq!(vec!["another".to_string(), "supermod".to_string()], config.modules);
332
333 // Check the disk version too.
334 let config2 = Config::load(&Some(config_path))?;
335 assert_eq!(config, config2);
336 Ok(())
337 }
338
339 #[test]
untrack_updates_config() -> Result<()>340 fn untrack_updates_config() -> Result<()> {
341 let home_dir = TempDir::new()?;
342 let config_path = Config::default_path(&path(&home_dir)).context("Writing config")?;
343 std::fs::write(
344 config_path.clone(),
345 r#"{"base": "droid", "modules": [ "mod_one", "mod_two" ]}"#,
346 )?;
347 let mut config = Config::load(&Some(config_path.clone())).context("LOAD")?;
348 assert_eq!(2, config.modules.len());
349 // Updates in-memory version.
350 config.untrack(&["mod_two".to_string()]).context("UNTRACK")?;
351 assert_eq!(vec!["mod_one"], config.modules);
352 // Updates on-disk version.
353 Ok(())
354 }
355
356 #[test]
ninja_args_updated_based_on_config()357 fn ninja_args_updated_based_on_config() {
358 let config =
359 Config { base: s("DROID"), modules: vec![s("ADEVICE_FP")], config_path: s("") };
360 assert_eq!(
361 crate::commands::split_string(
362 "-f outdir/combined-lynx.ninja -t inputs -i DROID ADEVICE_FP"
363 ),
364 config.ninja_args("lynx", "outdir")
365 );
366 // Find the args passed to ninja
367 }
368
369 #[test]
ninja_output_filtered_to_android_product_out() -> Result<()>370 fn ninja_output_filtered_to_android_product_out() -> Result<()> {
371 // Ensure only paths matching */target/product/ remain
372 let fake_out = vec![
373 // 2 good ones
374 "innie/target/product/vsoc_x86_64/system/app/BasicDreams/BasicDreams.apk\n",
375 "innie/target/product/vsoc_x86_64/system/app/BookmarkProvider/BookmarkProvider.apk\n",
376 // Target/product not at right position
377 "innie/nested/target/product/vsoc_x86_64/system/NOT_FOUND\n",
378 // Different partition
379 "innie/target/product/vsoc_x86_64/OTHER_PARTITION/app/BasicDreams/BasicDreams2.apk\n",
380 // Good again.
381 "innie/target/product/vsoc_x86_64/system_ext/ok_file\n",
382 ];
383
384 let output = process::Command::new("echo")
385 .args(&fake_out)
386 .output()
387 .context("Running ECHO to generate output")?;
388
389 assert_eq!(
390 vec![
391 "system/app/BasicDreams/BasicDreams.apk",
392 "system/app/BookmarkProvider/BookmarkProvider.apk",
393 "OTHER_PARTITION/app/BasicDreams/BasicDreams2.apk",
394 "system_ext/ok_file",
395 ],
396 tracked_files(&output)?
397 );
398 Ok(())
399 }
400
401 #[test]
check_ninja_failure_msg_for_tracked_module()402 fn check_ninja_failure_msg_for_tracked_module() {
403 // User tracks 'fish', which isn't a real module.
404 let config = Config { base: s("DROID"), modules: vec![s("fish")], config_path: s("") };
405 let msg = config.ninja_failure_msg(" error: unknown target 'fish', did you mean 'sh'");
406
407 assert!(msg.contains("adevice untrack fish"), "Actual: {msg}")
408 }
409
410 #[test]
check_ninja_failure_msg_for_special_base()411 fn check_ninja_failure_msg_for_special_base() {
412 let config = Config { base: s("R2D2_DROID"), modules: Vec::new(), config_path: s("") };
413 let msg = config.ninja_failure_msg(" error: unknown target 'R2D2_DROID'");
414
415 assert!(msg.contains("adevice track-base"), "Actual: {msg}")
416 }
417
418 #[test]
check_ninja_failure_msg_unrelated()419 fn check_ninja_failure_msg_unrelated() {
420 // User tracks 'bait', which is a real module, but gets some other error message.
421 let config = Config { base: s("DROID"), modules: vec![s("bait")], config_path: s("") };
422
423 // There should be no untrack command.
424 assert!(!config
425 .ninja_failure_msg(" error: unknown target 'fish', did you mean 'sh'")
426 .contains("untrack"))
427 }
428
429 /*
430 // Ensure we match the whole path component, i.e. "sys" should not match system.
431 #[test]
432 fn test_partition_filtering_partition_name_matches_path_component() {
433 let ninja_deps = vec![
434 "system/file1".to_string(),
435 "system_ext/file2".to_string(),
436 "file3".to_string(),
437 "data/sys/file4".to_string(),
438 ];
439 assert_eq!(
440 Vec::<String>::new(),
441 crate::tracking::filter_partitions(&ninja_deps, &[PathBuf::from("sys")])
442 );
443 }*/
444
445 // Convert TempDir to string we can use for fs::write/read.
path(dir: &TempDir) -> String446 fn path(dir: &TempDir) -> String {
447 dir.path().display().to_string()
448 }
449
450 // Tired of typing to_string()
s(str: &str) -> String451 fn s(str: &str) -> String {
452 str.to_string()
453 }
454 }
455