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 lazy_static::lazy_static;
9 use regex::Regex;
10 use serde::{Deserialize, Serialize};
11 use std::fs;
12 use std::io::BufReader;
13 use std::path::PathBuf;
14 use std::process;
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 lazy_static! {
302     static ref NINJA_OUT_PATH_MATCHER: Regex =
303         Regex::new(r"^[^/]+/target/product/[^/]+/(.+)$").expect("regex does not compile");
304 }
305 
strip_product_prefix(path: &str) -> Option<String>306 fn strip_product_prefix(path: &str) -> Option<String> {
307     NINJA_OUT_PATH_MATCHER.captures(path).map(|x| x[1].to_string())
308 }
309 
310 #[cfg(test)]
311 mod tests {
312     use super::*;
313     use tempfile::TempDir;
314 
315     #[test]
load_creates_new_config_with_droid() -> Result<()>316     fn load_creates_new_config_with_droid() -> Result<()> {
317         let home_dir = TempDir::new()?;
318         let config_path = home_dir.path().join("config.json").display().to_string();
319         let config = Config::load(&Some(config_path));
320         assert_eq!("droid", config?.base);
321         Ok(())
322     }
323 
324     #[test]
track_updates_config_file() -> Result<()>325     fn track_updates_config_file() -> Result<()> {
326         let home_dir = TempDir::new()?;
327         let config_path = home_dir.path().join("config.json").display().to_string();
328         let mut config = Config::load(&Some(config_path.clone()))?;
329         config.track(&["supermod".to_string()])?;
330         config.track(&["another".to_string()])?;
331         // Updates in-memory version, which gets sorted and deduped.
332         assert_eq!(vec!["another".to_string(), "supermod".to_string()], config.modules);
333 
334         // Check the disk version too.
335         let config2 = Config::load(&Some(config_path))?;
336         assert_eq!(config, config2);
337         Ok(())
338     }
339 
340     #[test]
untrack_updates_config() -> Result<()>341     fn untrack_updates_config() -> Result<()> {
342         let home_dir = TempDir::new()?;
343         let config_path = Config::default_path(&path(&home_dir)).context("Writing config")?;
344         std::fs::write(
345             config_path.clone(),
346             r#"{"base": "droid",  "modules": [ "mod_one", "mod_two" ]}"#,
347         )?;
348         let mut config = Config::load(&Some(config_path.clone())).context("LOAD")?;
349         assert_eq!(2, config.modules.len());
350         // Updates in-memory version.
351         config.untrack(&["mod_two".to_string()]).context("UNTRACK")?;
352         assert_eq!(vec!["mod_one"], config.modules);
353         // Updates on-disk version.
354         Ok(())
355     }
356 
357     #[test]
ninja_args_updated_based_on_config()358     fn ninja_args_updated_based_on_config() {
359         let config =
360             Config { base: s("DROID"), modules: vec![s("ADEVICE_FP")], config_path: s("") };
361         assert_eq!(
362             crate::commands::split_string(
363                 "-f outdir/combined-lynx.ninja -t inputs -i DROID ADEVICE_FP"
364             ),
365             config.ninja_args("lynx", "outdir")
366         );
367         // Find the args passed to ninja
368     }
369 
370     #[test]
ninja_output_filtered_to_android_product_out() -> Result<()>371     fn ninja_output_filtered_to_android_product_out() -> Result<()> {
372         // Ensure only paths matching */target/product/ remain
373         let fake_out = vec![
374             // 2 good ones
375             "innie/target/product/vsoc_x86_64/system/app/BasicDreams/BasicDreams.apk\n",
376             "innie/target/product/vsoc_x86_64/system/app/BookmarkProvider/BookmarkProvider.apk\n",
377             // Target/product not at right position
378             "innie/nested/target/product/vsoc_x86_64/system/NOT_FOUND\n",
379             // Different partition
380             "innie/target/product/vsoc_x86_64/OTHER_PARTITION/app/BasicDreams/BasicDreams2.apk\n",
381             // Good again.
382             "innie/target/product/vsoc_x86_64/system_ext/ok_file\n",
383         ];
384 
385         let output = process::Command::new("echo")
386             .args(&fake_out)
387             .output()
388             .context("Running ECHO to generate output")?;
389 
390         assert_eq!(
391             vec![
392                 "system/app/BasicDreams/BasicDreams.apk",
393                 "system/app/BookmarkProvider/BookmarkProvider.apk",
394                 "OTHER_PARTITION/app/BasicDreams/BasicDreams2.apk",
395                 "system_ext/ok_file",
396             ],
397             tracked_files(&output)?
398         );
399         Ok(())
400     }
401 
402     #[test]
check_ninja_failure_msg_for_tracked_module()403     fn check_ninja_failure_msg_for_tracked_module() {
404         // User tracks 'fish', which isn't a real module.
405         let config = Config { base: s("DROID"), modules: vec![s("fish")], config_path: s("") };
406         let msg = config.ninja_failure_msg(" error: unknown target 'fish', did you mean 'sh'");
407 
408         assert!(msg.contains("adevice untrack fish"), "Actual: {msg}")
409     }
410 
411     #[test]
check_ninja_failure_msg_for_special_base()412     fn check_ninja_failure_msg_for_special_base() {
413         let config = Config { base: s("R2D2_DROID"), modules: Vec::new(), config_path: s("") };
414         let msg = config.ninja_failure_msg(" error: unknown target 'R2D2_DROID'");
415 
416         assert!(msg.contains("adevice track-base"), "Actual: {msg}")
417     }
418 
419     #[test]
check_ninja_failure_msg_unrelated()420     fn check_ninja_failure_msg_unrelated() {
421         // User tracks 'bait', which is a real module, but gets some other error message.
422         let config = Config { base: s("DROID"), modules: vec![s("bait")], config_path: s("") };
423 
424         // There should be no untrack command.
425         assert!(!config
426             .ninja_failure_msg(" error: unknown target 'fish', did you mean 'sh'")
427             .contains("untrack"))
428     }
429 
430     /*
431     // Ensure we match the whole path component, i.e. "sys" should not match system.
432     #[test]
433     fn test_partition_filtering_partition_name_matches_path_component() {
434         let ninja_deps = vec![
435             "system/file1".to_string(),
436             "system_ext/file2".to_string(),
437             "file3".to_string(),
438             "data/sys/file4".to_string(),
439         ];
440         assert_eq!(
441             Vec::<String>::new(),
442             crate::tracking::filter_partitions(&ninja_deps, &[PathBuf::from("sys")])
443         );
444     }*/
445 
446     // Convert TempDir to string we can use for fs::write/read.
path(dir: &TempDir) -> String447     fn path(dir: &TempDir) -> String {
448         dir.path().display().to_string()
449     }
450 
451     // Tired of typing to_string()
s(str: &str) -> String452     fn s(str: &str) -> String {
453         str.to_string()
454     }
455 }
456