xref: /aosp_15_r20/tools/asuite/adevice/src/adevice.rs (revision c2e18aaa1096c836b086f94603d04f4eb9cf37f5)
1 use crate::cli;
2 use crate::commands;
3 use crate::device;
4 use crate::fingerprint;
5 use crate::metrics;
6 use crate::progress;
7 use crate::restart_chooser;
8 use crate::tracking::Config;
9 use anyhow::{anyhow, bail, Context, Result};
10 use fingerprint::{DiffMode, FileMetadata};
11 use itertools::Itertools;
12 use metrics::MetricSender;
13 use rayon::prelude::*;
14 use regex::Regex;
15 use restart_chooser::RestartChooser;
16 use tracing::{debug, Level};
17 
18 use std::collections::{HashMap, HashSet};
19 use std::ffi::OsString;
20 use std::fs;
21 use std::fs::File;
22 use std::io::{stdin, Write};
23 use std::path::{Path, PathBuf};
24 use std::sync::{LazyLock, Mutex};
25 use std::time::Duration;
26 
27 /// Methods that interact with the host, like fingerprinting and calling ninja to get deps.
28 pub trait Host {
29     /// Return all files in the given partitions at the partition_root along with metadata for those files.
30     /// The keys in the returned hashmap will be relative to partition_root.
fingerprint( &self, partition_root: &Path, partitions: &[PathBuf], ) -> Result<HashMap<PathBuf, FileMetadata>>31     fn fingerprint(
32         &self,
33         partition_root: &Path,
34         partitions: &[PathBuf],
35     ) -> Result<HashMap<PathBuf, FileMetadata>>;
36 
37     /// Return a list of all files that compose `droid` or whatever base and tracked
38     /// modules are listed in `config`.
39     /// Result strings are device relative. (i.e. start with system)
tracked_files(&self, config: &Config) -> Result<Vec<String>>40     fn tracked_files(&self, config: &Config) -> Result<Vec<String>>;
41 }
42 
43 /// Methods to interact with the device, like adb, rebooting, and fingerprinting.
44 pub trait Device {
45     /// Run the `commands` and return the stdout as a string.  If there is non-zero return code
46     /// or output on stderr, then the result is an Err.
run_adb_command(&self, args: &commands::AdbCommand) -> Result<String>47     fn run_adb_command(&self, args: &commands::AdbCommand) -> Result<String>;
48 
run_raw_adb_command(&self, args: &[String]) -> Result<String>49     fn run_raw_adb_command(&self, args: &[String]) -> Result<String>;
50 
51     /// Send commands to reboot device.
reboot(&self) -> Result<String>52     fn reboot(&self) -> Result<String>;
53     /// Send commands to do a soft restart.
soft_restart(&self) -> Result<String>54     fn soft_restart(&self) -> Result<String>;
55 
56     /// Call the fingerprint program on the device.
fingerprint(&self, partitions: &[String]) -> Result<HashMap<PathBuf, FileMetadata>>57     fn fingerprint(&self, partitions: &[String]) -> Result<HashMap<PathBuf, FileMetadata>>;
58 
59     /// Return the list apks that are currently installed, i.e. `adb install`
60     /// which live on the /data partition.
61     /// Returns the package name, i.e. "com.android.shell".
get_installed_apks(&self) -> Result<HashSet<String>>62     fn get_installed_apks(&self) -> Result<HashSet<String>>;
63 
64     /// Wait for the device to be ready after reboots/restarts.
65     /// Returns any relevant output from waiting.
wait(&self, profiler: &mut Profiler) -> Result<String>66     fn wait(&self, profiler: &mut Profiler) -> Result<String>;
67 
68     /// Run the commands needed to prep a userdebug device after a flash.
prep_after_flash(&self, profiler: &mut Profiler) -> Result<()>69     fn prep_after_flash(&self, profiler: &mut Profiler) -> Result<()>;
70 }
71 
72 pub struct RealHost {}
73 
74 impl Default for RealHost {
default() -> Self75     fn default() -> Self {
76         Self::new()
77     }
78 }
79 
80 impl RealHost {
new() -> RealHost81     pub fn new() -> RealHost {
82         RealHost {}
83     }
84 }
85 
86 impl Host for RealHost {
fingerprint( &self, partition_root: &Path, partitions: &[PathBuf], ) -> Result<HashMap<PathBuf, FileMetadata>>87     fn fingerprint(
88         &self,
89         partition_root: &Path,
90         partitions: &[PathBuf],
91     ) -> Result<HashMap<PathBuf, FileMetadata>> {
92         fingerprint::fingerprint_partitions(partition_root, partitions)
93     }
94 
tracked_files(&self, config: &Config) -> Result<Vec<String>>95     fn tracked_files(&self, config: &Config) -> Result<Vec<String>> {
96         config.tracked_files()
97     }
98 }
99 
100 /// Time how long it takes to run the function and store the
101 /// result in the given profiler field.
102 // TODO(rbraunstein): Ideally, use tracing or flamegraph crate or
103 // use Map rather than name all the fields.
104 // See: https://docs.rs/tracing/latest/tracing/index.html#using-the-macros and span!
105 #[macro_export]
106 macro_rules! time {
107     ($fn:expr, $ident:expr) => {{
108         let start = std::time::Instant::now();
109         let result = $fn;
110         $ident = start.elapsed();
111         result
112     }};
113 }
114 
adevice( host: &impl Host, device: &impl Device, cli: &cli::Cli, stdout: &mut impl Write, metrics: &mut impl MetricSender, opt_log_file: Option<File>, profiler: &mut Profiler, ) -> Result<()>115 pub fn adevice(
116     host: &impl Host,
117     device: &impl Device,
118     cli: &cli::Cli,
119     stdout: &mut impl Write,
120     metrics: &mut impl MetricSender,
121     opt_log_file: Option<File>,
122     profiler: &mut Profiler,
123 ) -> Result<()> {
124     // If we can initialize a log file, then setup the tracing/log subscriber to write there.
125     // Otherwise, logs will be dropped.
126     if let Some(log_file) = opt_log_file {
127         let subscriber = tracing_subscriber::fmt()
128             .with_max_level(Level::DEBUG)
129             .with_writer(Mutex::new(log_file))
130             .finish();
131         tracing::subscriber::set_global_default(subscriber)?;
132     }
133 
134     let restart_choice = cli.global_options.restart_choice.clone();
135 
136     let product_out = match &cli.global_options.product_out {
137         Some(po) => PathBuf::from(po),
138         None => get_product_out_from_env().ok_or(anyhow!(
139             "ANDROID_PRODUCT_OUT is not set. Please run source build/envsetup.sh and lunch."
140         ))?,
141     };
142 
143     let track_time = std::time::Instant::now();
144 
145     let mut config = Config::load(&cli.global_options.config_path)?;
146 
147     let command_line = std::env::args().collect::<Vec<String>>().join(" ");
148     metrics.add_start_event(&command_line, &config.src_root()?);
149 
150     // Early return for track/untrack commands.
151     match &cli.command {
152         cli::Commands::Track(names) => return config.track(&names.modules),
153         cli::Commands::TrackBase(base) => return config.trackbase(&base.base),
154         cli::Commands::Untrack(names) => return config.untrack(&names.modules),
155         _ => (),
156     }
157     config.print();
158 
159     writeln!(stdout, " * Checking for files to push to device")?;
160 
161     progress::start("Checking ninja installed files");
162     let mut ninja_installed_files =
163         time!(host.tracked_files(&config)?, profiler.ninja_deps_computer);
164     let partitions =
165         &validate_partitions(&product_out, &ninja_installed_files, &cli.global_options.partitions)?;
166     // Filter to paths on any partitions.
167     ninja_installed_files
168         .retain(|nif| partitions.iter().any(|p| PathBuf::from(nif).starts_with(p)));
169     debug!("Stale file tracking took {} millis", track_time.elapsed().as_millis());
170     progress::update("Checking files on device");
171     let mut device_tree: HashMap<PathBuf, FileMetadata> =
172         time!(device.fingerprint(partitions)?, profiler.device_fingerprint);
173     // We expect the device to create lost+found dirs when mounting
174     // new partitions.  Filter them out as if they don't exist.
175     // However, if there are file inside of them, don't filter the
176     // inner files.
177     for p in partitions {
178         device_tree.remove(&PathBuf::from(p).join("lost+found"));
179     }
180     progress::update("Checking files on host");
181     let partition_paths: Vec<PathBuf> = partitions.iter().map(PathBuf::from).collect();
182     let host_tree =
183         time!(host.fingerprint(&product_out, &partition_paths)?, profiler.host_fingerprint);
184     progress::update("Calculating diffs");
185     // For now ignore diffs in permissions.  This will allow us to have a new adevice host tool
186     // still working with an older adevice_fingerprint device tool.
187     // [It also works on windows hosts]
188     // Version 0.2 of the device tool will support permission mode.
189     // We can check for that version of the tool or check to see if the metadata
190     // on a well-known file (like system/bin/adevice_fingerprint) contains permission
191     // bits before we change this to UsePermissions.
192     let diff_mode = fingerprint::DiffMode::IgnorePermissions;
193 
194     let commands = &get_update_commands(
195         &device_tree,
196         &host_tree,
197         &ninja_installed_files,
198         product_out.clone(),
199         &device.get_installed_apks()?,
200         diff_mode,
201         &partition_paths,
202         cli.global_options.force,
203         stdout,
204     )?;
205     progress::stop();
206     #[allow(clippy::collapsible_if)]
207     if matches!(cli.command, cli::Commands::Status) {
208         if commands.is_empty() {
209             println!("   Device already up to date.");
210         }
211     }
212 
213     let max_changes = cli.global_options.max_allowed_changes;
214     if matches!(cli.command, cli::Commands::Clean { .. }) {
215         let deletes = &commands.deletes;
216         if deletes.is_empty() {
217             println!("   Nothing to clean.");
218             return Ok(());
219         }
220         if deletes.len() > max_changes {
221             bail!("There are {} files to be deleted which exceeds the configured limit of {}.\n  It is recommended that you reimage your device instead.  For small increases in the limit, you can run `adevice clean --max-allowed-changes={}.", deletes.len(), max_changes, deletes.len());
222         }
223         if matches!(cli.command, cli::Commands::Clean { force } if !force) {
224             println!(
225                 "You are about to delete {} [untracked pushed] files. Are you sure? y/N",
226                 deletes.len()
227             );
228             let mut should_delete = String::new();
229             stdin().read_line(&mut should_delete)?;
230             if should_delete.trim().to_lowercase() != "y" {
231                 bail!("Not deleting");
232             }
233         }
234 
235         // Consider always reboot instead of soft restart after a clean.
236         let restart_chooser = &RestartChooser::new(&restart_choice);
237         device::update(restart_chooser, deletes, profiler, device, cli.should_wait())?;
238     }
239 
240     if matches!(cli.command, cli::Commands::Update) {
241         // Status
242         if commands.is_empty() {
243             println!("   Device already up to date.");
244             return Ok(());
245         }
246         let all_cmds: HashMap<PathBuf, commands::AdbCommand> =
247             commands.upserts.clone().into_iter().chain(commands.deletes.clone()).collect();
248 
249         if all_cmds.len() > max_changes {
250             bail!("There are {} files out of date on the device, which exceeds the configured limit of {}.\n  It is recommended to reimage your device.  For small increases in the limit, you can run `adevice update --max-allowed-changes={}.", all_cmds.len(), max_changes, all_cmds.len());
251         }
252         writeln!(stdout, "\n * Updating {} files on device.", all_cmds.len())?;
253 
254         let changed_files = all_cmds.iter().map(|cmd| format!("{:?}", cmd.1.file)).collect();
255         metrics.add_action_event_with_files_changed(
256             "file_updates",
257             Duration::new(0, 0),
258             changed_files,
259         );
260 
261         // Send the update commands, but retry once if we need to remount rw an extra time after a flash.
262         for retry in 0..=1 {
263             let update_result = device::update(
264                 &RestartChooser::new(&restart_choice),
265                 &all_cmds,
266                 profiler,
267                 device,
268                 cli.should_wait(),
269             );
270             progress::stop();
271             if update_result.is_ok() {
272                 break;
273             }
274             if let Err(problem) = update_result {
275                 if retry == 1 {
276                     println!("\n\n");
277                     bail!("  !! Error.  Unable to push to device event after remount/reboot.\n  !! ADB command error: {:?}", problem);
278                 }
279                 // TODO(rbraunstein): Avoid string checks. Either check mounts directly for this case
280                 // or return json with the error message and code from adevice_fingerprint.
281 
282                 if problem.root_cause().to_string().contains("Read-only file system") {
283                     println!(" * The device has a read-only file system.  ");
284                     println!("   After a fresh image, the device needs an extra `remount` and `reboot` to adb push files.");
285                     println!("   Performing remount and reboot.");
286                     println!();
287                 }
288                 time!(device.prep_after_flash(profiler)?, profiler.first_remount_rw);
289             }
290             println!(" * Trying update again after remount and reboot.");
291         }
292     }
293     metrics.display_survey();
294     println!("New android update workflow tool available! go/a-update");
295 
296     Ok(())
297 }
298 
299 /// Returns the commands to update the device for every file that should be updated.
300 /// If there are errors, like some files in the staging set have not been built, then
301 /// an error result is returned.
302 #[allow(clippy::too_many_arguments)]
get_update_commands( device_tree: &HashMap<PathBuf, FileMetadata>, host_tree: &HashMap<PathBuf, FileMetadata>, ninja_installed_files: &[String], product_out: PathBuf, installed_packages: &HashSet<String>, diff_mode: DiffMode, partitions: &[PathBuf], force: bool, stdout: &mut impl Write, ) -> Result<commands::Commands>303 fn get_update_commands(
304     device_tree: &HashMap<PathBuf, FileMetadata>,
305     host_tree: &HashMap<PathBuf, FileMetadata>,
306     ninja_installed_files: &[String],
307     product_out: PathBuf,
308     installed_packages: &HashSet<String>,
309     diff_mode: DiffMode,
310     partitions: &[PathBuf],
311     force: bool,
312     stdout: &mut impl Write,
313 ) -> Result<commands::Commands> {
314     // NOTE: The Ninja deps list can be _ahead_of_ the product tree output list.
315     //      i.e. m `nothing` will update our ninja list even before someone
316     //      does a build to populate product out.
317     //      We don't have a way to know if we are in this case or if the user
318     //      ever did a `m droid`
319 
320     // We add implicit dirs up to the partition name to the tracked set so the set matches the staging set.
321     let mut ninja_installed_dirs: HashSet<PathBuf> =
322         ninja_installed_files.iter().flat_map(|p| parents(p, partitions)).collect();
323     for p in partitions {
324         ninja_installed_dirs.insert(PathBuf::from(p));
325     }
326 
327     let tracked_set: HashSet<PathBuf> =
328         ninja_installed_files.iter().map(PathBuf::from).chain(ninja_installed_dirs).collect();
329     let host_set: HashSet<PathBuf> = host_tree.keys().map(PathBuf::clone).collect();
330 
331     // Files that are in the tracked set but NOT in the build directory. These need
332     // to be built.
333     let needs_building: HashSet<&PathBuf> = tracked_set.difference(&host_set).collect();
334     let status_per_file = &collect_status_per_file(
335         &tracked_set,
336         host_tree,
337         device_tree,
338         &product_out,
339         installed_packages,
340         diff_mode,
341     )?;
342     progress::stop();
343     print_status(stdout, status_per_file)?;
344 
345     // Shadow apks are apks that are installed outside the system partition with `adb install`
346     // If they exist, we should print instructions to uninstall and stop the update.
347     shadow_apk_check(stdout, status_per_file)?;
348 
349     #[allow(clippy::len_zero)]
350     if needs_building.len() > 0 {
351         if force {
352             println!("UNSAFE: The above modules should be built, but were not. This may cause the device to crash:\nProceeding due to \"--force\" flag.");
353         } else {
354             bail!("ERROR: Please build the above modules before updating.\nIf you want to continue anyway (which may cause the device to crash), rerun adevice with the \"--force\" flag.");
355         }
356     }
357 
358     // Restrict the host set down to the ones that are in the tracked set and not installed in the data partition.
359     let filtered_host_set: HashMap<PathBuf, FileMetadata> = host_tree
360         .iter()
361         .filter_map(|(key, value)| {
362             if tracked_set.contains(key) {
363                 Some((key.clone(), value.clone()))
364             } else {
365                 None
366             }
367         })
368         .collect();
369 
370     let filtered_changes = fingerprint::diff(&filtered_host_set, device_tree, diff_mode);
371     Ok(commands::compose(&filtered_changes, &product_out))
372 }
373 
374 // These are the partitions we will try to install to.
375 // ADB sync also has data, oem and vendor.
376 // There are some partition images (like boot.img) that we don't have a good way of determining
377 // the changed status of. (i.e. did they touch files that forces a flash/reimage).
378 // By default we will clean all the default partitions of stale files.
379 const DEFAULT_PARTITIONS: &[&str] = &["system", "system_ext", "odm", "product"];
380 
381 /// If a user explicitly passes a partition, but that doesn't exist in the tracked files,
382 /// then bail.
383 /// Otherwise, if one of the default partitions does not exist (like system_ext), then
384 /// just remove it from the default.
validate_partitions( partition_root: &Path, tracked_files: &[String], cli_partitions: &Option<Vec<String>>, ) -> Result<Vec<String>>385 fn validate_partitions(
386     partition_root: &Path,
387     tracked_files: &[String],
388     cli_partitions: &Option<Vec<String>>,
389 ) -> Result<Vec<String>> {
390     // NOTE: We use PathBuf instead of String so starts_with matches path components.
391     // Use the partitions the user passed in or default to system and system_ext
392     if let Some(partitions) = cli_partitions {
393         for partition in partitions {
394             if !tracked_files.iter().any(|t| PathBuf::from(t).starts_with(partition)) {
395                 bail!("{partition:?} is not a valid partition for current lunch target.");
396             }
397         }
398         for partition in partitions {
399             if fs::read_dir(partition_root.join(partition)).is_err() {
400                 bail!("{partition:?} partition does not exist on host. Try rebuilding with m");
401             }
402         }
403         return Ok(partitions.clone());
404     }
405     let found_partitions: Vec<String> = DEFAULT_PARTITIONS
406         .iter()
407         .filter_map(|part| match tracked_files.iter().any(|t| PathBuf::from(t).starts_with(part)) {
408             true => Some(part.to_string()),
409             false => None,
410         })
411         .collect();
412     for partition in &found_partitions {
413         if fs::read_dir(partition_root.join(partition)).is_err() {
414             bail!("{partition:?} partition does not exist on host. Try rebuilding with m");
415         }
416     }
417 
418     Ok(found_partitions)
419 }
420 
421 #[derive(Clone, PartialEq)]
422 enum PushState {
423     Push,
424     /// File is tracked and the device and host fingerprints match.
425     UpToDate,
426     /// File is not tracked but exists on device and host.
427     TrackOrClean,
428     /// File is on the device, but not host and not tracked.
429     TrackAndBuildOrClean,
430     /// File is tracked and on host but not on device.
431     //PushNew,
432     /// File is on host, but not tracked and not on device.
433     TrackOrMakeClean,
434     /// File is tracked and on the device, but is not in the build tree.
435     /// `m` the module to build it.
436     UntrackOrBuild,
437     /// The apk was `installed` on top of the system image.  It will shadow any push
438     /// we make to the system partitions.  It should be explicitly installed or uninstalled, not pushed.
439     // TODO(rbraunstein): Store package name and path to file on disk so we can print a better
440     // message to the user.
441     ApkInstalled,
442 }
443 
444 impl PushState {
445     /// Message to print indicating what actions the user should take based on the
446     /// state of the file.
get_action_msg(self) -> String447     pub fn get_action_msg(self) -> String {
448         match self {
449 	    PushState::Push => "Ready to push:\n  (These files are out of date on the device and will be pushed when you run `adevice update`)".to_string(),
450 	    // Note: we don't print up to date files.
451 	    PushState::UpToDate => "Up to date:  (These files are up to date on the device. There is nothing to do.)".to_string(),
452 	    PushState::TrackOrClean => "Untracked pushed files:\n  (These files are not tracked but exist on the device and host.)\n  (Use `adevice track` for the appropriate module to have them pushed.)".to_string(),
453 	    PushState::TrackAndBuildOrClean => "Stale device files:\n  (These files are on the device, but not built or tracked.)\n  (They will be cleaned with `adevice update` or `adevice clean`.)".to_string(),
454 	    PushState::TrackOrMakeClean => "Untracked built files:\n  (These files are in the build tree but not tracked or on the device.)\n  (You might want to `adevice track` the module.  It is safe to do nothing.)".to_string(),
455 	    PushState::UntrackOrBuild => "Unbuilt files:\n  (These files should be built so the device can be updated.)\n  (Rebuild and `adevice update`)".to_string(),
456 	    PushState::ApkInstalled => format!("ADB Installed files:\n{RED_WARNING_LINE}  (These files were installed with `adb install` or similar.  Pushing to the system partition will not make them available.)\n  (Either `adb uninstall` these packages or `adb install` by hand.`)"),
457 	}
458     }
459 }
460 
461 // TODO(rbraunstein): Create a struct for each of the sections above for better formatting.
462 const RED_WARNING_LINE: &str = "  \x1b[1;31m!! Warning: !!\x1b[0m\n";
463 
464 /// Group each file by state and print the state message followed by the files in that state.
print_status(stdout: &mut impl Write, files: &HashMap<PathBuf, PushState>) -> Result<()>465 fn print_status(stdout: &mut impl Write, files: &HashMap<PathBuf, PushState>) -> Result<()> {
466     for state in [
467         PushState::Push,
468         // Skip UpToDate and TrackOrMakeClean, don't print those.
469         PushState::TrackOrClean,
470         PushState::TrackAndBuildOrClean,
471         PushState::UntrackOrBuild,
472         // Skip APKInstalled, it is handleded in shadow_apk_check.
473     ] {
474         print_files_in_state(stdout, files, state)?;
475     }
476     Ok(())
477 }
478 
479 /// Determine if file is an apk and decide if we need to give a warning
480 /// about pushing to a system directory because it is already installed in /data
481 /// and will shadow a system apk if we push it.
installed_apk_action( file: &Path, product_out: &Path, installed_packages: &HashSet<String>, ) -> Result<PushState>482 fn installed_apk_action(
483     file: &Path,
484     product_out: &Path,
485     installed_packages: &HashSet<String>,
486 ) -> Result<PushState> {
487     if file.extension() != Some(OsString::from("apk").as_os_str()) {
488         return Ok(PushState::Push);
489     }
490     // See if this file was installed.
491     if is_apk_installed(&product_out.join(file), installed_packages)? {
492         Ok(PushState::ApkInstalled)
493     } else {
494         Ok(PushState::Push)
495     }
496 }
497 
498 /// Determine if the given apk has been installed via `adb install`.
499 /// This will allow us to decide if pushing to /system will cause problems because the
500 /// version we push would be shadowed by the `installed` version.
501 /// Run PackageManager commands from the shell to check if something is installed.
502 /// If this is a problem, we can build something in to adevice_fingerprint that
503 /// calls PackageManager#getInstalledApplications.
504 /// adb exec-out pm list packages  -s -f
is_apk_installed(host_path: &Path, installed_packages: &HashSet<String>) -> Result<bool>505 fn is_apk_installed(host_path: &Path, installed_packages: &HashSet<String>) -> Result<bool> {
506     let host_apk_path = host_path.as_os_str().to_str().unwrap();
507     let aapt_output = std::process::Command::new("aapt2")
508         .args(["dump", "permissions", host_apk_path])
509         .output()
510         .context(format!("Running aapt2 on host to see if apk installed: {}", host_apk_path))?;
511 
512     if !aapt_output.status.success() {
513         let stderr = String::from_utf8(aapt_output.stderr)?;
514         bail!("Unable to run aapt2 to get installed packages {:?}", stderr);
515     }
516 
517     match package_from_aapt_dump_output(aapt_output.stdout) {
518         Ok(package) => {
519             debug!("AAPT dump found package: {package}");
520             Ok(installed_packages.contains(&package))
521         }
522         Err(e) => bail!("Unable to run aapt2 to get package information {e:?}"),
523     }
524 }
525 
526 static AAPT_PACKAGE_MATCHER: LazyLock<Regex> =
527     LazyLock::new(|| Regex::new(r"^package: (.+)$").expect("regex does not compile"));
528 
529 /// Filter aapt2 dump output to parse out the package name for the apk.
package_from_aapt_dump_output(stdout: Vec<u8>) -> Result<String>530 fn package_from_aapt_dump_output(stdout: Vec<u8>) -> Result<String> {
531     let package_match = String::from_utf8(stdout)?
532         .lines()
533         .filter_map(|line| AAPT_PACKAGE_MATCHER.captures(line).map(|x| x[1].to_string()))
534         .collect();
535     Ok(package_match)
536 }
537 
538 /// Go through all files that exist on the host, device, and tracking set.
539 /// Ignore any file that is in all three and has the same fingerprint on the host and device.
540 /// States where the user should take action:
541 ///   Build
542 ///   Clean
543 ///   Track
544 ///   Untrack
collect_status_per_file( tracked_set: &HashSet<PathBuf>, host_tree: &HashMap<PathBuf, FileMetadata>, device_tree: &HashMap<PathBuf, FileMetadata>, product_out: &Path, installed_packages: &HashSet<String>, diff_mode: DiffMode, ) -> Result<HashMap<PathBuf, PushState>>545 fn collect_status_per_file(
546     tracked_set: &HashSet<PathBuf>,
547     host_tree: &HashMap<PathBuf, FileMetadata>,
548     device_tree: &HashMap<PathBuf, FileMetadata>,
549     product_out: &Path,
550     installed_packages: &HashSet<String>,
551     diff_mode: DiffMode,
552 ) -> Result<HashMap<PathBuf, PushState>> {
553     let mut all_files: Vec<&PathBuf> =
554         host_tree.keys().chain(device_tree.keys()).chain(tracked_set.iter()).collect();
555     all_files.dedup();
556 
557     let states: HashMap<PathBuf, PushState> = all_files
558     .par_iter()
559     .map(|f| {
560         let on_device = device_tree.contains_key(*f);
561         let on_host = host_tree.contains_key(*f);
562         let tracked = tracked_set.contains(*f);
563 
564         // I think keeping tracked/untracked else is clearer than collapsing.
565         #[allow(clippy::collapsible_else_if)]
566         let push_state = if tracked {
567             if on_device && on_host {
568                 if fingerprint::is_metadata_diff(
569                     device_tree.get(*f).unwrap(),
570                     host_tree.get(*f).unwrap(),
571                     diff_mode,
572                 ) {
573                     // PushDiff
574                     installed_apk_action(f, product_out, installed_packages).expect("checking if apk installed")
575                 } else {
576                     // Else normal case, do nothing.
577                     // TODO(rbraunstein): Do we need to check for installed apk and warn.
578                     // 1) User updates apk
579                     // 2) User adb install
580                     // 3) User reverts code and builds
581                     //   (host and device match but installed apk shadows system version).
582                     // For now, don't look for extra problems.
583                     PushState::UpToDate
584                 }
585             } else if !on_host {
586                 // We don't care if it is on the device or not, it has to built if it isn't
587                 // on the host.
588                 PushState::UntrackOrBuild
589             } else {
590                 assert!(
591                     !on_device && on_host,
592                     "Unexpected state for file: {f:?}, tracked: {tracked} on_device: {on_device}, on_host: {on_host}"
593                 );
594                 // TODO(rbraunstein): Is it possible for an apk to be adb installed, but not in the system image?
595                 // I guess so, but seems weird.  Add check InstalledApk here too.
596                 // PushNew
597                 PushState::Push
598             }
599         } else {
600             if on_device && on_host {
601                 PushState::TrackOrClean
602             } else if on_device && !on_host {
603                 PushState::TrackAndBuildOrClean
604             } else {
605                 // Note: case of !tracked, !on_host, !on_device is not possible.
606                 // So only one case left.
607                 assert!(
608                     !on_device && on_host,
609                     "Unexpected state for file: {f:?}, tracked: {tracked} on_device: {on_device}, on_host: {on_host}"
610                 );
611                 PushState::TrackOrMakeClean
612             }
613         };
614         (PathBuf::from(f), push_state)
615     })
616     .collect();
617     Ok(states)
618 }
619 
620 /// Find all files in a given state, and if that file list is not empty, print the
621 /// state message and all the files (sorted).
622 /// Only prints stages that files in that stage.
print_files_in_state( stdout: &mut impl Write, files: &HashMap<PathBuf, PushState>, push_state: PushState, ) -> Result<()>623 fn print_files_in_state(
624     stdout: &mut impl Write,
625     files: &HashMap<PathBuf, PushState>,
626     push_state: PushState,
627 ) -> Result<()> {
628     let filtered_files: HashMap<&PathBuf, &PushState> =
629         files.iter().filter(|(_, state)| *state == &push_state).collect();
630 
631     if filtered_files.is_empty() {
632         return Ok(());
633     }
634     writeln!(stdout, "{}", &push_state.get_action_msg())?;
635     let file_list_output = filtered_files
636         .keys()
637         .sorted()
638         .map(|path| format!("\t{}", path.display()))
639         .collect::<Vec<String>>()
640         .join("\n");
641     writeln!(stdout, "{}", file_list_output)?;
642     Ok(())
643 }
644 
get_product_out_from_env() -> Option<PathBuf>645 fn get_product_out_from_env() -> Option<PathBuf> {
646     match std::env::var("ANDROID_PRODUCT_OUT") {
647         Ok(x) if !x.is_empty() => Some(PathBuf::from(x)),
648         _ => None,
649     }
650 }
651 
652 /// Prints uninstall commands for every package installed
653 /// Bails if there are any installed packages.
shadow_apk_check(stdout: &mut impl Write, files: &HashMap<PathBuf, PushState>) -> Result<()>654 fn shadow_apk_check(stdout: &mut impl Write, files: &HashMap<PathBuf, PushState>) -> Result<()> {
655     let filtered_files: HashMap<&PathBuf, &PushState> =
656         files.iter().filter(|(_, state)| *state == &PushState::ApkInstalled).collect();
657 
658     if filtered_files.is_empty() {
659         return Ok(());
660     }
661 
662     writeln!(stdout, "{}", PushState::ApkInstalled.get_action_msg())?;
663     let file_list_output = filtered_files
664         .keys()
665         .sorted()
666         .map(|path| format!("adb uninstall {};", path.display()))
667         .collect::<Vec<String>>()
668         .join("\n");
669     writeln!(stdout, "{}", file_list_output)?;
670     bail!("{} shadowing apks found. Uninstall to continue.", filtered_files.keys().len());
671 }
672 
673 /// Return all path components of file_path up to a passed partition.
674 /// Given system/bin/logd and partition "system",
675 /// return ["system/bin/logd", "system/bin"], not "system" or ""
676 
parents(file_path: &str, partitions: &[PathBuf]) -> Vec<PathBuf>677 fn parents(file_path: &str, partitions: &[PathBuf]) -> Vec<PathBuf> {
678     PathBuf::from(file_path)
679         .ancestors()
680         .map(|p| p.to_path_buf())
681         .take_while(|p| !partitions.contains(p))
682         .collect()
683 }
684 
685 #[allow(missing_docs)]
686 #[derive(Default)]
687 pub struct Profiler {
688     pub device_fingerprint: Duration,
689     pub host_fingerprint: Duration,
690     pub ninja_deps_computer: Duration,
691     /// Time to run all the "adb push" or "adb rm" commands.
692     pub adb_cmds: Duration,
693     /// Time to run "adb reboot" or "exec-out start".
694     pub restart: Duration,
695     pub restart_type: String,
696     /// Time for device to respond to "wait-for-device".
697     pub wait_for_device: Duration,
698     /// Time for sys.boot_completed to be 1 after wait-for-device.
699     pub wait_for_boot_completed: Duration,
700     /// The first time after a userdebug build is flashed/created, we need
701     /// to mount rw and reboot.
702     pub first_remount_rw: Duration,
703     pub total: Duration,
704 }
705 
706 impl std::fmt::Display for Profiler {
fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result707     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
708         write!(
709             f,
710             "{}",
711             [
712                 " Operation profile: (secs)".to_string(),
713                 format!("Device Fingerprint - {}", self.device_fingerprint.as_secs()),
714                 format!("Host fingerprint - {}", self.host_fingerprint.as_secs()),
715                 format!("Ninja - {}", self.ninja_deps_computer.as_secs()),
716                 format!("Adb Cmds - {}", self.adb_cmds.as_secs()),
717                 format!("Restart({})- {}", self.restart_type, self.restart.as_secs()),
718                 format!("Wait For device connected - {}", self.wait_for_device.as_secs()),
719                 format!("Wait For boot completed - {}", self.wait_for_boot_completed.as_secs()),
720                 format!("First remount RW - {}", self.first_remount_rw.as_secs()),
721                 format!("TOTAL - {}", self.total.as_secs()),
722             ]
723             .join("\n\t")
724         )
725     }
726 }
727 
728 #[cfg(test)]
729 mod tests {
730     use super::*;
731     use crate::fingerprint::{self, DiffMode};
732     use std::path::PathBuf;
733     use tempfile::TempDir;
734 
735     // TODO(rbraunstein): Capture/test stdout and logging.
736     //  Test stdout: https://users.rust-lang.org/t/how-to-test-functions-that-use-println/67188/5
737     #[test]
empty_inputs() -> Result<()>738     fn empty_inputs() -> Result<()> {
739         let device_files: HashMap<PathBuf, FileMetadata> = HashMap::new();
740         let host_files: HashMap<PathBuf, FileMetadata> = HashMap::new();
741         let ninja_deps: Vec<String> = vec![];
742         let product_out = PathBuf::from("");
743         let installed_apks = HashSet::<String>::new();
744         let partitions = Vec::new();
745         let force = false;
746         let mut stdout = Vec::new();
747 
748         let results = get_update_commands(
749             &device_files,
750             &host_files,
751             &ninja_deps,
752             product_out,
753             &installed_apks,
754             DiffMode::UsePermissions,
755             &partitions,
756             force,
757             &mut stdout,
758         )?;
759         assert_eq!(results.upserts.values().len(), 0);
760         Ok(())
761     }
762 
763     #[test]
host_and_ninja_file_not_on_device() -> Result<()>764     fn host_and_ninja_file_not_on_device() -> Result<()> {
765         // Relative to product out?
766         let product_out = PathBuf::from("");
767         let installed_apks = HashSet::<String>::new();
768         let partitions = Vec::new();
769         let mut stdout = Vec::new();
770         let force = true;
771 
772         let results = get_update_commands(
773             // Device files
774             &HashMap::new(),
775             // Host files
776             &HashMap::from([
777                 (PathBuf::from("system/myfile"), file_metadata("digest1")),
778                 (PathBuf::from("system"), dir_metadata()),
779             ]),
780             // Ninja deps
781             &["system".to_string(), "system/myfile".to_string()],
782             product_out,
783             &installed_apks,
784             DiffMode::UsePermissions,
785             &partitions,
786             force,
787             &mut stdout,
788         )?;
789         assert_eq!(results.upserts.values().len(), 2);
790         Ok(())
791     }
792 
793     #[test]
host_and_ninja_file_not_on_device_force_false() -> Result<()>794     fn host_and_ninja_file_not_on_device_force_false() -> Result<()> {
795         let product_out = PathBuf::from("");
796         let installed_apks = HashSet::<String>::new();
797         let partitions = Vec::new();
798         let mut stdout = Vec::new();
799         let force = false;
800 
801         let results = get_update_commands(
802             // Device files
803             &HashMap::new(),
804             // Host files
805             &HashMap::from([
806                 (PathBuf::from("system/myfile"), file_metadata("digest1")),
807                 (PathBuf::from("system"), dir_metadata()),
808             ]),
809             // Ninja deps
810             &["system".to_string(), "system/myfile".to_string()],
811             product_out,
812             &installed_apks,
813             DiffMode::UsePermissions,
814             &partitions,
815             force,
816             &mut stdout,
817         );
818         assert!(results.is_err());
819         if let Err(e) = results {
820             assert!(e
821                 .to_string()
822                 .contains("ERROR: Please build the above modules before updating."));
823         }
824         Ok(())
825     }
826 
827     #[test]
test_shadow_apk_check_no_shadowing_apks() -> Result<()>828     fn test_shadow_apk_check_no_shadowing_apks() -> Result<()> {
829         let mut output = Vec::new();
830         let files = &HashMap::from([(PathBuf::from("/system/app1.apk"), PushState::Push)]);
831         let result = shadow_apk_check(&mut output, files);
832 
833         assert!(result.is_ok());
834         assert!(output.is_empty());
835         Ok(())
836     }
837 
838     #[test]
test_shadow_apk_check_with_shadowing_apks() -> Result<()>839     fn test_shadow_apk_check_with_shadowing_apks() -> Result<()> {
840         let mut output = Vec::new();
841         let files = &HashMap::from([
842             (PathBuf::from("/system/app1.apk"), PushState::Push),
843             (PathBuf::from("/data/app2.apk"), PushState::ApkInstalled),
844             (PathBuf::from("/data/app3.apk"), PushState::ApkInstalled),
845         ]);
846         let result = shadow_apk_check(&mut output, files);
847         assert!(result.is_err());
848         let output_str = String::from_utf8(output).unwrap();
849         assert!(
850             output_str.contains("Either `adb uninstall` these packages or `adb install` by hand.")
851         );
852         assert!(output_str.contains("adb uninstall /data/app2.apk;"));
853         assert!(output_str.contains("adb uninstall /data/app3.apk;"));
854         Ok(())
855     }
856 
857     #[test]
on_host_not_in_tracked_on_device() -> Result<()>858     fn on_host_not_in_tracked_on_device() -> Result<()> {
859         let results = call_update(&FakeState {
860             device_data: &["system/f1"],
861             host_data: &["system/f1"],
862             tracked_set: &[],
863         })?
864         .upserts;
865         assert_eq!(0, results.values().len());
866         Ok(())
867     }
868 
869     #[test]
in_host_not_in_tracked_not_on_device() -> Result<()>870     fn in_host_not_in_tracked_not_on_device() -> Result<()> {
871         let results = call_update(&FakeState {
872             device_data: &[""],
873             host_data: &["system/f1"],
874             tracked_set: &[],
875         })?
876         .upserts;
877         assert_eq!(0, results.values().len());
878         Ok(())
879     }
880 
881     #[test]
test_parents_stops_at_partition()882     fn test_parents_stops_at_partition() {
883         assert_eq!(
884             vec![
885                 PathBuf::from("some/long/path/file"),
886                 PathBuf::from("some/long/path"),
887                 PathBuf::from("some/long"),
888             ],
889             parents("some/long/path/file", &[PathBuf::from("some")]),
890         );
891     }
892 
893     #[test]
validate_partition_removes_unused_default_partition() -> Result<()>894     fn validate_partition_removes_unused_default_partition() -> Result<()> {
895         let tmp_root = TempDir::new().unwrap();
896         fs::create_dir_all(tmp_root.path().join("system")).unwrap();
897 
898         // No system_ext here, so remove from default partitions
899         let ninja_deps = vec![
900             "system/file1".to_string(),
901             "file3".to_string(),
902             "system/dir2/file1".to_string(),
903             "data/sys/file4".to_string(),
904         ];
905         assert_eq!(
906             vec!["system".to_string(),],
907             validate_partitions(tmp_root.path(), &ninja_deps, &None)?
908         );
909         Ok(())
910     }
911 
912     #[test]
validate_partition_bails_on_bad_partition_name()913     fn validate_partition_bails_on_bad_partition_name() {
914         let tmp_root = TempDir::new().unwrap();
915         fs::create_dir_all(tmp_root.path().join("system")).unwrap();
916         fs::create_dir_all(tmp_root.path().join("sys")).unwrap();
917 
918         let ninja_deps = vec![
919             "system/file1".to_string(),
920             "file3".to_string(),
921             "system/dir2/file1".to_string(),
922             "data/sys/file4".to_string(),
923         ];
924         // "sys" isn't a valid partition name, but it matches a prefix of "system".
925         // Should bail.
926         match validate_partitions(tmp_root.path(), &ninja_deps, &Some(vec!["sys".to_string()])) {
927             Ok(_) => panic!("Expected error"),
928             Err(e) => {
929                 assert!(
930                     e.to_string().contains("\"sys\" is not a valid partition"),
931                     "{}",
932                     e.to_string()
933                 )
934             }
935         }
936     }
937 
938     #[test]
validate_partition_bails_on_no_partition_on_host()939     fn validate_partition_bails_on_no_partition_on_host() {
940         let tmp_root = TempDir::new().unwrap();
941 
942         let ninja_deps = vec!["system/file1".to_string()];
943         match validate_partitions(tmp_root.path(), &ninja_deps, &Some(vec!["system".to_string()])) {
944             Ok(_) => panic!("Expected error"),
945             Err(e) => {
946                 assert!(
947                     e.to_string().contains("\"system\" partition does not exist on host"),
948                     "{}",
949                     e.to_string()
950                 )
951             }
952         }
953     }
954 
955     // TODO(rbraunstein): Test case where on device and up to date, but not tracked.
956 
957     struct FakeState {
958         device_data: &'static [&'static str],
959         host_data: &'static [&'static str],
960         tracked_set: &'static [&'static str],
961     }
962 
963     // Helper to call update.
964     // Uses filename for the digest in the fingerprint
965     // Add directories for every file on the host like walkdir would do.
966     // `update` adds the directories for the tracked set so we don't do that here.
call_update(fake_state: &FakeState) -> Result<commands::Commands>967     fn call_update(fake_state: &FakeState) -> Result<commands::Commands> {
968         let product_out = PathBuf::from("");
969         let installed_apks = HashSet::<String>::new();
970         let partitions = Vec::new();
971         let force = false;
972         let mut device_files: HashMap<PathBuf, FileMetadata> = HashMap::new();
973         let mut host_files: HashMap<PathBuf, FileMetadata> = HashMap::new();
974         for d in fake_state.device_data {
975             // Set the digest to the filename for now.
976             device_files.insert(PathBuf::from(d), file_metadata(d));
977         }
978         for h in fake_state.host_data {
979             // Set the digest to the filename for now.
980             host_files.insert(PathBuf::from(h), file_metadata(h));
981             // Add the dir too.
982         }
983 
984         let tracked_set: Vec<String> =
985             fake_state.tracked_set.iter().map(|s| s.to_string()).collect();
986 
987         let mut stdout = Vec::new();
988         get_update_commands(
989             &device_files,
990             &host_files,
991             &tracked_set,
992             product_out,
993             &installed_apks,
994             DiffMode::UsePermissions,
995             &partitions,
996             force,
997             &mut stdout,
998         )
999     }
1000 
file_metadata(digest: &str) -> FileMetadata1001     fn file_metadata(digest: &str) -> FileMetadata {
1002         FileMetadata {
1003             file_type: fingerprint::FileType::File,
1004             digest: digest.to_string(),
1005             ..Default::default()
1006         }
1007     }
1008 
dir_metadata() -> FileMetadata1009     fn dir_metadata() -> FileMetadata {
1010         FileMetadata { file_type: fingerprint::FileType::Directory, ..Default::default() }
1011     }
1012     // TODO(rbraunstein): Add tests for collect_status_per_file after we decide on output.
1013 }
1014