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