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