| #!/usr/bin/env python3 |
| # |
| # Copyright (C) 2025 The Android Open Source Project |
| # |
| # Licensed under the Apache License, Version 2.0 (the "License"); |
| # you may not use this file except in compliance with the License. |
| # You may obtain a copy of the License at |
| # |
| # http://www.apache.org/licenses/LICENSE-2.0 |
| # |
| # Unless required by applicable law or agreed to in writing, software |
| # distributed under the License is distributed on an "AS IS" BASIS, |
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| # See the License for the specific language governing permissions and |
| # limitations under the License. |
| |
| """Automates boot image profile generation for Android Automotive. |
| |
| This script orchestrates the process of generating a boot image profile |
| by performing the following main stages: |
| |
| 1. Configuring a device using art/tools/boot-image-profile-configure-device.sh |
| to generate a `boot.zip` file. |
| 2. Executing a simple set of activities (Maps, Play Store) on the Android |
| device via ADB shell commands to generate usage profiles. |
| Note: This "Starting Maps and Play activities" is a simple CUJ |
| implementation as a starting point. It may need to be expanded or customized |
| for better profiling by reflecting real usage. |
| 3. Generating boot image profiles based on the generated `boot.zip` file |
| and the collected device profiles, using |
| art/tools/boot-image-profile-generate.sh. |
| |
| The script requires the ANDROID_BUILD_TOP environment variable to be set. |
| |
| Example Usage: |
| $ source build/envsetup.sh # Or equivalent setup |
| $ python3 art/tools/boot-image-profile-aaos-sample-generate.py |
| |
| Example Output Trace: |
| --- Stage 1: Configuring Device --- |
| --- Stage 2: Starting ADB/CUJ Sequence --- |
| --- Stage 3: Running Generation Script --- |
| --- All stages completed successfully! --- |
| """ |
| |
| import os |
| import shlex |
| import subprocess |
| import sys |
| import time |
| from typing import List, Optional |
| |
| |
| # --- Helper Functions --- |
| def print_message(message: str) -> None: |
| """Prints a message to stderr.""" |
| print(f"{message}", file=sys.stderr) |
| |
| |
| def print_separator() -> None: |
| """Prints a separator line.""" |
| print_message("---------------------------------------\n") |
| |
| |
| # --- End Helper Functions --- |
| |
| # --- Configuration --- |
| # Pattern to look for in logcat to indicate the device is ready for profiling. |
| # Set to None to skip the logcat check and only wait for ADB device. |
| # LOGCAT_READY_PATTERN: Optional[str] = ( |
| # "Displayed com.android.car.carlauncher/.CarLauncher" |
| # ) |
| LOGCAT_READY_PATTERN = None # Example: Disable logcat check |
| |
| # Logcat filters to apply when monitoring for the ready pattern. |
| LOGCAT_FILTER_SPECS: List[str] = [ |
| "ActivityTaskManager:I", |
| "*:S", |
| ] |
| |
| # Ensure ANDROID_BUILD_TOP is set and resolve its absolute path. |
| ANDROID_BUILD_TOP: str = os.getenv("ANDROID_BUILD_TOP", "") |
| if not ANDROID_BUILD_TOP: |
| print_message( |
| "Error: ANDROID_BUILD_TOP environment variable is not set. " |
| "Please run `source build/envsetup.sh` and select a target." |
| ) |
| sys.exit(1) |
| ANDROID_BUILD_TOP = os.path.abspath(ANDROID_BUILD_TOP) |
| print(f"--- Using ANDROID_BUILD_TOP: {ANDROID_BUILD_TOP} ---") |
| |
| # Define paths to required scripts and generated files. |
| _SCRIPT_DIR = "art/tools" # Relative path to scripts within build top |
| _FRAMEWORKS_CONFIG_DIR = "frameworks/base/config" # Relative path to denylist |
| |
| SCRIPT1_PATH: str = os.path.join( |
| ANDROID_BUILD_TOP, _SCRIPT_DIR, "boot-image-profile-configure-device.sh" |
| ) |
| SCRIPT1_ARG: str = "boot.zip" |
| |
| SCRIPT2_PATH: str = os.path.join( |
| ANDROID_BUILD_TOP, _SCRIPT_DIR, "boot-image-profile-generate.sh" |
| ) |
| BOOT_ZIP_ARG: str = os.path.join(ANDROID_BUILD_TOP, "boot.zip") |
| DENYLIST_ARG: str = os.path.join( |
| ANDROID_BUILD_TOP, _FRAMEWORKS_CONFIG_DIR, "preloaded-classes-denylist" |
| ) |
| MAPS_PROF_ARG: str = os.path.join(ANDROID_BUILD_TOP, "maps.prof") |
| VENDING_PROF_ARG: str = os.path.join(ANDROID_BUILD_TOP, "vending.prof") |
| |
| # Arguments for the generation script (SCRIPT2). |
| SCRIPT2_ARGS_LIST: List[str] = [ |
| ANDROID_BUILD_TOP, |
| BOOT_ZIP_ARG, |
| DENYLIST_ARG, |
| MAPS_PROF_ARG, |
| VENDING_PROF_ARG, |
| "--profman-arg", |
| "--upgrade-startup-to-hot=false", |
| ] |
| |
| # Package names and activities for the CUJ steps. |
| MAPS_PACKAGE: str = ( |
| "com.google.android.apps.maps/com.google.android.maps.MapsActivity" |
| ) |
| VENDING_PACKAGE: str = ( |
| "com.android.vending/com.google.android.finsky.activities.MainActivity" |
| ) |
| |
| # Duration to sleep after a CUJ activity is launched, to allow it to run |
| # before taking a snapshot of the profile. |
| CUJ_ACTIVITY_SLEEP_SECONDS: int = 15 |
| |
| # Duration to sleep after starting logcat, to allow it to initialize. |
| LOGCAT_START_SLEEP_SECONDS: float = 0.5 |
| |
| # --- End Configuration --- |
| |
| |
| def run_generic_command( |
| command_list: List[str], |
| check: bool = True, |
| capture: bool = True, |
| cmd_timeout: Optional[int | float] = None, |
| **kwargs, |
| ) -> Optional[subprocess.CompletedProcess]: |
| """Runs a command, handling errors and timeouts. |
| |
| Args: |
| command_list: A list of strings representing the command and its |
| arguments. |
| check: If True, raise CalledProcessError on non-zero exit code. If False, |
| return the CompletedProcess object regardless of exit code. |
| capture: If True, capture stdout/stderr. If False, let them pass through. |
| cmd_timeout: Timeout in seconds for the command. |
| **kwargs: Additional arguments to pass to subprocess.run. |
| |
| Returns: |
| A CompletedProcess object or None on error. |
| |
| Raises: |
| subprocess.CalledProcessError: If check is True and the command fails. |
| subprocess.TimeoutExpired: If the command times out and check is True. |
| """ |
| command_str = " ".join(shlex.quote(part) for part in command_list) |
| print_message(f"--- Running command: {command_str} ---") |
| try: |
| result = subprocess.run( |
| command_list, |
| check=check, |
| stdout=subprocess.PIPE if capture else None, |
| stderr=subprocess.PIPE if capture else None, |
| text=True, # Use text mode for string I/O |
| encoding="utf-8", # Explicitly set encoding |
| errors="replace", # Replace uninterpretable characters |
| timeout=cmd_timeout, |
| **kwargs, |
| ) |
| # If check=True, subprocess.run already raises on error. |
| # If check=False, we just return the result. |
| return result |
| except FileNotFoundError: |
| print_message( |
| f"Error: Command not found: '{command_list[0]}'. Is it in your PATH?" |
| ) |
| except subprocess.TimeoutExpired: |
| print_message( |
| f"Error: Command '{command_str}' timed out after {cmd_timeout} seconds." |
| ) |
| if check: |
| raise |
| except subprocess.CalledProcessError as e: |
| print_message( |
| f"Error: Command '{command_str}' failed with exit code {e.returncode}." |
| ) |
| if e.stdout: |
| print_message(f"--- stdout ---\n{e.stdout.strip()}") |
| if e.stderr: |
| print_message(f"--- stderr ---\n{e.stderr.strip()}") |
| if check: |
| raise |
| except Exception as e: |
| print_message(f"Unexpected Python error running {command_str}: {e}") |
| return None # Return None on any caught exception |
| |
| |
| def run_shell_script( |
| script_path: str, *script_args: str, interpreter: str = "bash" |
| ) -> bool: |
| """Runs a shell script using the specified interpreter. |
| |
| Args: |
| script_path: The path to the shell script. |
| *script_args: Positional arguments to pass to the script. |
| interpreter: The shell interpreter to use (e.g., "bash", "sh"). |
| |
| Returns: |
| True if the script exited with code 0, False otherwise or if an |
| error occurred running the command. |
| """ |
| command = [interpreter, script_path] + list(script_args) |
| print_message(f"--- Running script: {script_path} ---") |
| # Run with check=False so we can inspect the return code manually |
| result = run_generic_command(command, check=False, capture=True) |
| |
| if result is None: |
| # An exception occurred in run_generic_command |
| print_message(f"--- Script {script_path} command execution failed ---") |
| print_separator() |
| return False |
| |
| # Output captured stdout/stderr for user information |
| if result.stdout: |
| print_message(f"--- stdout ---\n{result.stdout.strip()}") |
| if result.stderr: |
| print_message(f"--- stderr ---\n{result.stderr.strip()}") |
| |
| if result.returncode == 0: |
| print_message(f"--- Script {script_path} SUCCESS ---") |
| print_separator() |
| return True |
| print_message( |
| f"--- Script {script_path} FAILED with exit code {result.returncode} ---" |
| ) |
| print_separator() |
| return False |
| |
| |
| def wait_for_device_ready( |
| timeout_seconds: int = 360, |
| check_interval: int = 1, |
| logcat_pattern: Optional[str] = LOGCAT_READY_PATTERN, |
| logcat_filters: List[str] = LOGCAT_FILTER_SPECS, |
| ) -> bool: |
| """Waits for ADB connection and optionally a specific logcat pattern. |
| |
| Args: |
| timeout_seconds: Maximum time in seconds to wait. |
| check_interval: Time in seconds to wait between logcat reads. |
| logcat_pattern: The string or regex pattern to look for in logcat. If |
| None, only waits for ADB device connection. |
| logcat_filters: List of logcat filter specifications (e.g., "TAG:LEVEL"). |
| |
| Returns: |
| True if the device becomes ready within the timeout, False otherwise. |
| """ |
| print_message(f"--- Waiting for ADB device (max {timeout_seconds}s) ---") |
| start_time = time.time() |
| |
| print_message("[Wait Step 1/2] Waiting for ADB connection...") |
| adb_wait_cmd = ["adb", "wait-for-device"] |
| try: |
| # Using run here is simpler as wait-for-device holds until ready or timeout |
| subprocess.run( |
| adb_wait_cmd, |
| timeout=timeout_seconds, |
| capture_output=True, |
| text=True, |
| check=True, # check=True is appropriate here |
| encoding="utf-8", |
| errors="replace", |
| ) |
| elapsed_time = time.time() - start_time |
| print_message( |
| "[Wait Step 1/2] Complete: Device connected. (Elapsed:" |
| f" {elapsed_time:.1f}s)" |
| ) |
| except ( |
| subprocess.TimeoutExpired, |
| subprocess.CalledProcessError, |
| FileNotFoundError, |
| ) as e: |
| print_message(f"ERROR: Initial ADB connection failed: {e}") |
| print_message("Is device connected? Is adb in PATH?") |
| print_separator() |
| return False |
| except Exception as e: |
| print_message(f"ERROR during 'adb wait-for-device': {e}") |
| print_separator() |
| return False |
| |
| if not logcat_pattern: |
| print_message( |
| "[Wait Step 2/2] Skipping logcat pattern check" |
| " (LOGCAT_READY_PATTERN is None)." |
| ) |
| print_message("--- Device ready based on ADB connection ---") |
| print_separator() |
| return True |
| |
| logcat_proc: Optional[subprocess.Popen] = None |
| logcat_found: bool = False |
| logcat_failed: bool = False |
| |
| print_message( |
| "\n[Wait Step 2/2] Starting logcat check for pattern" |
| f" '{logcat_pattern}'..." |
| ) |
| |
| try: |
| print_message(" Clearing logcat buffer...") |
| run_generic_command(["adb", "logcat", "-c"], check=False) |
| |
| logcat_cmd = ["adb", "logcat", "-v", "brief"] + logcat_filters |
| print_message( |
| f" Starting logcat: {' '.join(shlex.quote(p) for p in logcat_cmd)}" |
| ) |
| try: |
| # Use Popen to read line by line |
| logcat_proc = subprocess.Popen( |
| logcat_cmd, |
| stdout=subprocess.PIPE, |
| stderr=subprocess.PIPE, |
| text=True, |
| encoding="utf-8", |
| errors="replace", |
| ) |
| time.sleep(LOGCAT_START_SLEEP_SECONDS) |
| except (FileNotFoundError, Exception) as proc_err: |
| print_message(f"ERROR: Failed to start logcat process: {proc_err}") |
| logcat_failed = True |
| |
| if not logcat_failed: |
| while time.time() - start_time < timeout_seconds: |
| # Check if logcat process died unexpectedly |
| if logcat_proc and logcat_proc.poll() is not None: |
| print_message( |
| " Error: logcat process terminated unexpectedly (Code:" |
| f" {logcat_proc.returncode})." |
| f" Stderr:\n{logcat_proc.stderr.read()}" |
| ) |
| logcat_failed = True |
| break |
| |
| # Attempt to read line. readline() can block, but the outer |
| # timeout loop will eventually catch it. |
| try: |
| # Check if stdout is not None before reading |
| if logcat_proc and logcat_proc.stdout: |
| line = logcat_proc.stdout.readline() |
| if not line: |
| # If readline returns empty string, process might be exiting |
| # or stream is just slow. Check poll() again or wait. |
| if logcat_proc.poll() is not None: |
| print_message(" logcat stream ended.") |
| logcat_failed = True |
| break |
| # Wait a bit if no line was read but process is still running |
| time.sleep(check_interval) |
| continue |
| |
| line = line.strip() |
| if not line: |
| continue # Skip empty lines |
| |
| if logcat_pattern in line: |
| print_message(f' Found target logcat line: "{line}"') |
| print_message( |
| "[Wait Step 2/2] SUCCESS: Found logcat pattern. (Elapsed:" |
| f" {time.time() - start_time:.1f}s)" |
| ) |
| logcat_found = True |
| break |
| |
| except Exception as read_err: |
| print_message(f" Warning: Error reading logcat stream: {read_err}.") |
| # Continue the loop after a short pause |
| time.sleep(check_interval) |
| |
| finally: |
| # Ensure the logcat subprocess is terminated |
| if logcat_proc and logcat_proc.poll() is None: |
| print_message(" Terminating logcat process...") |
| try: |
| # Give it a moment to terminate gracefully |
| logcat_proc.terminate() |
| logcat_proc.wait(timeout=2) |
| except subprocess.TimeoutExpired: |
| # If it doesn't terminate, kill it |
| print_message(" Logcat process did not terminate, killing...") |
| logcat_proc.kill() |
| except Exception as term_err: |
| # Catch any other errors during termination |
| print_message(f" Error during logcat termination: {term_err}") |
| |
| if logcat_found: |
| print_message("--- Device ready based on logcat pattern ---") |
| print_separator() |
| return True |
| else: |
| print_message( |
| f"--- Logcat pattern '{logcat_pattern}' not found within timeout or" |
| " logcat failed. ---" |
| ) |
| print_separator() |
| return False |
| |
| |
| def execute_adb_step( |
| cmd_list: List[str], |
| step_num: int, |
| description: str, |
| allow_fail: bool = False, |
| ) -> bool: |
| """Executes a single ADB shell command step. |
| |
| Args: |
| cmd_list: The list representing the ADB command and its arguments. |
| step_num: The sequential number of this step. |
| description: A brief description of the step. |
| allow_fail: If True, the function returns True even if the command fails |
| (non-zero exit code). If False, it returns False on failure. |
| |
| Returns: |
| True if the step completed successfully or allow_fail is True and |
| the command ran (even if it failed). False if the command execution |
| itself failed (e.g., command not found, timeout) or if allow_fail |
| is False and the command returned a non-zero exit code. |
| """ |
| print_message(f"\n--- ADB Step {step_num}: {description} ---") |
| result = run_generic_command(cmd_list, check=False, capture=True) |
| |
| if result is None: |
| print_message( |
| f"ERROR: Step {step_num} ({description}) command execution failed." |
| ) |
| print_separator() |
| return False |
| |
| # Decode stdout and stderr if they exist |
| cmd_stdout = result.stdout.strip() if result.stdout else "" |
| cmd_stderr = result.stderr.strip() if result.stderr else "" |
| |
| if result.returncode != 0: |
| error_message_base = ( |
| f"Step {step_num} ({description}) failed with exit code" |
| f" {result.returncode}." |
| ) |
| if cmd_stdout: |
| print_message(f"Stdout from failed command:\n{cmd_stdout}") |
| if cmd_stderr: |
| print_message(f"Stderr from failed command:\n{cmd_stderr}") |
| if allow_fail: |
| print_message( |
| f"Warning: {error_message_base} Proceeding as allow_fail is True.", |
| ) |
| print_separator() |
| return True # Command ran, allowed to fail |
| else: |
| print_message(f"ERROR: {error_message_base} Stopping ADB sequence.") |
| print_separator() |
| return False # Command ran, not allowed to fail |
| |
| # Command succeeded (returncode == 0) |
| print_separator() |
| return True |
| |
| |
| def execute_adb_step_or_exit( |
| cmd_list: List[str], |
| step_num: int, |
| description: str, |
| allow_fail: bool = False, |
| ) -> None: |
| """Executes an ADB step and exits if it fails (unless allow_fail is True). |
| |
| Args: |
| cmd_list: The list representing the ADB command and its arguments. |
| step_num: The sequential number of this step. |
| description: A brief description of the step. |
| allow_fail: If True, the function does not exit on failure, just prints a |
| warning. |
| """ |
| if not execute_adb_step(cmd_list, step_num, description, allow_fail): |
| print_message( |
| f"ERROR: ADB Step {step_num} ({description}) failed. Aborting." |
| ) |
| sys.exit(1) |
| |
| |
| def main() -> None: |
| """Main function to orchestrate the profile generation process.""" |
| print_message("-" * 20) # Separator before start |
| print_message(f"Starting script from directory: {os.getcwd()}") |
| print_message(f"Script 1: {SCRIPT1_PATH}") |
| print_message(f"Script 2: {SCRIPT2_PATH}") |
| print_message( |
| f"Wait method: Logcat pattern ('{LOGCAT_READY_PATTERN}')" |
| if LOGCAT_READY_PATTERN |
| else "Wait method: Only ADB connection (Logcat pattern disabled)" |
| ) |
| print_message("-" * 20) # Separator after info |
| |
| print_message("\n--- Stage 1: Configuring Device ---") |
| print_message(f"\n--- Running Configuration Script ---") |
| script1_abs_path = os.path.join(ANDROID_BUILD_TOP, SCRIPT1_PATH) |
| if not os.path.exists(script1_abs_path): |
| print_message( |
| f"Error: Configuration script not found at {script1_abs_path}" |
| ) |
| sys.exit(1) |
| if not run_shell_script(script1_abs_path, SCRIPT1_ARG): |
| print_message("Configuration Script failed. Aborting.") |
| sys.exit(1) |
| print_message("Configuration Script successful.") |
| print_message("\n--- Waiting for Device Readiness ---") |
| if not wait_for_device_ready(): |
| print_message("Device not ready within timeout. Aborting.") |
| sys.exit(1) |
| print_message("Device reported ready.") |
| |
| print_message("\n--- Stage 2: Starting ADB/CUJ Sequence ---") |
| execute_adb_step_or_exit( |
| [ |
| "adb", |
| "shell", |
| "find /data/misc/profiles -name '*.prof' -exec truncate -s 0 {} \\;", |
| ], |
| 1, |
| "Clear existing profiles", |
| allow_fail=True, # Allowed to fail if dir doesn't exist etc. |
| ) |
| execute_adb_step_or_exit( |
| ["adb", "shell", "am", "get-current-user"], 2, "Get current user" |
| ) |
| execute_adb_step_or_exit( |
| [ |
| "adb", |
| "shell", |
| "am", |
| "start", |
| "-S", |
| "-W", |
| "-a", |
| "android.intent.action.MAIN", |
| "-n", |
| MAPS_PACKAGE, |
| ], |
| 3, |
| f"Start Maps activity ({MAPS_PACKAGE})", |
| ) |
| print_message( |
| f"Pausing {CUJ_ACTIVITY_SLEEP_SECONDS} seconds after Maps activity" |
| " launch..." |
| ) |
| time.sleep(CUJ_ACTIVITY_SLEEP_SECONDS) |
| execute_adb_step_or_exit( |
| ["adb", "shell", "cmd", "package", "snapshot-profile", "android"], |
| 4, |
| "Snapshot profile after Maps", |
| ) |
| execute_adb_step_or_exit( |
| ["adb", "pull", "/data/misc/profman/android.prof", MAPS_PROF_ARG], |
| 5, |
| f"Pull profile to {MAPS_PROF_ARG}", |
| ) |
| execute_adb_step_or_exit( |
| ["adb", "shell", "am", "start", "-S", "-W", "-n", VENDING_PACKAGE], |
| 6, |
| f"Start Vending activity ({VENDING_PACKAGE})", |
| ) |
| print_message( |
| f"Pausing {CUJ_ACTIVITY_SLEEP_SECONDS} seconds after Vending activity" |
| " launch..." |
| ) |
| time.sleep(CUJ_ACTIVITY_SLEEP_SECONDS) |
| execute_adb_step_or_exit( |
| ["adb", "shell", "cmd", "package", "snapshot-profile", "android"], |
| 7, |
| "Snapshot profile after Vending", |
| ) |
| execute_adb_step_or_exit( |
| ["adb", "pull", "/data/misc/profman/android.prof", VENDING_PROF_ARG], |
| 8, |
| f"Pull profile to {VENDING_PROF_ARG}", |
| ) |
| |
| print_message("\n--- Stage 3: Running Generation Script ---") |
| print_message( |
| "ADB/CUJ sequence finished (check logs for warnings). Proceeding." |
| ) |
| script2_abs_path = os.path.join(ANDROID_BUILD_TOP, SCRIPT2_PATH) |
| if not os.path.exists(script2_abs_path): |
| print_message(f"Error: Generation script not found at {script2_abs_path}") |
| sys.exit(1) |
| if run_shell_script(script2_abs_path, *SCRIPT2_ARGS_LIST): |
| print_message("\n--- All stages completed successfully! ---") |
| sys.exit(0) |
| else: |
| print_message(f"\n--- Generation Script ({SCRIPT2_PATH}) failed. ---") |
| sys.exit(1) |
| |
| |
| if __name__ == "__main__": |
| main() |