tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

apk_operations.py (92724B)


      1 #!/usr/bin/env vpython3
      2 # Copyright 2017 The Chromium Authors
      3 # Use of this source code is governed by a BSD-style license that can be
      4 # found in the LICENSE file.
      5 
      6 # Using colorama.Fore/Back/Style members
      7 # pylint: disable=no-member
      8 
      9 
     10 import argparse
     11 import collections
     12 import json
     13 import logging
     14 import os
     15 import posixpath
     16 import random
     17 import re
     18 import shlex
     19 import shutil
     20 import subprocess
     21 import sys
     22 import tempfile
     23 import textwrap
     24 import zipfile
     25 
     26 import adb_command_line
     27 import devil_chromium
     28 from devil import devil_env
     29 from devil.android import apk_helper
     30 from devil.android import device_errors
     31 from devil.android import device_utils
     32 from devil.android.sdk import adb_wrapper
     33 from devil.android.sdk import build_tools
     34 from devil.android.sdk import intent
     35 from devil.android.sdk import version_codes
     36 from devil.utils import run_tests_helper
     37 
     38 _DIR_SOURCE_ROOT = os.path.normpath(
     39    os.path.join(os.path.dirname(__file__), '..', '..'))
     40 _JAVA_HOME = os.path.join(_DIR_SOURCE_ROOT, 'third_party', 'jdk', 'current')
     41 
     42 with devil_env.SysPath(
     43    os.path.join(_DIR_SOURCE_ROOT, 'third_party', 'colorama', 'src')):
     44  import colorama
     45 
     46 from incremental_install import installer
     47 from pylib import constants
     48 from pylib.symbols import deobfuscator
     49 from pylib.utils import simpleperf
     50 from pylib.utils import app_bundle_utils
     51 
     52 with devil_env.SysPath(
     53    os.path.join(_DIR_SOURCE_ROOT, 'build', 'android', 'gyp')):
     54  import bundletool
     55 
     56 BASE_MODULE = 'base'
     57 
     58 
     59 def _Colorize(text, style=''):
     60  return (style
     61      + text
     62      + colorama.Style.RESET_ALL)
     63 
     64 
     65 def _InstallApk(devices, apk, install_dict):
     66  def install(device):
     67    if install_dict:
     68      installer.Install(device, install_dict, apk=apk, permissions=[])
     69    else:
     70      device.Install(apk, permissions=[], allow_downgrade=True, reinstall=True)
     71 
     72  logging.info('Installing %sincremental apk.', '' if install_dict else 'non-')
     73  device_utils.DeviceUtils.parallel(devices).pMap(install)
     74 
     75 
     76 # A named tuple containing the information needed to convert a bundle into
     77 # an installable .apks archive.
     78 # Fields:
     79 #   bundle_path: Path to input bundle file.
     80 #   bundle_apk_path: Path to output bundle .apks archive file.
     81 #   aapt2_path: Path to aapt2 tool.
     82 #   keystore_path: Path to keystore file.
     83 #   keystore_password: Password for the keystore file.
     84 #   keystore_alias: Signing key name alias within the keystore file.
     85 #   system_image_locales: List of Chromium locales to include in system .apks.
     86 BundleGenerationInfo = collections.namedtuple(
     87    'BundleGenerationInfo',
     88    'bundle_path,bundle_apks_path,aapt2_path,keystore_path,keystore_password,'
     89    'keystore_alias,system_image_locales')
     90 
     91 
     92 def _GenerateBundleApks(info,
     93                        output_path=None,
     94                        minimal=False,
     95                        minimal_sdk_version=None,
     96                        mode=None,
     97                        optimize_for=None):
     98  """Generate an .apks archive from a bundle on demand.
     99 
    100  Args:
    101    info: A BundleGenerationInfo instance.
    102    output_path: Path of output .apks archive.
    103    minimal: Create the minimal set of apks possible (english-only).
    104    minimal_sdk_version: When minimal=True, use this sdkVersion.
    105    mode: Build mode, either None, or one of app_bundle_utils.BUILD_APKS_MODES.
    106    optimize_for: Override split config, either None, or one of
    107      app_bundle_utils.OPTIMIZE_FOR_OPTIONS.
    108  """
    109  logging.info('Generating .apks file')
    110  app_bundle_utils.GenerateBundleApks(
    111      info.bundle_path,
    112      # Store .apks file beside the .aab file by default so that it gets cached.
    113      output_path or info.bundle_apks_path,
    114      info.aapt2_path,
    115      info.keystore_path,
    116      info.keystore_password,
    117      info.keystore_alias,
    118      system_image_locales=info.system_image_locales,
    119      mode=mode,
    120      minimal=minimal,
    121      minimal_sdk_version=minimal_sdk_version,
    122      optimize_for=optimize_for)
    123 
    124 
    125 def _InstallBundle(devices,
    126                   apk_helper_instance,
    127                   modules,
    128                   fake_modules,
    129                   locales=None):
    130 
    131  def Install(device):
    132    device.Install(apk_helper_instance,
    133                   permissions=[],
    134                   modules=modules,
    135                   fake_modules=fake_modules,
    136                   additional_locales=locales,
    137                   allow_downgrade=True,
    138                   reinstall=True)
    139 
    140  # Basic checks for |modules| and |fake_modules|.
    141  # * |fake_modules| cannot include 'base'.
    142  # * If |fake_modules| is given, ensure |modules| includes 'base'.
    143  # * They must be disjoint (checked by device.Install).
    144  modules_set = set(modules) if modules else set()
    145  fake_modules_set = set(fake_modules) if fake_modules else set()
    146  if BASE_MODULE in fake_modules_set:
    147    raise Exception('\'-f {}\' is disallowed.'.format(BASE_MODULE))
    148  if fake_modules_set and BASE_MODULE not in modules_set:
    149    raise Exception(
    150        '\'-f FAKE\' must be accompanied by \'-m {}\''.format(BASE_MODULE))
    151 
    152  logging.info('Installing bundle.')
    153  device_utils.DeviceUtils.parallel(devices).pMap(Install)
    154 
    155 
    156 def _UninstallApk(devices, install_dict, package_name):
    157  def uninstall(device):
    158    if install_dict:
    159      installer.Uninstall(device, package_name)
    160    else:
    161      device.Uninstall(package_name)
    162  device_utils.DeviceUtils.parallel(devices).pMap(uninstall)
    163 
    164 
    165 def _IsWebViewProvider(apk_helper_instance):
    166  meta_data = apk_helper_instance.GetAllMetadata()
    167  meta_data_keys = [pair[0] for pair in meta_data]
    168  return 'com.android.webview.WebViewLibrary' in meta_data_keys
    169 
    170 
    171 def _SetWebViewProvider(devices, package_name):
    172 
    173  def switch_provider(device):
    174    if device.build_version_sdk < version_codes.NOUGAT:
    175      logging.error('No need to switch provider on pre-Nougat devices (%s)',
    176                    device.serial)
    177    else:
    178      device.SetWebViewImplementation(package_name)
    179 
    180  device_utils.DeviceUtils.parallel(devices).pMap(switch_provider)
    181 
    182 
    183 def _NormalizeProcessName(debug_process_name, package_name):
    184  if not debug_process_name:
    185    debug_process_name = package_name
    186  elif debug_process_name.startswith(':'):
    187    debug_process_name = package_name + debug_process_name
    188  elif '.' not in debug_process_name:
    189    debug_process_name = package_name + ':' + debug_process_name
    190  return debug_process_name
    191 
    192 
    193 def _ResolveActivity(device, package_name, category, action):
    194  # E.g.:
    195  # Activity Resolver Table:
    196  #   Schemes:
    197  #     http:
    198  #       67e97c0 org.chromium.pkg/.MainActivityfilter c91d43e
    199  #         Action: "android.intent.action.VIEW"
    200  #         Category: "android.intent.category.DEFAULT"
    201  #         Category: "android.intent.category.BROWSABLE"
    202  #         Scheme: "http"
    203  #         Scheme: "https"
    204  #
    205  #   Non-Data Actions:
    206  #     android.intent.action.MAIN:
    207  #       67e97c0 org.chromium.pkg/.MainActivity filter 4a34cf9
    208  #         Action: "android.intent.action.MAIN"
    209  #         Category: "android.intent.category.LAUNCHER"
    210  lines = device.RunShellCommand(['dumpsys', 'package', package_name],
    211                                 check_return=True)
    212 
    213  # Extract the Activity Resolver Table: section.
    214  start_idx = next((i for i, l in enumerate(lines)
    215                    if l.startswith('Activity Resolver Table:')), None)
    216  if start_idx is None:
    217    if not device.IsApplicationInstalled(package_name):
    218      raise Exception('Package not installed: ' + package_name)
    219    raise Exception('No Activity Resolver Table in:\n' + '\n'.join(lines))
    220  line_count = next(i for i, l in enumerate(lines[start_idx + 1:])
    221                    if l and not l[0].isspace())
    222  data = '\n'.join(lines[start_idx:start_idx + line_count])
    223 
    224  # Split on each Activity entry.
    225  entries = re.split(r'^        [0-9a-f]+ ', data, flags=re.MULTILINE)
    226 
    227  def activity_name_from_entry(entry):
    228    assert entry.startswith(package_name), 'Got: ' + entry
    229    activity_name = entry[len(package_name) + 1:].split(' ', 1)[0]
    230    if activity_name[0] == '.':
    231      activity_name = package_name + activity_name
    232    return activity_name
    233 
    234  # Find the one with the text we want.
    235  category_text = f'Category: "{category}"'
    236  action_text = f'Action: "{action}"'
    237  matched_entries = [
    238      e for e in entries[1:] if category_text in e and action_text in e
    239  ]
    240 
    241  if not matched_entries:
    242    raise Exception(f'Did not find {category_text}, {action_text} in\n{data}')
    243  if len(matched_entries) > 1:
    244    # When there are multiple matches, look for the one marked as default.
    245    # Necessary for Monochrome, which also has MonochromeLauncherActivity.
    246    default_entries = [
    247        e for e in matched_entries if 'android.intent.category.DEFAULT' in e
    248    ]
    249    matched_entries = default_entries or matched_entries
    250 
    251  # See if all matches point to the same activity.
    252  activity_names = {activity_name_from_entry(e) for e in matched_entries}
    253 
    254  if len(activity_names) > 1:
    255    raise Exception('Found multiple launcher activities:\n * ' +
    256                    '\n * '.join(sorted(activity_names)))
    257  return next(iter(activity_names))
    258 
    259 
    260 def _ReadDeviceFlags(device, command_line_flags_file):
    261  device_path = f'/data/local/tmp/{command_line_flags_file}'
    262  old_flags = device.RunShellCommand(f'cat {device_path} 2>/dev/null',
    263                                     as_root=True,
    264                                     shell=True,
    265                                     check_return=False,
    266                                     raw_output=True)
    267  if not old_flags:
    268    return None
    269  if old_flags.startswith('_ '):
    270    old_flags = old_flags[2:]
    271 
    272  return old_flags
    273 
    274 
    275 def _UpdateDeviceFlags(device, command_line_flags_file, new_flags):
    276  if not command_line_flags_file:
    277    if new_flags:
    278      logging.warning('Command-line flags are not configured for this target.')
    279    return
    280 
    281  old_flags = _ReadDeviceFlags(device, command_line_flags_file)
    282 
    283  if new_flags is None:
    284    if old_flags:
    285      logging.warning('Using pre-existing command-line flags: %s', old_flags)
    286    return
    287 
    288  if new_flags != old_flags:
    289    adb_command_line.CheckBuildTypeSupportsFlags(device,
    290                                                 command_line_flags_file)
    291    # This file does not need to be owned by root, but devil's flag_changer
    292    # helper uses as_root, so existing files cannot be updated without it.
    293    device_path = f'/data/local/tmp/{command_line_flags_file}'
    294    if new_flags:
    295      logging.info('Updated flags file: %s with value: %s', device_path,
    296                   new_flags)
    297      device.WriteFile(device_path, '_ ' + new_flags, as_root=True)
    298    else:
    299      logging.info('Removed flags file: %s', device_path)
    300      device.RemovePath(device_path, force=True, as_root=True)
    301 
    302 
    303 def _LaunchUrl(devices,
    304               package_name,
    305               argv=None,
    306               command_line_flags_file=None,
    307               url=None,
    308               wait_for_java_debugger=False,
    309               debug_process_name=None,
    310               nokill=None):
    311  if argv and command_line_flags_file is None:
    312    raise Exception('This apk does not support any flags.')
    313 
    314  debug_process_name = _NormalizeProcessName(debug_process_name, package_name)
    315 
    316  if url is None:
    317    category = 'android.intent.category.LAUNCHER'
    318    action = 'android.intent.action.MAIN'
    319  else:
    320    category = 'android.intent.category.BROWSABLE'
    321    action = 'android.intent.action.VIEW'
    322 
    323  def launch(device):
    324    activity = _ResolveActivity(device, package_name, category, action)
    325    # --persistent is required to have Settings.Global.DEBUG_APP be set, which
    326    # we currently use to allow reading of flags. https://crbug.com/784947
    327    if not nokill:
    328      cmd = ['am', 'set-debug-app', '--persistent', debug_process_name]
    329      if wait_for_java_debugger:
    330        cmd[-1:-1] = ['-w']
    331      # Ignore error since it will fail if apk is not debuggable.
    332      device.RunShellCommand(cmd, check_return=False)
    333 
    334      # The flags are first updated with input args.
    335      _UpdateDeviceFlags(device, command_line_flags_file, argv)
    336 
    337    launch_intent = intent.Intent(action=action,
    338                                  activity=activity,
    339                                  data=url,
    340                                  package=package_name)
    341    logging.info('Sending launch intent for %s', activity)
    342    device.StartActivity(launch_intent)
    343 
    344  device_utils.DeviceUtils.parallel(devices).pMap(launch)
    345  if wait_for_java_debugger:
    346    print('Waiting for debugger to attach to process: ' +
    347          _Colorize(debug_process_name, colorama.Fore.YELLOW))
    348 
    349 
    350 def _TargetCpuToTargetArch(target_cpu):
    351  if target_cpu == 'x64':
    352    return 'x86_64'
    353  if target_cpu == 'mipsel':
    354    return 'mips'
    355  return target_cpu
    356 
    357 
    358 def _RunGdb(device, package_name, debug_process_name, pid, output_directory,
    359            target_cpu, port, ide, verbose):
    360  if not pid:
    361    debug_process_name = _NormalizeProcessName(debug_process_name, package_name)
    362    pid = device.GetApplicationPids(debug_process_name, at_most_one=True)
    363  if not pid:
    364    # Attaching gdb makes the app run so slow that it takes *minutes* to start
    365    # up (as of 2018). Better to just fail than to start & attach.
    366    raise Exception('App not running.')
    367 
    368  gdb_script_path = os.path.dirname(__file__) + '/adb_gdb'
    369  cmd = [
    370      gdb_script_path,
    371      '--package-name=%s' % package_name,
    372      '--output-directory=%s' % output_directory,
    373      '--adb=%s' % adb_wrapper.AdbWrapper.GetAdbPath(),
    374      '--device=%s' % device.serial,
    375      '--pid=%s' % pid,
    376      '--port=%d' % port,
    377  ]
    378  if ide:
    379    cmd.append('--ide')
    380  # Enable verbose output of adb_gdb if it's set for this script.
    381  if verbose:
    382    cmd.append('--verbose')
    383  if target_cpu:
    384    cmd.append('--target-arch=%s' % _TargetCpuToTargetArch(target_cpu))
    385  logging.warning('Running: %s', ' '.join(shlex.quote(x) for x in cmd))
    386  print(_Colorize('All subsequent output is from adb_gdb script.',
    387                  colorama.Fore.YELLOW))
    388  os.execv(gdb_script_path, cmd)
    389 
    390 
    391 def _RunLldb(device,
    392             package_name,
    393             debug_process_name,
    394             pid,
    395             output_directory,
    396             port,
    397             target_cpu=None,
    398             ndk_dir=None,
    399             lldb_server=None,
    400             lldb=None,
    401             verbose=None):
    402  if not pid:
    403    debug_process_name = _NormalizeProcessName(debug_process_name, package_name)
    404    pid = device.GetApplicationPids(debug_process_name, at_most_one=True)
    405  if not pid:
    406    # Attaching lldb makes the app run so slow that it takes *minutes* to start
    407    # up (as of 2018). Better to just fail than to start & attach.
    408    raise Exception('App not running.')
    409 
    410  lldb_script_path = os.path.dirname(__file__) + '/connect_lldb.sh'
    411  cmd = [
    412      lldb_script_path,
    413      '--package-name=%s' % package_name,
    414      '--output-directory=%s' % output_directory,
    415      '--adb=%s' % adb_wrapper.AdbWrapper.GetAdbPath(),
    416      '--device=%s' % device.serial,
    417      '--pid=%s' % pid,
    418      '--port=%d' % port,
    419  ]
    420  # Enable verbose output of connect_lldb.sh if it's set for this script.
    421  if verbose:
    422    cmd.append('--verbose')
    423  if target_cpu:
    424    cmd.append('--target-arch=%s' % _TargetCpuToTargetArch(target_cpu))
    425  if ndk_dir:
    426    cmd.append('--ndk-dir=%s' % ndk_dir)
    427  if lldb_server:
    428    cmd.append('--lldb-server=%s' % lldb_server)
    429  if lldb:
    430    cmd.append('--lldb=%s' % lldb)
    431  logging.warning('Running: %s', ' '.join(shlex.quote(x) for x in cmd))
    432  print(
    433      _Colorize('All subsequent output is from connect_lldb.sh script.',
    434                colorama.Fore.YELLOW))
    435  os.execv(lldb_script_path, cmd)
    436 
    437 
    438 def _PrintPerDeviceOutput(devices, results, single_line=False):
    439  for d, result in zip(devices, results):
    440    if not single_line and d is not devices[0]:
    441      sys.stdout.write('\n')
    442    sys.stdout.write(
    443          _Colorize('{} ({}):'.format(d, d.build_description),
    444                    colorama.Fore.YELLOW))
    445    sys.stdout.write(' ' if single_line else '\n')
    446    yield result
    447 
    448 
    449 def _RunMemUsage(devices, package_name, query_app=False):
    450  cmd_args = ['dumpsys', 'meminfo']
    451  if not query_app:
    452    cmd_args.append('--local')
    453 
    454  def mem_usage_helper(d):
    455    ret = []
    456    for process in sorted(_GetPackageProcesses(d, package_name)):
    457      meminfo = d.RunShellCommand(cmd_args + [str(process.pid)])
    458      ret.append((process.name, '\n'.join(meminfo)))
    459    return ret
    460 
    461  parallel_devices = device_utils.DeviceUtils.parallel(devices)
    462  all_results = parallel_devices.pMap(mem_usage_helper).pGet(None)
    463  for result in _PrintPerDeviceOutput(devices, all_results):
    464    if not result:
    465      print('No processes found.')
    466    else:
    467      for name, usage in sorted(result):
    468        print(_Colorize('==== Output of "dumpsys meminfo %s" ====' % name,
    469                        colorama.Fore.GREEN))
    470        print(usage)
    471 
    472 
    473 def _DuHelper(device, path_spec, run_as=None):
    474  """Runs "du -s -k |path_spec|" on |device| and returns parsed result.
    475 
    476  Args:
    477    device: A DeviceUtils instance.
    478    path_spec: The list of paths to run du on. May contain shell expansions
    479        (will not be escaped).
    480    run_as: Package name to run as, or None to run as shell user. If not None
    481        and app is not android:debuggable (run-as fails), then command will be
    482        run as root.
    483 
    484  Returns:
    485    A dict of path->size in KiB containing all paths in |path_spec| that exist
    486    on device. Paths that do not exist are silently ignored.
    487  """
    488  # Example output for: du -s -k /data/data/org.chromium.chrome/{*,.*}
    489  # 144     /data/data/org.chromium.chrome/cache
    490  # 8       /data/data/org.chromium.chrome/files
    491  # <snip>
    492  # du: .*: No such file or directory
    493 
    494  # The -d flag works differently across android version, so use -s instead.
    495  # Without the explicit 2>&1, stderr and stdout get combined at random :(.
    496  cmd_str = 'du -s -k ' + path_spec + ' 2>&1'
    497  lines = device.RunShellCommand(cmd_str, run_as=run_as, shell=True,
    498                                 check_return=False)
    499  output = '\n'.join(lines)
    500  # run-as: Package 'com.android.chrome' is not debuggable
    501  if output.startswith('run-as:'):
    502    # check_return=False needed for when some paths in path_spec do not exist.
    503    lines = device.RunShellCommand(cmd_str, as_root=True, shell=True,
    504                                   check_return=False)
    505  ret = {}
    506  try:
    507    for line in lines:
    508      # du: .*: No such file or directory
    509      if line.startswith('du:'):
    510        continue
    511      size, subpath = line.split(None, 1)
    512      ret[subpath] = int(size)
    513    return ret
    514  except ValueError:
    515    logging.error('du command was: %s', cmd_str)
    516    logging.error('Failed to parse du output:\n%s', output)
    517    raise
    518 
    519 
    520 def _RunDiskUsage(devices, package_name):
    521  # Measuring dex size is a bit complicated:
    522  # https://source.android.com/devices/tech/dalvik/jit-compiler
    523  #
    524  # For KitKat and below:
    525  #   dumpsys package contains:
    526  #     dataDir=/data/data/org.chromium.chrome
    527  #     codePath=/data/app/org.chromium.chrome-1.apk
    528  #     resourcePath=/data/app/org.chromium.chrome-1.apk
    529  #     nativeLibraryPath=/data/app-lib/org.chromium.chrome-1
    530  #   To measure odex:
    531  #     ls -l /data/dalvik-cache/data@app@org.chromium.chrome-1.apk@classes.dex
    532  #
    533  # For Android L and M (and maybe for N+ system apps):
    534  #   dumpsys package contains:
    535  #     codePath=/data/app/org.chromium.chrome-1
    536  #     resourcePath=/data/app/org.chromium.chrome-1
    537  #     legacyNativeLibraryDir=/data/app/org.chromium.chrome-1/lib
    538  #   To measure odex:
    539  #     # Option 1:
    540  #  /data/dalvik-cache/arm/data@app@org.chromium.chrome-1@base.apk@classes.dex
    541  #  /data/dalvik-cache/arm/data@app@org.chromium.chrome-1@base.apk@classes.vdex
    542  #     ls -l /data/dalvik-cache/profiles/org.chromium.chrome
    543  #         (these profiles all appear to be 0 bytes)
    544  #     # Option 2:
    545  #     ls -l /data/app/org.chromium.chrome-1/oat/arm/base.odex
    546  #
    547  # For Android N+:
    548  #   dumpsys package contains:
    549  #     dataDir=/data/user/0/org.chromium.chrome
    550  #     codePath=/data/app/org.chromium.chrome-UuCZ71IE-i5sZgHAkU49_w==
    551  #     resourcePath=/data/app/org.chromium.chrome-UuCZ71IE-i5sZgHAkU49_w==
    552  #     legacyNativeLibraryDir=/data/app/org.chromium.chrome-GUID/lib
    553  #     Instruction Set: arm
    554  #       path: /data/app/org.chromium.chrome-UuCZ71IE-i5sZgHAkU49_w==/base.apk
    555  #       status: /data/.../oat/arm/base.odex[status=kOatUpToDate, compilation_f
    556  #       ilter=quicken]
    557  #     Instruction Set: arm64
    558  #       path: /data/app/org.chromium.chrome-UuCZ71IE-i5sZgHAkU49_w==/base.apk
    559  #       status: /data/.../oat/arm64/base.odex[status=..., compilation_filter=q
    560  #       uicken]
    561  #   To measure odex:
    562  #     ls -l /data/app/.../oat/arm/base.odex
    563  #     ls -l /data/app/.../oat/arm/base.vdex (optional)
    564  #   To measure the correct odex size:
    565  #     cmd package compile -m speed org.chromium.chrome  # For webview
    566  #     cmd package compile -m speed-profile org.chromium.chrome  # For others
    567  def disk_usage_helper(d):
    568    package_output = '\n'.join(d.RunShellCommand(
    569        ['dumpsys', 'package', package_name], check_return=True))
    570    # Does not return error when apk is not installed.
    571    if not package_output or 'Unable to find package:' in package_output:
    572      return None
    573 
    574    # Ignore system apks that have updates installed.
    575    package_output = re.sub(r'Hidden system packages:.*?^\b', '',
    576                            package_output, flags=re.S | re.M)
    577 
    578    try:
    579      data_dir = re.search(r'dataDir=(.*)', package_output).group(1)
    580      code_path = re.search(r'codePath=(.*)', package_output).group(1)
    581      lib_path = re.search(r'(?:legacyN|n)ativeLibrary(?:Dir|Path)=(.*)',
    582                           package_output).group(1)
    583    except AttributeError as e:
    584      raise Exception('Error parsing dumpsys output: ' + package_output) from e
    585 
    586    if code_path.startswith('/system'):
    587      logging.warning('Measurement of system image apks can be innacurate')
    588 
    589    compilation_filters = set()
    590    # Match "compilation_filter=value", where a line break can occur at any spot
    591    # (refer to examples above).
    592    awful_wrapping = r'\s*'.join('compilation_filter=')
    593    for m in re.finditer(awful_wrapping + r'([\s\S]+?)[\],]', package_output):
    594      compilation_filters.add(re.sub(r'\s+', '', m.group(1)))
    595    # Starting Android Q, output looks like:
    596    #  arm: [status=speed-profile] [reason=install]
    597    for m in re.finditer(r'\[status=(.+?)\]', package_output):
    598      compilation_filters.add(m.group(1))
    599    compilation_filter = ','.join(sorted(compilation_filters))
    600 
    601    data_dir_sizes = _DuHelper(d, '%s/{*,.*}' % data_dir, run_as=package_name)
    602    # Measure code_cache separately since it can be large.
    603    code_cache_sizes = {}
    604    code_cache_dir = next(
    605        (k for k in data_dir_sizes if k.endswith('/code_cache')), None)
    606    if code_cache_dir:
    607      data_dir_sizes.pop(code_cache_dir)
    608      code_cache_sizes = _DuHelper(d, '%s/{*,.*}' % code_cache_dir,
    609                                   run_as=package_name)
    610 
    611    apk_path_spec = code_path
    612    if not apk_path_spec.endswith('.apk'):
    613      apk_path_spec += '/*.apk'
    614    apk_sizes = _DuHelper(d, apk_path_spec)
    615    if lib_path.endswith('/lib'):
    616      # Shows architecture subdirectory.
    617      lib_sizes = _DuHelper(d, '%s/{*,.*}' % lib_path)
    618    else:
    619      lib_sizes = _DuHelper(d, lib_path)
    620 
    621    # Look at all possible locations for odex files.
    622    odex_paths = []
    623    for apk_path in apk_sizes:
    624      mangled_apk_path = apk_path[1:].replace('/', '@')
    625      apk_basename = posixpath.basename(apk_path)[:-4]
    626      for ext in ('dex', 'odex', 'vdex', 'art'):
    627        # Easier to check all architectures than to determine active ones.
    628        for arch in ('arm', 'arm64', 'x86', 'x86_64', 'mips', 'mips64'):
    629          odex_paths.append(
    630              '%s/oat/%s/%s.%s' % (code_path, arch, apk_basename, ext))
    631          # No app could possibly have more than 6 dex files.
    632          for suffix in ('', '2', '3', '4', '5'):
    633            odex_paths.append('/data/dalvik-cache/%s/%s@classes%s.%s' % (
    634                arch, mangled_apk_path, suffix, ext))
    635            # This path does not have |arch|, so don't repeat it for every arch.
    636            if arch == 'arm':
    637              odex_paths.append('/data/dalvik-cache/%s@classes%s.dex' % (
    638                  mangled_apk_path, suffix))
    639 
    640    odex_sizes = _DuHelper(d, ' '.join(shlex.quote(p) for p in odex_paths))
    641 
    642    return (data_dir_sizes, code_cache_sizes, apk_sizes, lib_sizes, odex_sizes,
    643            compilation_filter)
    644 
    645  def print_sizes(desc, sizes):
    646    print('%s: %d KiB' % (desc, sum(sizes.values())))
    647    for path, size in sorted(sizes.items()):
    648      print('    %s: %s KiB' % (path, size))
    649 
    650  parallel_devices = device_utils.DeviceUtils.parallel(devices)
    651  all_results = parallel_devices.pMap(disk_usage_helper).pGet(None)
    652  for result in _PrintPerDeviceOutput(devices, all_results):
    653    if not result:
    654      print('APK is not installed.')
    655      continue
    656 
    657    (data_dir_sizes, code_cache_sizes, apk_sizes, lib_sizes, odex_sizes,
    658     compilation_filter) = result
    659    total = sum(sum(sizes.values()) for sizes in result[:-1])
    660 
    661    print_sizes('Apk', apk_sizes)
    662    print_sizes('App Data (non-code cache)', data_dir_sizes)
    663    print_sizes('App Data (code cache)', code_cache_sizes)
    664    print_sizes('Native Libs', lib_sizes)
    665    show_warning = compilation_filter and 'speed' not in compilation_filter
    666    compilation_filter = compilation_filter or 'n/a'
    667    print_sizes('odex (compilation_filter=%s)' % compilation_filter, odex_sizes)
    668    if show_warning:
    669      logging.warning('For a more realistic odex size, run:')
    670      logging.warning('    %s compile-dex [speed|speed-profile]', sys.argv[0])
    671    print('Total: %s KiB (%.1f MiB)' % (total, total / 1024.0))
    672 
    673 
    674 class _LogcatProcessor:
    675  ParsedLine = collections.namedtuple(
    676      'ParsedLine',
    677      ['date', 'invokation_time', 'pid', 'tid', 'priority', 'tag', 'message'])
    678 
    679  class NativeStackSymbolizer:
    680    """Buffers lines from native stacks and symbolizes them when done."""
    681    # E.g.: #06 pc 0x0000d519 /apex/com.android.runtime/lib/libart.so
    682    # E.g.: #01 pc 00180c8d  /data/data/.../lib/libbase.cr.so
    683    _STACK_PATTERN = re.compile(r'\s*#\d+\s+(?:pc )?(0x)?[0-9a-f]{8,16}\s')
    684 
    685    def __init__(self, stack_script_context, print_func):
    686      # To symbolize native stacks, we need to pass all lines at once.
    687      self._stack_script_context = stack_script_context
    688      self._print_func = print_func
    689      self._crash_lines_buffer = None
    690 
    691    def _FlushLines(self):
    692      """Prints queued lines after sending them through stack.py."""
    693      if self._crash_lines_buffer is None:
    694        return
    695 
    696      crash_lines = self._crash_lines_buffer
    697      self._crash_lines_buffer = None
    698      with tempfile.NamedTemporaryFile(mode='w') as f:
    699        f.writelines(x[0].message + '\n' for x in crash_lines)
    700        f.flush()
    701        proc = self._stack_script_context.Popen(
    702            input_file=f.name, stdout=subprocess.PIPE)
    703        lines = proc.communicate()[0].splitlines()
    704 
    705      for i, line in enumerate(lines):
    706        parsed_line, dim = crash_lines[min(i, len(crash_lines) - 1)]
    707        d = parsed_line._asdict()
    708        d['message'] = line
    709        parsed_line = _LogcatProcessor.ParsedLine(**d)
    710        self._print_func(parsed_line, dim)
    711 
    712    def AddLine(self, parsed_line, dim):
    713      # Assume all lines from DEBUG are stacks.
    714      # Also look for "stack-looking" lines to catch manual stack prints.
    715      # It's important to not buffer non-stack lines because stack.py does not
    716      # pass them through.
    717      is_crash_line = parsed_line.tag == 'DEBUG' or (self._STACK_PATTERN.match(
    718          parsed_line.message))
    719 
    720      if is_crash_line:
    721        if self._crash_lines_buffer is None:
    722          self._crash_lines_buffer = []
    723        self._crash_lines_buffer.append((parsed_line, dim))
    724        return
    725 
    726      self._FlushLines()
    727 
    728      self._print_func(parsed_line, dim)
    729 
    730 
    731  # Logcat tags for messages that are generally relevant but are not from PIDs
    732  # associated with the apk.
    733  _ALLOWLISTED_TAGS = {
    734      'ActivityManager',  # Shows activity lifecycle messages.
    735      'ActivityTaskManager',  # More activity lifecycle messages.
    736      'AndroidRuntime',  # Java crash dumps
    737      'AppZygoteInit',  # Android's native application zygote support.
    738      'DEBUG',  # Native crash dump.
    739  }
    740 
    741  # Matches messages only on pre-L (Dalvik) that are spammy and unimportant.
    742  _DALVIK_IGNORE_PATTERN = re.compile('|'.join([
    743      r'^Added shared lib',
    744      r'^Could not find ',
    745      r'^DexOpt:',
    746      r'^GC_',
    747      r'^Late-enabling CheckJNI',
    748      r'^Link of class',
    749      r'^No JNI_OnLoad found in',
    750      r'^Trying to load lib',
    751      r'^Unable to resolve superclass',
    752      r'^VFY:',
    753      r'^WAIT_',
    754  ]))
    755 
    756  def __init__(self,
    757               device,
    758               package_name,
    759               stack_script_context,
    760               deobfuscate=None,
    761               verbose=False,
    762               exit_on_match=None,
    763               extra_package_names=None):
    764    self._device = device
    765    self._package_name = package_name
    766    self._extra_package_names = extra_package_names or []
    767    self._verbose = verbose
    768    self._deobfuscator = deobfuscate
    769    if exit_on_match is not None:
    770      self._exit_on_match = re.compile(exit_on_match)
    771    else:
    772      self._exit_on_match = None
    773    self._found_exit_match = False
    774    if stack_script_context:
    775      self._print_func = _LogcatProcessor.NativeStackSymbolizer(
    776          stack_script_context, self._PrintParsedLine).AddLine
    777    else:
    778      self._print_func = self._PrintParsedLine
    779    # Process ID for the app's main process (with no :name suffix).
    780    self._primary_pid = None
    781    # Set of all Process IDs that belong to the app.
    782    self._my_pids = set()
    783    # Set of all Process IDs that we've parsed at some point.
    784    self._seen_pids = set()
    785    # Start proc 22953:com.google.chromeremotedesktop/
    786    self._pid_pattern = re.compile(r'Start proc (\d+):{}/'.format(package_name))
    787    # START u0 {act=android.intent.action.MAIN \
    788    # cat=[android.intent.category.LAUNCHER] \
    789    # flg=0x10000000 pkg=com.google.chromeremotedesktop} from uid 2000
    790    self._start_pattern = re.compile(r'START .*(?:cmp|pkg)=' + package_name)
    791 
    792    self.nonce = 'Chromium apk_operations.py nonce={}'.format(random.random())
    793    # Holds lines buffered on start-up, before we find our nonce message.
    794    self._initial_buffered_lines = []
    795    self._UpdateMyPids()
    796    # Give preference to PID reported by "ps" over those found from
    797    # _start_pattern. There can be multiple "Start proc" messages from prior
    798    # runs of the app.
    799    self._found_initial_pid = self._primary_pid is not None
    800    # Retrieve any additional patterns that are relevant for the User.
    801    self._user_defined_highlight = None
    802    user_regex = os.environ.get('CHROMIUM_LOGCAT_HIGHLIGHT')
    803    if user_regex:
    804      self._user_defined_highlight = re.compile(user_regex)
    805      if not self._user_defined_highlight:
    806        print(_Colorize(
    807            'Rejecting invalid regular expression: {}'.format(user_regex),
    808            colorama.Fore.RED + colorama.Style.BRIGHT))
    809 
    810  def _UpdateMyPids(self):
    811    # We intentionally do not clear self._my_pids to make sure that the
    812    # ProcessLine method below also includes lines from processes which may
    813    # have already exited.
    814    self._primary_pid = None
    815    for package_name in [self._package_name] + self._extra_package_names:
    816      for process in _GetPackageProcesses(self._device, package_name):
    817        # We take only the first "main" process found in order to account for
    818        # possibly forked() processes.
    819        if ':' not in process.name and self._primary_pid is None:
    820          self._primary_pid = process.pid
    821        self._my_pids.add(process.pid)
    822 
    823  def _GetPidStyle(self, pid, dim=False):
    824    if pid == self._primary_pid:
    825      return colorama.Fore.WHITE
    826    if pid in self._my_pids:
    827      # TODO(wnwen): Use one separate persistent color per process, pop LRU
    828      return colorama.Fore.YELLOW
    829    if dim:
    830      return colorama.Style.DIM
    831    return ''
    832 
    833  def _GetPriorityStyle(self, priority, dim=False):
    834    # pylint:disable=no-self-use
    835    if dim:
    836      return ''
    837    style = colorama.Fore.BLACK
    838    if priority in ('E', 'F'):
    839      style += colorama.Back.RED
    840    elif priority == 'W':
    841      style += colorama.Back.YELLOW
    842    elif priority == 'I':
    843      style += colorama.Back.GREEN
    844    elif priority == 'D':
    845      style += colorama.Back.BLUE
    846    return style
    847 
    848  def _ParseLine(self, line):
    849    tokens = line.split(None, 6)
    850 
    851    def consume_token_or_default(default):
    852      return tokens.pop(0) if len(tokens) > 0 else default
    853 
    854    def consume_integer_token_or_default(default):
    855      if len(tokens) == 0:
    856        return default
    857 
    858      try:
    859        return int(tokens.pop(0))
    860      except ValueError:
    861        return default
    862 
    863    date = consume_token_or_default('')
    864    invokation_time = consume_token_or_default('')
    865    pid = consume_integer_token_or_default(-1)
    866    tid = consume_integer_token_or_default(-1)
    867    priority = consume_token_or_default('')
    868    tag = consume_token_or_default('')
    869    original_message = consume_token_or_default('')
    870 
    871    # Example:
    872    #   09-19 06:35:51.113  9060  9154 W GCoreFlp: No location...
    873    #   09-19 06:01:26.174  9060 10617 I Auth    : [ReflectiveChannelBinder]...
    874    # Parsing "GCoreFlp:" vs "Auth    :", we only want tag to contain the word,
    875    # and we don't want to keep the colon for the message.
    876    if tag and tag[-1] == ':':
    877      tag = tag[:-1]
    878    elif len(original_message) > 2:
    879      original_message = original_message[2:]
    880    return self.ParsedLine(
    881        date, invokation_time, pid, tid, priority, tag, original_message)
    882 
    883  def _PrintParsedLine(self, parsed_line, dim=False):
    884    if self._exit_on_match and self._exit_on_match.search(parsed_line.message):
    885      self._found_exit_match = True
    886 
    887    tid_style = colorama.Style.NORMAL
    888    user_match = self._user_defined_highlight and (
    889        re.search(self._user_defined_highlight, parsed_line.tag)
    890        or re.search(self._user_defined_highlight, parsed_line.message))
    891 
    892    # Make the main thread bright.
    893    if not dim and parsed_line.pid == parsed_line.tid:
    894      tid_style = colorama.Style.BRIGHT
    895    pid_style = self._GetPidStyle(parsed_line.pid, dim)
    896    msg_style = pid_style if not user_match else (colorama.Fore.GREEN +
    897                                                  colorama.Style.BRIGHT)
    898    # We have to pad before adding color as that changes the width of the tag.
    899    pid_str = _Colorize('{:5}'.format(parsed_line.pid), pid_style)
    900    tid_str = _Colorize('{:5}'.format(parsed_line.tid), tid_style)
    901    tag = _Colorize('{:8}'.format(parsed_line.tag),
    902                    pid_style + ('' if dim else colorama.Style.BRIGHT))
    903    priority = _Colorize(parsed_line.priority,
    904                         self._GetPriorityStyle(parsed_line.priority))
    905    messages = [parsed_line.message]
    906    if self._deobfuscator:
    907      messages = self._deobfuscator.TransformLines(messages)
    908    for message in messages:
    909      message = _Colorize(message, msg_style)
    910      sys.stdout.write('{} {} {} {} {} {}: {}\n'.format(
    911          parsed_line.date, parsed_line.invokation_time, pid_str, tid_str,
    912          priority, tag, message))
    913 
    914  def _TriggerNonceFound(self):
    915    # Once the nonce is hit, we have confidence that we know which lines
    916    # belong to the current run of the app. Process all of the buffered lines.
    917    if self._primary_pid:
    918      for args in self._initial_buffered_lines:
    919        self._print_func(*args)
    920    self._initial_buffered_lines = None
    921    self.nonce = None
    922 
    923  def FoundExitMatch(self):
    924    return self._found_exit_match
    925 
    926  def ProcessLine(self, line):
    927    if not line or line.startswith('------'):
    928      return
    929 
    930    if self.nonce and self.nonce in line:
    931      self._TriggerNonceFound()
    932 
    933    nonce_found = self.nonce is None
    934 
    935    log = self._ParseLine(line)
    936    if log.pid not in self._seen_pids:
    937      self._seen_pids.add(log.pid)
    938      if nonce_found:
    939        # Update list of owned PIDs each time a new PID is encountered.
    940        self._UpdateMyPids()
    941 
    942    # Search for "Start proc $pid:$package_name/" message.
    943    if not nonce_found:
    944      # Capture logs before the nonce. Start with the most recent "am start".
    945      if self._start_pattern.match(log.message):
    946        self._initial_buffered_lines = []
    947 
    948      # If we didn't find the PID via "ps", then extract it from log messages.
    949      # This will happen if the app crashes too quickly.
    950      if not self._found_initial_pid:
    951        m = self._pid_pattern.match(log.message)
    952        if m:
    953          # Find the most recent "Start proc" line before the nonce.
    954          # Track only the primary pid in this mode.
    955          # The main use-case is to find app logs when no current PIDs exist.
    956          # E.g.: When the app crashes on launch.
    957          self._primary_pid = m.group(1)
    958          self._my_pids.clear()
    959          self._my_pids.add(m.group(1))
    960 
    961    owned_pid = log.pid in self._my_pids
    962    if owned_pid and not self._verbose and log.tag == 'dalvikvm':
    963      if self._DALVIK_IGNORE_PATTERN.match(log.message):
    964        return
    965 
    966    if owned_pid or self._verbose or (log.priority == 'F' or  # Java crash dump
    967                                      log.tag in self._ALLOWLISTED_TAGS):
    968      if nonce_found:
    969        self._print_func(log, not owned_pid)
    970      else:
    971        self._initial_buffered_lines.append((log, not owned_pid))
    972 
    973 
    974 def _RunLogcat(device,
    975               package_name,
    976               stack_script_context,
    977               deobfuscate,
    978               verbose,
    979               exit_on_match=None,
    980               extra_package_names=None):
    981  logcat_processor = _LogcatProcessor(device,
    982                                      package_name,
    983                                      stack_script_context,
    984                                      deobfuscate,
    985                                      verbose,
    986                                      exit_on_match=exit_on_match,
    987                                      extra_package_names=extra_package_names)
    988  device.RunShellCommand(['log', logcat_processor.nonce])
    989  for line in device.adb.Logcat(logcat_format='threadtime'):
    990    try:
    991      logcat_processor.ProcessLine(line)
    992      if logcat_processor.FoundExitMatch():
    993        return
    994    except:
    995      sys.stderr.write('Failed to process line: ' + line + '\n')
    996      # Skip stack trace for the common case of the adb server being
    997      # restarted.
    998      if 'unexpected EOF' in line:
    999        sys.exit(1)
   1000      raise
   1001 
   1002 
   1003 def _GetPackageProcesses(device, package_name):
   1004  my_names = (package_name, package_name + '_zygote')
   1005  return [
   1006      p for p in device.ListProcesses(package_name)
   1007      if p.name in my_names or p.name.startswith(package_name + ':')
   1008  ]
   1009 
   1010 
   1011 def _RunPs(devices, package_name):
   1012  parallel_devices = device_utils.DeviceUtils.parallel(devices)
   1013  all_processes = parallel_devices.pMap(
   1014      lambda d: _GetPackageProcesses(d, package_name)).pGet(None)
   1015  for processes in _PrintPerDeviceOutput(devices, all_processes):
   1016    if not processes:
   1017      print('No processes found.')
   1018    else:
   1019      proc_map = collections.defaultdict(list)
   1020      for p in processes:
   1021        proc_map[p.name].append(str(p.pid))
   1022      for name, pids in sorted(proc_map.items()):
   1023        print(name, ','.join(pids))
   1024 
   1025 
   1026 def _RunShell(devices, package_name, cmd):
   1027  if cmd:
   1028    parallel_devices = device_utils.DeviceUtils.parallel(devices)
   1029    outputs = parallel_devices.RunShellCommand(
   1030        cmd, run_as=package_name).pGet(None)
   1031    for output in _PrintPerDeviceOutput(devices, outputs):
   1032      for line in output:
   1033        print(line)
   1034  else:
   1035    adb_path = adb_wrapper.AdbWrapper.GetAdbPath()
   1036    cmd = [adb_path, '-s', devices[0].serial, 'shell']
   1037    # Pre-N devices do not support -t flag.
   1038    if devices[0].build_version_sdk >= version_codes.NOUGAT:
   1039      cmd += ['-t', 'run-as', package_name]
   1040    else:
   1041      print('Upon entering the shell, run:')
   1042      print('run-as', package_name)
   1043      print()
   1044    os.execv(adb_path, cmd)
   1045 
   1046 
   1047 def _RunCompileDex(devices, package_name, compilation_filter):
   1048  cmd = ['cmd', 'package', 'compile', '-f', '-m', compilation_filter,
   1049         package_name]
   1050  parallel_devices = device_utils.DeviceUtils.parallel(devices)
   1051  return parallel_devices.RunShellCommand(cmd, timeout=120).pGet(None)
   1052 
   1053 
   1054 def _RunProfile(device, package_name, host_build_directory, pprof_out_path,
   1055                process_specifier, thread_specifier, events, extra_args):
   1056  simpleperf.PrepareDevice(device)
   1057  device_simpleperf_path = simpleperf.InstallSimpleperf(device, package_name)
   1058  with tempfile.NamedTemporaryFile() as fh:
   1059    host_simpleperf_out_path = fh.name
   1060 
   1061    with simpleperf.RunSimpleperf(device, device_simpleperf_path, package_name,
   1062                                  process_specifier, thread_specifier,
   1063                                  events, extra_args, host_simpleperf_out_path):
   1064      sys.stdout.write('Profiler is running; press Enter to stop...\n')
   1065      sys.stdin.read(1)
   1066      sys.stdout.write('Post-processing data...\n')
   1067 
   1068    simpleperf.ConvertSimpleperfToPprof(host_simpleperf_out_path,
   1069                                        host_build_directory, pprof_out_path)
   1070    print(textwrap.dedent("""
   1071        Profile data written to %(s)s.
   1072 
   1073        To view profile as a call graph in browser:
   1074          pprof -web %(s)s
   1075 
   1076        To print the hottest methods:
   1077          pprof -top %(s)s
   1078 
   1079        pprof has many useful customization options; `pprof --help` for details.
   1080        """ % {'s': pprof_out_path}))
   1081 
   1082 
   1083 class _StackScriptContext:
   1084  """Maintains temporary files needed by stack.py."""
   1085 
   1086  def __init__(self,
   1087               output_directory,
   1088               apk_path,
   1089               bundle_generation_info,
   1090               quiet=False):
   1091    self._output_directory = output_directory
   1092    self._apk_path = apk_path
   1093    self._bundle_generation_info = bundle_generation_info
   1094    self._staging_dir = None
   1095    self._quiet = quiet
   1096 
   1097  def _CreateStaging(self):
   1098    # In many cases, stack decoding requires APKs to map trace lines to native
   1099    # libraries. Create a temporary directory, and either unpack a bundle's
   1100    # APKS into it, or simply symlink the standalone APK into it. This
   1101    # provides an unambiguous set of APK files for the stack decoding process
   1102    # to inspect.
   1103    logging.debug('Creating stack staging directory')
   1104    self._staging_dir = tempfile.mkdtemp()
   1105    bundle_generation_info = self._bundle_generation_info
   1106 
   1107    if bundle_generation_info:
   1108      # TODO(wnwen): Use apk_helper instead.
   1109      _GenerateBundleApks(bundle_generation_info)
   1110      logging.debug('Extracting .apks file')
   1111      with zipfile.ZipFile(bundle_generation_info.bundle_apks_path, 'r') as z:
   1112        files_to_extract = [
   1113            f for f in z.namelist() if f.endswith('-master.apk')
   1114        ]
   1115        z.extractall(self._staging_dir, files_to_extract)
   1116    elif self._apk_path:
   1117      # Otherwise an incremental APK and an empty apks directory is correct.
   1118      output = os.path.join(self._staging_dir, os.path.basename(self._apk_path))
   1119      os.symlink(self._apk_path, output)
   1120 
   1121  def Close(self):
   1122    if self._staging_dir:
   1123      logging.debug('Clearing stack staging directory')
   1124      shutil.rmtree(self._staging_dir)
   1125      self._staging_dir = None
   1126 
   1127  def Popen(self, input_file=None, **kwargs):
   1128    if self._staging_dir is None:
   1129      self._CreateStaging()
   1130    stack_script = os.path.join(
   1131        constants.host_paths.ANDROID_PLATFORM_DEVELOPMENT_SCRIPTS_PATH,
   1132        'stack.py')
   1133    cmd = [
   1134        stack_script, '--output-directory', self._output_directory,
   1135        '--apks-directory', self._staging_dir
   1136    ]
   1137    if self._quiet:
   1138      cmd.append('--quiet')
   1139    if input_file:
   1140      cmd.append(input_file)
   1141    logging.info('Running: %s', shlex.join(cmd))
   1142    return subprocess.Popen(cmd, universal_newlines=True, **kwargs)
   1143 
   1144 
   1145 def _GenerateAvailableDevicesMessage(devices):
   1146  devices_obj = device_utils.DeviceUtils.parallel(devices)
   1147  descriptions = devices_obj.pMap(lambda d: d.build_description).pGet(None)
   1148  msg = 'Available devices:\n'
   1149  for d, desc in zip(devices, descriptions):
   1150    msg += '  %s (%s)\n' % (d, desc)
   1151  return msg
   1152 
   1153 
   1154 # TODO(agrieve):add "--all" in the MultipleDevicesError message and use it here.
   1155 def _GenerateMissingAllFlagMessage(devices):
   1156  return ('More than one device available. Use --all to select all devices, ' +
   1157          'or use --device to select a device by serial.\n\n' +
   1158          _GenerateAvailableDevicesMessage(devices))
   1159 
   1160 def _SanitizeFilename(filename):
   1161  for sep in os.path.sep, os.path.altsep:
   1162    if sep is not None:
   1163      filename = filename.replace(sep, '_')
   1164  return filename
   1165 
   1166 
   1167 def _DeviceCachePath(device, output_directory):
   1168  file_name = 'device_cache_%s.json' % _SanitizeFilename(device.serial)
   1169  return os.path.join(output_directory, file_name)
   1170 
   1171 
   1172 def _LoadDeviceCaches(devices, output_directory):
   1173  if not output_directory:
   1174    return
   1175  for d in devices:
   1176    cache_path = _DeviceCachePath(d, output_directory)
   1177    if os.path.exists(cache_path):
   1178      logging.debug('Using device cache: %s', cache_path)
   1179      with open(cache_path) as f:
   1180        d.LoadCacheData(f.read())
   1181      # Delete the cached file so that any exceptions cause it to be cleared.
   1182      os.unlink(cache_path)
   1183    else:
   1184      logging.debug('No cache present for device: %s', d)
   1185 
   1186 
   1187 def _SaveDeviceCaches(devices, output_directory):
   1188  if not output_directory:
   1189    return
   1190  for d in devices:
   1191    cache_path = _DeviceCachePath(d, output_directory)
   1192    with open(cache_path, 'w') as f:
   1193      f.write(d.DumpCacheData())
   1194      logging.info('Wrote device cache: %s', cache_path)
   1195 
   1196 
   1197 class _Command:
   1198  name = None
   1199  description = None
   1200  long_description = None
   1201  needs_package_name = False
   1202  needs_output_directory = False
   1203  needs_apk_helper = False
   1204  supports_incremental = False
   1205  accepts_command_line_flags = False
   1206  accepts_args = False
   1207  need_device_args = True
   1208  all_devices_by_default = False
   1209  calls_exec = False
   1210  supports_multiple_devices = True
   1211 
   1212  def __init__(self, from_wrapper_script, is_bundle, is_test_apk):
   1213    self._parser = None
   1214    self._from_wrapper_script = from_wrapper_script
   1215    self.args = None
   1216    self.incremental_apk_path = None
   1217    self.apk_helper = None
   1218    self.additional_apk_helpers = None
   1219    self.install_dict = None
   1220    self.devices = None
   1221    self.is_bundle = is_bundle
   1222    self.is_test_apk = is_test_apk
   1223    self.bundle_generation_info = None
   1224    # Only support  incremental install from APK wrapper scripts.
   1225    if is_bundle or not from_wrapper_script:
   1226      self.supports_incremental = False
   1227 
   1228  def RegisterBundleGenerationInfo(self, bundle_generation_info):
   1229    self.bundle_generation_info = bundle_generation_info
   1230 
   1231  def _RegisterExtraArgs(self, group):
   1232    pass
   1233 
   1234  def RegisterArgs(self, parser):
   1235    subp = parser.add_parser(
   1236        self.name, help=self.description,
   1237        description=self.long_description or self.description,
   1238        formatter_class=argparse.RawDescriptionHelpFormatter)
   1239    self._parser = subp
   1240    subp.set_defaults(command=self)
   1241    if self.need_device_args:
   1242      subp.add_argument('--all',
   1243                        action='store_true',
   1244                        default=self.all_devices_by_default,
   1245                        help='Operate on all connected devices.',)
   1246      subp.add_argument('-d',
   1247                        '--device',
   1248                        action='append',
   1249                        default=[],
   1250                        dest='devices',
   1251                        help='Target device for script to work on. Enter '
   1252                            'multiple times for multiple devices.')
   1253    subp.add_argument('-v',
   1254                      '--verbose',
   1255                      action='count',
   1256                      default=0,
   1257                      dest='verbose_count',
   1258                      help='Verbose level (multiple times for more)')
   1259    group = subp.add_argument_group('%s arguments' % self.name)
   1260 
   1261    if self.needs_package_name:
   1262      # Three cases to consider here, since later code assumes
   1263      #  self.args.package_name always exists, even if None:
   1264      #
   1265      # - Called from a bundle wrapper script, the package_name is already
   1266      #   set through parser.set_defaults(), so don't call add_argument()
   1267      #   to avoid overriding its value.
   1268      #
   1269      # - Called from an apk wrapper script. The --package-name argument
   1270      #   should not appear, but self.args.package_name will be gleaned from
   1271      #   the --apk-path file later.
   1272      #
   1273      # - Called directly, then --package-name is required on the command-line.
   1274      #
   1275      # Similarly is_official_build is set directly by the bundle wrapper
   1276      # script, so it also needs to be added for non-bundle builds.
   1277      if not self.is_bundle:
   1278        group.add_argument(
   1279            '--package-name',
   1280            help=argparse.SUPPRESS if self._from_wrapper_script else (
   1281                "App's package name."))
   1282 
   1283        group.add_argument(
   1284            '--is-official-build',
   1285            action='store_true',
   1286            default=False,
   1287            help=argparse.SUPPRESS if self._from_wrapper_script else
   1288            ('Whether this is an official build or not (affects perf).'))
   1289 
   1290    if self.needs_apk_helper or self.needs_package_name:
   1291      # Adding this argument to the subparser would override the set_defaults()
   1292      # value set by on the parent parser (even if None).
   1293      if not self._from_wrapper_script and not self.is_bundle:
   1294        group.add_argument(
   1295            '--apk-path', required=self.needs_apk_helper, help='Path to .apk')
   1296 
   1297    if self.supports_incremental:
   1298      group.add_argument('--incremental',
   1299                          action='store_true',
   1300                          default=False,
   1301                          help='Always install an incremental apk.')
   1302      group.add_argument('--non-incremental',
   1303                          action='store_true',
   1304                          default=False,
   1305                          help='Always install a non-incremental apk.')
   1306 
   1307    # accepts_command_line_flags and accepts_args are mutually exclusive.
   1308    # argparse will throw if they are both set.
   1309    if self.accepts_command_line_flags:
   1310      group.add_argument(
   1311          '--args', help='Command-line flags. Use = to assign args.')
   1312 
   1313    if self.accepts_args:
   1314      group.add_argument(
   1315          '--args', help='Extra arguments. Use = to assign args')
   1316 
   1317    if not self._from_wrapper_script and self.accepts_command_line_flags:
   1318      # Provided by wrapper scripts.
   1319      group.add_argument(
   1320          '--command-line-flags-file',
   1321          help='Name of the command-line flags file')
   1322 
   1323    self._RegisterExtraArgs(group)
   1324 
   1325  def _CreateApkHelpers(self, args, incremental_apk_path, install_dict):
   1326    """Returns true iff self.apk_helper was created and assigned."""
   1327    if self.apk_helper is None:
   1328      if args.apk_path:
   1329        self.apk_helper = apk_helper.ToHelper(args.apk_path)
   1330      elif incremental_apk_path:
   1331        self.install_dict = install_dict
   1332        self.apk_helper = apk_helper.ToHelper(incremental_apk_path)
   1333      elif self.is_bundle:
   1334        _GenerateBundleApks(self.bundle_generation_info)
   1335        self.apk_helper = apk_helper.ToHelper(
   1336            self.bundle_generation_info.bundle_apks_path)
   1337    if args.additional_apk_paths and self.additional_apk_helpers is None:
   1338      self.additional_apk_helpers = [
   1339          apk_helper.ToHelper(apk_path)
   1340          for apk_path in args.additional_apk_paths
   1341      ]
   1342    return self.apk_helper is not None
   1343 
   1344  def _FindSupportedDevices(self, devices):
   1345    """Returns supported devices and reasons for each not supported one."""
   1346    app_abis = self.apk_helper.GetAbis()
   1347    calling_script_name = os.path.basename(sys.argv[0])
   1348    is_webview = 'webview' in calling_script_name
   1349    requires_32_bit = self.apk_helper.Get32BitAbiOverride() == '0xffffffff'
   1350    logging.debug('App supports (requires 32bit: %r, is webview: %r): %r',
   1351                  requires_32_bit, is_webview, app_abis)
   1352    # Webview 32_64 targets can work even on 64-bit only devices since only the
   1353    # webview library in the target needs the correct bitness.
   1354    if requires_32_bit and not is_webview:
   1355      app_abis = [abi for abi in app_abis if '64' not in abi]
   1356      logging.debug('App supports (filtered): %r', app_abis)
   1357    if not app_abis:
   1358      # The app does not have any native libs, so all devices can support it.
   1359      return devices, {}
   1360    fully_supported = []
   1361    not_supported_reasons = {}
   1362    for device in devices:
   1363      device_abis = device.GetSupportedABIs()
   1364      device_primary_abi = device_abis[0]
   1365      logging.debug('Device primary: %s', device_primary_abi)
   1366      logging.debug('Device supports: %r', device_abis)
   1367 
   1368      # x86/x86_64 emulators sometimes advertises arm support but arm builds do
   1369      # not work on them. Thus these non-functional ABIs need to be filtered out
   1370      # here to avoid resulting in hard to understand runtime failures.
   1371      if device_primary_abi in ('x86', 'x86_64'):
   1372        device_abis = [abi for abi in device_abis if not abi.startswith('arm')]
   1373        logging.debug('Device supports (filtered): %r', device_abis)
   1374 
   1375      if any(abi in app_abis for abi in device_abis):
   1376        fully_supported.append(device)
   1377        if is_webview and not all(abi in app_abis for abi in device_abis):
   1378          logging.warning(
   1379              'Using a webview that supports only %s on a device that '
   1380              'supports %s. You probably need to set GN arg:\n\n'
   1381              '    enable_android_secondary_abi=true\n', ','.join(app_abis),
   1382              ','.join(device_abis))
   1383      else:  # No common supported ABIs between the device and app.
   1384        if device_primary_abi == 'x86':
   1385          target_cpu = 'x86'
   1386        elif device_primary_abi == 'x86_64':
   1387          target_cpu = 'x64'
   1388        elif device_primary_abi.startswith('arm64'):
   1389          target_cpu = 'arm64'
   1390        elif device_primary_abi.startswith('armeabi'):
   1391          target_cpu = 'arm'
   1392        else:
   1393          target_cpu = '<something else>'
   1394        # pylint: disable=line-too-long
   1395        native_lib_link = 'https://chromium.googlesource.com/chromium/src/+/main/docs/android_native_libraries.md'
   1396        not_supported_reasons[device.serial] = (
   1397            f"none of the app's ABIs ({','.join(app_abis)}) match this "
   1398            f"device's ABIs ({','.join(device_abis)}), you may need to set "
   1399            f'target_cpu="{target_cpu}" in your args.gn. If you already set '
   1400            'the target_cpu arg, you may need to use one of the _64 or _64_32 '
   1401            f'targets, see {native_lib_link} for more details.')
   1402    return fully_supported, not_supported_reasons
   1403 
   1404  def ProcessArgs(self, args):
   1405    self.args = args
   1406    # Ensure these keys always exist. They are set by wrapper scripts, but not
   1407    # always added when not using wrapper scripts.
   1408    args.__dict__.setdefault('apk_path', None)
   1409    args.__dict__.setdefault('incremental_json', None)
   1410 
   1411    self.incremental_apk_path = None
   1412    install_dict = None
   1413    if args.incremental_json and not (self.supports_incremental and
   1414                                      args.non_incremental):
   1415      with open(args.incremental_json) as f:
   1416        install_dict = json.load(f)
   1417        self.incremental_apk_path = os.path.join(args.output_directory,
   1418                                                 install_dict['apk_path'])
   1419        if not os.path.exists(self.incremental_apk_path):
   1420          self.incremental_apk_path = None
   1421 
   1422    if self.supports_incremental:
   1423      if args.incremental and args.non_incremental:
   1424        self._parser.error('Must use only one of --incremental and '
   1425                           '--non-incremental')
   1426      elif args.non_incremental:
   1427        if not args.apk_path:
   1428          self._parser.error('Apk has not been built.')
   1429      elif args.incremental:
   1430        if not self.incremental_apk_path:
   1431          self._parser.error('Incremental apk has not been built.')
   1432        args.apk_path = None
   1433 
   1434      if args.apk_path and self.incremental_apk_path:
   1435        self._parser.error('Both incremental and non-incremental apks exist. '
   1436                           'Select using --incremental or --non-incremental')
   1437 
   1438 
   1439    # Gate apk_helper creation with _CreateApkHelpers since for bundles it takes
   1440    # a while to unpack the apks file from the aab file, so avoid this slowdown
   1441    # for simple commands that don't need apk_helper.
   1442    if self.needs_apk_helper:
   1443      if not self._CreateApkHelpers(args, self.incremental_apk_path,
   1444                                    install_dict):
   1445        self._parser.error('App is not built.')
   1446 
   1447    if self.needs_package_name and not args.package_name:
   1448      if self._CreateApkHelpers(args, self.incremental_apk_path, install_dict):
   1449        args.package_name = self.apk_helper.GetPackageName()
   1450      elif self._from_wrapper_script:
   1451        self._parser.error('App is not built.')
   1452      else:
   1453        self._parser.error('One of --package-name or --apk-path is required.')
   1454 
   1455    self.devices = []
   1456    if self.need_device_args:
   1457      # Avoid filtering by ABIs with catapult since some x86 or x86_64 emulators
   1458      # can still work with the right target_cpu GN arg and the right targets.
   1459      # Doing this manually allows us to output more informative warnings to
   1460      # help devs towards the right course, see: https://crbug.com/1335139
   1461      available_devices = device_utils.DeviceUtils.HealthyDevices(
   1462          device_arg=args.devices,
   1463          enable_device_files_cache=bool(args.output_directory),
   1464          default_retries=0)
   1465      if not available_devices:
   1466        raise Exception('Cannot find any available devices.')
   1467 
   1468      if not self._CreateApkHelpers(args, self.incremental_apk_path,
   1469                                    install_dict):
   1470        self.devices = available_devices
   1471      else:
   1472        fully_supported, not_supported_reasons = self._FindSupportedDevices(
   1473            available_devices)
   1474        reason_string = '\n'.join(
   1475            'The device (serial={}) is not supported because {}'.format(
   1476                serial, reason)
   1477            for serial, reason in not_supported_reasons.items())
   1478        if args.devices:
   1479          if reason_string:
   1480            logging.warning('Devices not supported: %s', reason_string)
   1481          self.devices = available_devices
   1482        elif fully_supported:
   1483          self.devices = fully_supported
   1484        else:
   1485          raise Exception('Cannot find any supported devices for this app.\n\n'
   1486                          f'{reason_string}')
   1487 
   1488      # TODO(agrieve): Device cache should not depend on output directory.
   1489      #     Maybe put into /tmp?
   1490      _LoadDeviceCaches(self.devices, args.output_directory)
   1491 
   1492      try:
   1493        if len(self.devices) > 1:
   1494          if not self.supports_multiple_devices:
   1495            self._parser.error(device_errors.MultipleDevicesError(self.devices))
   1496          if not args.all and not args.devices:
   1497            self._parser.error(_GenerateMissingAllFlagMessage(self.devices))
   1498        # Save cache now if command will not get a chance to afterwards.
   1499        if self.calls_exec:
   1500          _SaveDeviceCaches(self.devices, args.output_directory)
   1501      except:
   1502        _SaveDeviceCaches(self.devices, args.output_directory)
   1503        raise
   1504 
   1505 
   1506 class _DevicesCommand(_Command):
   1507  name = 'devices'
   1508  description = 'Describe attached devices.'
   1509  all_devices_by_default = True
   1510 
   1511  def Run(self):
   1512    print(_GenerateAvailableDevicesMessage(self.devices))
   1513 
   1514 
   1515 class _PackageInfoCommand(_Command):
   1516  name = 'package-info'
   1517  description = 'Show various attributes of this app.'
   1518  need_device_args = False
   1519  needs_package_name = True
   1520  needs_apk_helper = True
   1521 
   1522  def Run(self):
   1523    # Format all (even ints) as strings, to handle cases where APIs return None
   1524    print('Package name: "%s"' % self.args.package_name)
   1525    print('versionCode: %s' % self.apk_helper.GetVersionCode())
   1526    print('versionName: "%s"' % self.apk_helper.GetVersionName())
   1527    print('minSdkVersion: %s' % self.apk_helper.GetMinSdkVersion())
   1528    print('targetSdkVersion: %s' % self.apk_helper.GetTargetSdkVersion())
   1529    print('Supported ABIs: %r' % self.apk_helper.GetAbis())
   1530 
   1531 
   1532 class _InstallCommand(_Command):
   1533  name = 'install'
   1534  description = 'Installs the APK or bundle to one or more devices.'
   1535  needs_package_name = True
   1536  needs_apk_helper = True
   1537  supports_incremental = True
   1538  default_modules = []
   1539 
   1540  def _RegisterExtraArgs(self, group):
   1541    if self.is_bundle:
   1542      group.add_argument(
   1543          '--locales',
   1544          action='append',
   1545          help=
   1546          'Locale splits to install (english is in base, so always installed).')
   1547      group.add_argument(
   1548          '-m',
   1549          '--module',
   1550          action='append',
   1551          default=self.default_modules,
   1552          help='Module to install. Can be specified multiple times.')
   1553      group.add_argument(
   1554          '-f',
   1555          '--fake',
   1556          action='append',
   1557          default=[],
   1558          help='Fake bundle module install. Can be specified multiple times. '
   1559          'Requires \'-m {0}\' to be given, and \'-f {0}\' is illegal.'.format(
   1560              BASE_MODULE))
   1561      # Add even if |self.default_modules| is empty, for consistency.
   1562      group.add_argument('--no-module',
   1563                         action='append',
   1564                         choices=self.default_modules,
   1565                         default=[],
   1566                         help='Module to exclude from default install.')
   1567 
   1568  def Run(self):
   1569    if self.additional_apk_helpers:
   1570      for additional_apk_helper in self.additional_apk_helpers:
   1571        _InstallApk(self.devices, additional_apk_helper, None)
   1572    if self.is_bundle:
   1573      modules = list(
   1574          set(self.args.module) - set(self.args.no_module) -
   1575          set(self.args.fake))
   1576      _InstallBundle(self.devices, self.apk_helper, modules, self.args.fake,
   1577                     self.args.locales)
   1578    else:
   1579      _InstallApk(self.devices, self.apk_helper, self.install_dict)
   1580    if self.args.is_official_build:
   1581      _RunCompileDex(self.devices, self.args.package_name, 'speed')
   1582 
   1583 
   1584 class _UninstallCommand(_Command):
   1585  name = 'uninstall'
   1586  description = 'Removes the APK or bundle from one or more devices.'
   1587  needs_package_name = True
   1588 
   1589  def Run(self):
   1590    _UninstallApk(self.devices, self.install_dict, self.args.package_name)
   1591 
   1592 
   1593 class _SetWebViewProviderCommand(_Command):
   1594  name = 'set-webview-provider'
   1595  description = ("Sets the device's WebView provider to this APK's "
   1596                 "package name.")
   1597  needs_package_name = True
   1598  needs_apk_helper = True
   1599 
   1600  def Run(self):
   1601    if not _IsWebViewProvider(self.apk_helper):
   1602      raise Exception('This package does not have a WebViewLibrary meta-data '
   1603                      'tag. Are you sure it contains a WebView implementation?')
   1604    _SetWebViewProvider(self.devices, self.args.package_name)
   1605 
   1606 
   1607 class _LaunchCommand(_Command):
   1608  name = 'launch'
   1609  description = ('Sends a launch intent for the APK or bundle after first '
   1610                 'writing the command-line flags file.')
   1611  needs_package_name = True
   1612  accepts_command_line_flags = True
   1613  all_devices_by_default = True
   1614 
   1615  def _RegisterExtraArgs(self, group):
   1616    group.add_argument('-w', '--wait-for-java-debugger', action='store_true',
   1617                       help='Pause execution until debugger attaches. Applies '
   1618                            'only to the main process. To have renderers wait, '
   1619                            'use --args="--renderer-wait-for-java-debugger"')
   1620    group.add_argument('--debug-process-name',
   1621                       help='Name of the process to debug. '
   1622                            'E.g. "privileged_process0", or "foo.bar:baz"')
   1623    group.add_argument('--nokill', action='store_true',
   1624                       help='Do not set the debug-app, nor set command-line '
   1625                            'flags. Useful to load a URL without having the '
   1626                             'app restart.')
   1627    group.add_argument('url', nargs='?', help='A URL to launch with.')
   1628 
   1629  def Run(self):
   1630    if self.is_test_apk:
   1631      raise Exception('Use the bin/run_* scripts to run test apks.')
   1632    _LaunchUrl(self.devices,
   1633               self.args.package_name,
   1634               argv=self.args.args,
   1635               command_line_flags_file=self.args.command_line_flags_file,
   1636               url=self.args.url,
   1637               wait_for_java_debugger=self.args.wait_for_java_debugger,
   1638               debug_process_name=self.args.debug_process_name,
   1639               nokill=self.args.nokill)
   1640 
   1641 
   1642 class _StopCommand(_Command):
   1643  name = 'stop'
   1644  description = 'Force-stops the app.'
   1645  needs_package_name = True
   1646  all_devices_by_default = True
   1647 
   1648  def Run(self):
   1649    device_utils.DeviceUtils.parallel(self.devices).ForceStop(
   1650        self.args.package_name)
   1651 
   1652 
   1653 class _ClearDataCommand(_Command):
   1654  name = 'clear-data'
   1655  descriptions = 'Clears all app data.'
   1656  needs_package_name = True
   1657  all_devices_by_default = True
   1658 
   1659  def Run(self):
   1660    device_utils.DeviceUtils.parallel(self.devices).ClearApplicationState(
   1661        self.args.package_name)
   1662 
   1663 
   1664 class _ArgvCommand(_Command):
   1665  name = 'argv'
   1666  description = 'Display and optionally update command-line flags file.'
   1667  needs_package_name = True
   1668  accepts_command_line_flags = True
   1669  all_devices_by_default = True
   1670 
   1671  def Run(self):
   1672    command_line_flags_file = self.args.command_line_flags_file
   1673    argv = self.args.args
   1674    devices = self.devices
   1675    parallel_devices = device_utils.DeviceUtils.parallel(devices)
   1676 
   1677    if argv is not None:
   1678      parallel_devices.pMap(
   1679          lambda d: _UpdateDeviceFlags(d, command_line_flags_file, argv))
   1680 
   1681    outputs = parallel_devices.pMap(
   1682        lambda d: _ReadDeviceFlags(d, command_line_flags_file) or '').pGet(None)
   1683 
   1684    print(f'Showing flags via /data/local/tmp/{command_line_flags_file}:')
   1685    for flags in _PrintPerDeviceOutput(devices, outputs, single_line=True):
   1686      print(flags or 'No flags set.')
   1687 
   1688 
   1689 class _GdbCommand(_Command):
   1690  name = 'gdb'
   1691  description = 'Runs //build/android/adb_gdb with apk-specific args.'
   1692  long_description = description + """
   1693 
   1694 To attach to a process other than the APK's main process, use --pid=1234.
   1695 To list all PIDs, use the "ps" command.
   1696 
   1697 If no apk process is currently running, sends a launch intent.
   1698 """
   1699  needs_package_name = True
   1700  needs_output_directory = True
   1701  calls_exec = True
   1702  supports_multiple_devices = False
   1703 
   1704  def Run(self):
   1705    _RunGdb(self.devices[0], self.args.package_name,
   1706            self.args.debug_process_name, self.args.pid,
   1707            self.args.output_directory, self.args.target_cpu, self.args.port,
   1708            self.args.ide, bool(self.args.verbose_count))
   1709 
   1710  def _RegisterExtraArgs(self, group):
   1711    pid_group = group.add_mutually_exclusive_group()
   1712    pid_group.add_argument('--debug-process-name',
   1713                           help='Name of the process to attach to. '
   1714                                'E.g. "privileged_process0", or "foo.bar:baz"')
   1715    pid_group.add_argument('--pid',
   1716                           help='The process ID to attach to. Defaults to '
   1717                                'the main process for the package.')
   1718    group.add_argument('--ide', action='store_true',
   1719                       help='Rather than enter a gdb prompt, set up the '
   1720                            'gdb connection and wait for an IDE to '
   1721                            'connect.')
   1722    # Same default port that ndk-gdb.py uses.
   1723    group.add_argument('--port', type=int, default=5039,
   1724                       help='Use the given port for the GDB connection')
   1725 
   1726 
   1727 class _LldbCommand(_Command):
   1728  name = 'lldb'
   1729  description = 'Runs //build/android/connect_lldb.sh with apk-specific args.'
   1730  long_description = description + """
   1731 
   1732 To attach to a process other than the APK's main process, use --pid=1234.
   1733 To list all PIDs, use the "ps" command.
   1734 
   1735 If no apk process is currently running, sends a launch intent.
   1736 """
   1737  needs_package_name = True
   1738  needs_output_directory = True
   1739  calls_exec = True
   1740  supports_multiple_devices = False
   1741 
   1742  def Run(self):
   1743    _RunLldb(device=self.devices[0],
   1744             package_name=self.args.package_name,
   1745             debug_process_name=self.args.debug_process_name,
   1746             pid=self.args.pid,
   1747             output_directory=self.args.output_directory,
   1748             port=self.args.port,
   1749             target_cpu=self.args.target_cpu,
   1750             ndk_dir=self.args.ndk_dir,
   1751             lldb_server=self.args.lldb_server,
   1752             lldb=self.args.lldb,
   1753             verbose=bool(self.args.verbose_count))
   1754 
   1755  def _RegisterExtraArgs(self, group):
   1756    pid_group = group.add_mutually_exclusive_group()
   1757    pid_group.add_argument('--debug-process-name',
   1758                           help='Name of the process to attach to. '
   1759                           'E.g. "privileged_process0", or "foo.bar:baz"')
   1760    pid_group.add_argument('--pid',
   1761                           help='The process ID to attach to. Defaults to '
   1762                           'the main process for the package.')
   1763    group.add_argument('--ndk-dir',
   1764                       help='Select alternative NDK root directory.')
   1765    group.add_argument('--lldb-server',
   1766                       help='Select alternative on-device lldb-server.')
   1767    group.add_argument('--lldb', help='Select alternative client lldb.sh.')
   1768    # Same default port that ndk-gdb.py uses.
   1769    group.add_argument('--port',
   1770                       type=int,
   1771                       default=5039,
   1772                       help='Use the given port for the LLDB connection')
   1773 
   1774 
   1775 class _LogcatCommand(_Command):
   1776  name = 'logcat'
   1777  description = 'Runs "adb logcat" with filters relevant the current APK.'
   1778  long_description = description + """
   1779 
   1780 "Relevant filters" means:
   1781  * Log messages from processes belonging to the apk,
   1782  * Plus log messages from log tags: ActivityManager|DEBUG,
   1783  * Plus fatal logs from any process,
   1784  * Minus spamy dalvikvm logs (for pre-L devices).
   1785 
   1786 Colors:
   1787  * Primary process is white
   1788  * Other processes (gpu, renderer) are yellow
   1789  * Non-apk processes are grey
   1790  * UI thread has a bolded Thread-ID
   1791 
   1792 Java stack traces are detected and deobfuscated (for release builds).
   1793 
   1794 To disable filtering, (but keep coloring), use --verbose.
   1795 """
   1796  needs_package_name = True
   1797  supports_multiple_devices = False
   1798 
   1799  def Run(self):
   1800    deobfuscate = None
   1801    if self.args.proguard_mapping_path and not self.args.no_deobfuscate:
   1802      deobfuscate = deobfuscator.Deobfuscator(self.args.proguard_mapping_path)
   1803    apk_path = self.args.apk_path or self.incremental_apk_path
   1804    if apk_path or self.bundle_generation_info:
   1805      stack_script_context = _StackScriptContext(self.args.output_directory,
   1806                                                 apk_path,
   1807                                                 self.bundle_generation_info,
   1808                                                 quiet=True)
   1809    else:
   1810      stack_script_context = None
   1811 
   1812    extra_package_names = []
   1813    if self.is_test_apk and self.additional_apk_helpers:
   1814      for additional_apk_helper in self.additional_apk_helpers:
   1815        extra_package_names.append(additional_apk_helper.GetPackageName())
   1816 
   1817    try:
   1818      _RunLogcat(self.devices[0],
   1819                 self.args.package_name,
   1820                 stack_script_context,
   1821                 deobfuscate,
   1822                 bool(self.args.verbose_count),
   1823                 self.args.exit_on_match,
   1824                 extra_package_names=extra_package_names)
   1825    except KeyboardInterrupt:
   1826      pass  # Don't show stack trace upon Ctrl-C
   1827    finally:
   1828      if stack_script_context:
   1829        stack_script_context.Close()
   1830      if deobfuscate:
   1831        deobfuscate.Close()
   1832 
   1833  def _RegisterExtraArgs(self, group):
   1834    if self._from_wrapper_script:
   1835      group.add_argument('--no-deobfuscate', action='store_true',
   1836          help='Disables ProGuard deobfuscation of logcat.')
   1837    else:
   1838      group.set_defaults(no_deobfuscate=False)
   1839      group.add_argument('--proguard-mapping-path',
   1840          help='Path to ProGuard map (enables deobfuscation)')
   1841    group.add_argument('--exit-on-match',
   1842                       help='Exits logcat when a message matches this regex.')
   1843 
   1844 
   1845 class _PsCommand(_Command):
   1846  name = 'ps'
   1847  description = 'Show PIDs of any APK processes currently running.'
   1848  needs_package_name = True
   1849  all_devices_by_default = True
   1850 
   1851  def Run(self):
   1852    _RunPs(self.devices, self.args.package_name)
   1853 
   1854 
   1855 class _DiskUsageCommand(_Command):
   1856  name = 'disk-usage'
   1857  description = 'Show how much device storage is being consumed by the app.'
   1858  needs_package_name = True
   1859  all_devices_by_default = True
   1860 
   1861  def Run(self):
   1862    _RunDiskUsage(self.devices, self.args.package_name)
   1863 
   1864 
   1865 class _MemUsageCommand(_Command):
   1866  name = 'mem-usage'
   1867  description = 'Show memory usage of currently running APK processes.'
   1868  needs_package_name = True
   1869  all_devices_by_default = True
   1870 
   1871  def _RegisterExtraArgs(self, group):
   1872    group.add_argument('--query-app', action='store_true',
   1873        help='Do not add --local to "dumpsys meminfo". This will output '
   1874             'additional metrics (e.g. Context count), but also cause memory '
   1875             'to be used in order to gather the metrics.')
   1876 
   1877  def Run(self):
   1878    _RunMemUsage(self.devices, self.args.package_name,
   1879                 query_app=self.args.query_app)
   1880 
   1881 
   1882 class _ShellCommand(_Command):
   1883  name = 'shell'
   1884  description = ('Same as "adb shell <command>", but runs as the apk\'s uid '
   1885                 '(via run-as). Useful for inspecting the app\'s data '
   1886                 'directory.')
   1887  needs_package_name = True
   1888 
   1889  @property
   1890  def calls_exec(self):
   1891    return not self.args.cmd
   1892 
   1893  @property
   1894  def supports_multiple_devices(self):
   1895    return not self.args.cmd
   1896 
   1897  def _RegisterExtraArgs(self, group):
   1898    group.add_argument(
   1899        'cmd', nargs=argparse.REMAINDER, help='Command to run.')
   1900 
   1901  def Run(self):
   1902    _RunShell(self.devices, self.args.package_name, self.args.cmd)
   1903 
   1904 
   1905 class _CompileDexCommand(_Command):
   1906  name = 'compile-dex'
   1907  description = ('Applicable only for Android N+. Forces .odex files to be '
   1908                 'compiled with the given compilation filter. To see existing '
   1909                 'filter, use "disk-usage" command.')
   1910  needs_package_name = True
   1911  all_devices_by_default = True
   1912 
   1913  def _RegisterExtraArgs(self, group):
   1914    group.add_argument(
   1915        'compilation_filter',
   1916        choices=['verify', 'quicken', 'space-profile', 'space',
   1917                 'speed-profile', 'speed'],
   1918        help='For WebView/Monochrome, use "speed". For other apks, use '
   1919             '"speed-profile".')
   1920 
   1921  def Run(self):
   1922    outputs = _RunCompileDex(self.devices, self.args.package_name,
   1923                             self.args.compilation_filter)
   1924    for output in _PrintPerDeviceOutput(self.devices, outputs):
   1925      for line in output:
   1926        print(line)
   1927 
   1928 
   1929 class _PrintCertsCommand(_Command):
   1930  name = 'print-certs'
   1931  description = 'Print info about certificates used to sign this APK.'
   1932  need_device_args = False
   1933  needs_apk_helper = True
   1934 
   1935  def _RegisterExtraArgs(self, group):
   1936    group.add_argument(
   1937        '--full-cert',
   1938        action='store_true',
   1939        help=("Print the certificate's full signature, Base64-encoded. "
   1940              "Useful when configuring an Android image's "
   1941              "config_webview_packages.xml."))
   1942 
   1943  def Run(self):
   1944    keytool = os.path.join(_JAVA_HOME, 'bin', 'keytool')
   1945    pem_certificate_pattern = re.compile(
   1946        r'-+BEGIN CERTIFICATE-+([\r\n0-9A-Za-z+/=]+)-+END CERTIFICATE-+[\r\n]*')
   1947    if self.is_bundle:
   1948      # Bundles are not signed until converted to .apks. The wrapper scripts
   1949      # record which key will be used to sign though.
   1950      with tempfile.NamedTemporaryFile() as f:
   1951        logging.warning('Bundles are not signed until turned into .apk files.')
   1952        logging.warning('Showing signing info based on associated keystore.')
   1953        cmd = [
   1954            keytool, '-exportcert', '-keystore',
   1955            self.bundle_generation_info.keystore_path, '-storepass',
   1956            self.bundle_generation_info.keystore_password, '-alias',
   1957            self.bundle_generation_info.keystore_alias, '-file', f.name
   1958        ]
   1959        logging.warning('Running: %s', shlex.join(cmd))
   1960        subprocess.check_output(cmd, stderr=subprocess.STDOUT)
   1961        cmd = [keytool, '-printcert', '-file', f.name]
   1962        logging.warning('Running: %s', shlex.join(cmd))
   1963        subprocess.check_call(cmd)
   1964        if self.args.full_cert:
   1965          # Redirect stderr to hide a keytool warning about using non-standard
   1966          # keystore format.
   1967          cmd += ['-rfc']
   1968          logging.warning('Running: %s', shlex.join(cmd))
   1969          pem_encoded_certificate = subprocess.check_output(
   1970              cmd, stderr=subprocess.STDOUT).decode()
   1971    else:
   1972 
   1973      def run_apksigner(min_sdk_version):
   1974        cmd = [
   1975            build_tools.GetPath('apksigner'), 'verify', '--min-sdk-version',
   1976            str(min_sdk_version), '--print-certs-pem', '--verbose',
   1977            self.apk_helper.path
   1978        ]
   1979        logging.warning('Running: %s', shlex.join(cmd))
   1980        env = os.environ.copy()
   1981        env['PATH'] = os.path.pathsep.join(
   1982            [os.path.join(_JAVA_HOME, 'bin'),
   1983             env.get('PATH')])
   1984        # Redirect stderr to hide verification failures (see explanation below).
   1985        return subprocess.check_output(cmd,
   1986                                       env=env,
   1987                                       universal_newlines=True,
   1988                                       stderr=subprocess.STDOUT)
   1989 
   1990      # apksigner's default behavior is nonintuitive: it will print "Verified
   1991      # using <scheme number>...: false" for any scheme which is obsolete for
   1992      # the APK's minSdkVersion even if it actually was signed with that scheme
   1993      # (ex. it prints "Verified using v1 scheme: false" for Monochrome because
   1994      # v1 was obsolete by N). To workaround this, we force apksigner to use the
   1995      # lowest possible minSdkVersion. We need to fallback to higher
   1996      # minSdkVersions in case the APK fails to verify for that minSdkVersion
   1997      # (which means the APK is genuinely not signed with that scheme). These
   1998      # SDK values are the highest SDK version before the next scheme is
   1999      # available:
   2000      versions = [
   2001          version_codes.MARSHMALLOW,  # before v2 launched in N
   2002          version_codes.OREO_MR1,  # before v3 launched in P
   2003          version_codes.Q,  # before v4 launched in R
   2004          version_codes.R,
   2005      ]
   2006      stdout = None
   2007      for min_sdk_version in versions:
   2008        try:
   2009          stdout = run_apksigner(min_sdk_version)
   2010          break
   2011        except subprocess.CalledProcessError:
   2012          # Doesn't verify with this min-sdk-version, so try again with a higher
   2013          # one
   2014          continue
   2015      if not stdout:
   2016        raise RuntimeError('apksigner was not able to verify APK')
   2017 
   2018      # Separate what the '--print-certs' flag would output vs. the additional
   2019      # signature output included by '--print-certs-pem'. The additional PEM
   2020      # output is only printed when self.args.full_cert is specified.
   2021      verification_hash_info = pem_certificate_pattern.sub('', stdout)
   2022      print(verification_hash_info)
   2023      if self.args.full_cert:
   2024        m = pem_certificate_pattern.search(stdout)
   2025        if not m:
   2026          raise Exception('apksigner did not print a certificate')
   2027        pem_encoded_certificate = m.group(0)
   2028 
   2029 
   2030    if self.args.full_cert:
   2031      m = pem_certificate_pattern.search(pem_encoded_certificate)
   2032      if not m:
   2033        raise Exception(
   2034            'Unable to parse certificate:\n{}'.format(pem_encoded_certificate))
   2035      signature = re.sub(r'[\r\n]+', '', m.group(1))
   2036      print()
   2037      print('Full Signature:')
   2038      print(signature)
   2039 
   2040 
   2041 class _ProfileCommand(_Command):
   2042  name = 'profile'
   2043  description = ('Run the simpleperf sampling CPU profiler on the currently-'
   2044                 'running APK. If --args is used, the extra arguments will be '
   2045                 'passed on to simpleperf; otherwise, the following default '
   2046                 'arguments are used: -g -f 1000 -o /data/local/tmp/perf.data')
   2047  needs_package_name = True
   2048  needs_output_directory = True
   2049  supports_multiple_devices = False
   2050  accepts_args = True
   2051 
   2052  def _RegisterExtraArgs(self, group):
   2053    group.add_argument(
   2054        '--profile-process', default='browser',
   2055        help=('Which process to profile. This may be a process name or pid '
   2056              'such as you would get from running `%s ps`; or '
   2057              'it can be one of (browser, renderer, gpu).' % sys.argv[0]))
   2058    group.add_argument(
   2059        '--profile-thread', default=None,
   2060        help=('(Optional) Profile only a single thread. This may be either a '
   2061              'thread ID such as you would get by running `adb shell ps -t` '
   2062              '(pre-Oreo) or `adb shell ps -e -T` (Oreo and later); or it may '
   2063              'be one of (io, compositor, main, render), in which case '
   2064              '--profile-process is also required. (Note that "render" thread '
   2065              'refers to a thread in the browser process that manages a '
   2066              'renderer; to profile the main thread of the renderer process, '
   2067              'use --profile-thread=main).'))
   2068    group.add_argument('--profile-output', default='profile.pb',
   2069                       help='Output file for profiling data')
   2070    group.add_argument('--profile-events', default='cpu-cycles',
   2071                      help=('A comma separated list of perf events to capture '
   2072                      '(e.g. \'cpu-cycles,branch-misses\'). Run '
   2073                      '`simpleperf list` on your device to see available '
   2074                      'events.'))
   2075 
   2076  def Run(self):
   2077    extra_args = shlex.split(self.args.args or '')
   2078    _RunProfile(self.devices[0], self.args.package_name,
   2079                self.args.output_directory, self.args.profile_output,
   2080                self.args.profile_process, self.args.profile_thread,
   2081                self.args.profile_events, extra_args)
   2082 
   2083 
   2084 class _RunCommand(_InstallCommand, _LaunchCommand, _LogcatCommand):
   2085  name = 'run'
   2086  description = 'Install, launch, and show logcat (when targeting one device).'
   2087  all_devices_by_default = False
   2088  supports_multiple_devices = True
   2089 
   2090  def _RegisterExtraArgs(self, group):
   2091    _InstallCommand._RegisterExtraArgs(self, group)
   2092    _LaunchCommand._RegisterExtraArgs(self, group)
   2093    _LogcatCommand._RegisterExtraArgs(self, group)
   2094    group.add_argument('--no-logcat', action='store_true',
   2095                       help='Install and launch, but do not enter logcat.')
   2096 
   2097  def Run(self):
   2098    if self.is_test_apk:
   2099      raise Exception('Use the bin/run_* scripts to run test apks.')
   2100    logging.warning('Installing...')
   2101    _InstallCommand.Run(self)
   2102    logging.warning('Sending launch intent...')
   2103    _LaunchCommand.Run(self)
   2104    if len(self.devices) == 1 and not self.args.no_logcat:
   2105      logging.warning('Entering logcat...')
   2106      _LogcatCommand.Run(self)
   2107 
   2108 
   2109 class _BuildBundleApks(_Command):
   2110  name = 'build-bundle-apks'
   2111  description = ('Build the .apks archive from an Android app bundle, and '
   2112                 'optionally copy it to a specific destination.')
   2113  need_device_args = False
   2114 
   2115  def _RegisterExtraArgs(self, group):
   2116    group.add_argument(
   2117        '--output-apks', required=True, help='Destination path for .apks file.')
   2118    group.add_argument(
   2119        '--minimal',
   2120        action='store_true',
   2121        help='Build .apks archive that targets the bundle\'s minSdkVersion and '
   2122        'contains only english splits. It still contains optional splits.')
   2123    group.add_argument(
   2124        '--sdk-version', help='The sdkVersion to build the .apks for.')
   2125    group.add_argument(
   2126        '--build-mode',
   2127        choices=app_bundle_utils.BUILD_APKS_MODES,
   2128        help='Specify which type of APKs archive to build. "default" '
   2129        'generates regular splits, "universal" generates an archive with a '
   2130        'single universal APK, "system" generates an archive with a system '
   2131        'image APK, while "system_compressed" generates a compressed system '
   2132        'APK, with an additional stub APK for the system image.')
   2133    group.add_argument(
   2134        '--optimize-for',
   2135        choices=app_bundle_utils.OPTIMIZE_FOR_OPTIONS,
   2136        help='Override split configuration.')
   2137 
   2138  def Run(self):
   2139    _GenerateBundleApks(
   2140        self.bundle_generation_info,
   2141        output_path=self.args.output_apks,
   2142        minimal=self.args.minimal,
   2143        minimal_sdk_version=self.args.sdk_version,
   2144        mode=self.args.build_mode,
   2145        optimize_for=self.args.optimize_for)
   2146 
   2147 
   2148 class _ManifestCommand(_Command):
   2149  name = 'dump-manifest'
   2150  description = 'Dump the android manifest as XML, to stdout.'
   2151  need_device_args = False
   2152  needs_apk_helper = True
   2153 
   2154  def Run(self):
   2155    if self.is_bundle:
   2156      sys.stdout.write(
   2157          bundletool.RunBundleTool([
   2158              'dump', 'manifest', '--bundle',
   2159              self.bundle_generation_info.bundle_path
   2160          ]))
   2161    else:
   2162      apkanalyzer = os.path.join(_DIR_SOURCE_ROOT, 'third_party', 'android_sdk',
   2163                                 'public', 'cmdline-tools', 'latest', 'bin',
   2164                                 'apkanalyzer')
   2165      cmd = [apkanalyzer, 'manifest', 'print', self.apk_helper.path]
   2166      logging.info('Running: %s', shlex.join(cmd))
   2167      subprocess.check_call(cmd)
   2168 
   2169 
   2170 class _StackCommand(_Command):
   2171  name = 'stack'
   2172  description = 'Decodes an Android stack.'
   2173  need_device_args = False
   2174 
   2175  def _RegisterExtraArgs(self, group):
   2176    group.add_argument(
   2177        'file',
   2178        nargs='?',
   2179        help='File to decode. If not specified, stdin is processed.')
   2180 
   2181  def Run(self):
   2182    apk_path = self.args.apk_path or self.incremental_apk_path
   2183    context = _StackScriptContext(self.args.output_directory, apk_path,
   2184                                  self.bundle_generation_info)
   2185    try:
   2186      proc = context.Popen(input_file=self.args.file)
   2187      if proc.wait():
   2188        raise Exception('stack script returned {}'.format(proc.returncode))
   2189    finally:
   2190      context.Close()
   2191 
   2192 
   2193 # Shared commands for regular APKs and app bundles.
   2194 _COMMANDS = [
   2195    _DevicesCommand,
   2196    _PackageInfoCommand,
   2197    _InstallCommand,
   2198    _UninstallCommand,
   2199    _SetWebViewProviderCommand,
   2200    _LaunchCommand,
   2201    _StopCommand,
   2202    _ClearDataCommand,
   2203    _ArgvCommand,
   2204    _GdbCommand,
   2205    _LldbCommand,
   2206    _LogcatCommand,
   2207    _PsCommand,
   2208    _DiskUsageCommand,
   2209    _MemUsageCommand,
   2210    _ShellCommand,
   2211    _CompileDexCommand,
   2212    _PrintCertsCommand,
   2213    _ProfileCommand,
   2214    _RunCommand,
   2215    _StackCommand,
   2216    _ManifestCommand,
   2217 ]
   2218 
   2219 # Commands specific to app bundles.
   2220 _BUNDLE_COMMANDS = [
   2221    _BuildBundleApks,
   2222 ]
   2223 
   2224 
   2225 def _ParseArgs(parser, from_wrapper_script, is_bundle, is_test_apk):
   2226  subparsers = parser.add_subparsers()
   2227  command_list = _COMMANDS + (_BUNDLE_COMMANDS if is_bundle else [])
   2228  commands = [
   2229      clazz(from_wrapper_script, is_bundle, is_test_apk)
   2230      for clazz in command_list
   2231  ]
   2232 
   2233  for command in commands:
   2234    if from_wrapper_script or not command.needs_output_directory:
   2235      command.RegisterArgs(subparsers)
   2236 
   2237  # Show extended help when no command is passed.
   2238  argv = sys.argv[1:]
   2239  if not argv:
   2240    argv = ['--help']
   2241 
   2242  return parser.parse_args(argv)
   2243 
   2244 
   2245 def _RunInternal(parser,
   2246                 output_directory=None,
   2247                 additional_apk_paths=None,
   2248                 bundle_generation_info=None,
   2249                 is_test_apk=False):
   2250  colorama.init()
   2251  parser.set_defaults(
   2252      additional_apk_paths=additional_apk_paths,
   2253      output_directory=output_directory)
   2254  from_wrapper_script = bool(output_directory)
   2255  args = _ParseArgs(parser,
   2256                    from_wrapper_script,
   2257                    is_bundle=bool(bundle_generation_info),
   2258                    is_test_apk=is_test_apk)
   2259  run_tests_helper.SetLogLevel(args.verbose_count)
   2260  if bundle_generation_info:
   2261    args.command.RegisterBundleGenerationInfo(bundle_generation_info)
   2262  if args.additional_apk_paths:
   2263    for i, path in enumerate(args.additional_apk_paths):
   2264      if path and not os.path.exists(path):
   2265        inc_path = path.replace('.apk', '_incremental.apk')
   2266        if os.path.exists(inc_path):
   2267          args.additional_apk_paths[i] = inc_path
   2268          path = inc_path
   2269      if not path or not os.path.exists(path):
   2270        raise Exception('Invalid additional APK path "{}"'.format(path))
   2271  args.command.ProcessArgs(args)
   2272  args.command.Run()
   2273  # Incremental install depends on the cache being cleared when uninstalling.
   2274  if args.command.name != 'uninstall':
   2275    _SaveDeviceCaches(args.command.devices, output_directory)
   2276 
   2277 
   2278 def Run(output_directory, apk_path, additional_apk_paths, incremental_json,
   2279        command_line_flags_file, target_cpu, proguard_mapping_path):
   2280  """Entry point for generated wrapper scripts."""
   2281  constants.SetOutputDirectory(output_directory)
   2282  devil_chromium.Initialize(output_directory=output_directory)
   2283  parser = argparse.ArgumentParser()
   2284  exists_or_none = lambda p: p if p and os.path.exists(p) else None
   2285 
   2286  parser.set_defaults(
   2287      command_line_flags_file=command_line_flags_file,
   2288      target_cpu=target_cpu,
   2289      apk_path=exists_or_none(apk_path),
   2290      incremental_json=exists_or_none(incremental_json),
   2291      proguard_mapping_path=proguard_mapping_path)
   2292  _RunInternal(
   2293      parser,
   2294      output_directory=output_directory,
   2295      additional_apk_paths=additional_apk_paths)
   2296 
   2297 
   2298 def RunForBundle(output_directory, bundle_path, bundle_apks_path,
   2299                 additional_apk_paths, aapt2_path, keystore_path,
   2300                 keystore_password, keystore_alias, package_name,
   2301                 command_line_flags_file, proguard_mapping_path, target_cpu,
   2302                 system_image_locales, default_modules, is_official_build):
   2303  """Entry point for generated app bundle wrapper scripts.
   2304 
   2305  Args:
   2306    output_dir: Chromium output directory path.
   2307    bundle_path: Input bundle path.
   2308    bundle_apks_path: Output bundle .apks archive path.
   2309    additional_apk_paths: Additional APKs to install prior to bundle install.
   2310    aapt2_path: Aapt2 tool path.
   2311    keystore_path: Keystore file path.
   2312    keystore_password: Keystore password.
   2313    keystore_alias: Signing key name alias in keystore file.
   2314    package_name: Application's package name.
   2315    command_line_flags_file: Optional. Name of an on-device file that will be
   2316      used to store command-line flags for this bundle.
   2317    proguard_mapping_path: Input path to the Proguard mapping file, used to
   2318      deobfuscate Java stack traces.
   2319    target_cpu: Chromium target CPU name, used by the 'gdb' command.
   2320    system_image_locales: List of Chromium locales that should be included in
   2321      system image APKs.
   2322    default_modules: List of modules that are installed in addition to those
   2323      given by the '-m' switch.
   2324  """
   2325  constants.SetOutputDirectory(output_directory)
   2326  devil_chromium.Initialize(output_directory=output_directory)
   2327  bundle_generation_info = BundleGenerationInfo(
   2328      bundle_path=bundle_path,
   2329      bundle_apks_path=bundle_apks_path,
   2330      aapt2_path=aapt2_path,
   2331      keystore_path=keystore_path,
   2332      keystore_password=keystore_password,
   2333      keystore_alias=keystore_alias,
   2334      system_image_locales=system_image_locales)
   2335  _InstallCommand.default_modules = default_modules
   2336 
   2337  parser = argparse.ArgumentParser()
   2338  parser.set_defaults(package_name=package_name,
   2339                      command_line_flags_file=command_line_flags_file,
   2340                      proguard_mapping_path=proguard_mapping_path,
   2341                      target_cpu=target_cpu,
   2342                      is_official_build=is_official_build)
   2343  _RunInternal(parser,
   2344               output_directory=output_directory,
   2345               additional_apk_paths=additional_apk_paths,
   2346               bundle_generation_info=bundle_generation_info)
   2347 
   2348 
   2349 def RunForTestApk(*, output_directory, package_name, test_apk_path,
   2350                  test_apk_json, proguard_mapping_path, additional_apk_paths):
   2351  """Entry point for generated test apk wrapper scripts.
   2352 
   2353  This is intended to make commands like logcat (with proguard deobfuscation)
   2354  available. The run_* scripts should be used to actually run tests.
   2355 
   2356  Args:
   2357    output_dir: Chromium output directory path.
   2358    package_name: The package name for the test apk.
   2359    test_apk_path: The test apk to install.
   2360    test_apk_json: The incremental json dict for the test apk.
   2361    proguard_mapping_path: Input path to the Proguard mapping file, used to
   2362      deobfuscate Java stack traces.
   2363    additional_apk_paths: Additional APKs to install.
   2364  """
   2365  constants.SetOutputDirectory(output_directory)
   2366  devil_chromium.Initialize(output_directory=output_directory)
   2367 
   2368  parser = argparse.ArgumentParser()
   2369  exists_or_none = lambda p: p if p and os.path.exists(p) else None
   2370 
   2371  parser.set_defaults(apk_path=exists_or_none(test_apk_path),
   2372                      incremental_json=exists_or_none(test_apk_json),
   2373                      package_name=package_name,
   2374                      proguard_mapping_path=proguard_mapping_path)
   2375 
   2376  _RunInternal(parser,
   2377               output_directory=output_directory,
   2378               additional_apk_paths=additional_apk_paths,
   2379               is_test_apk=True)
   2380 
   2381 
   2382 def main():
   2383  devil_chromium.Initialize()
   2384  _RunInternal(argparse.ArgumentParser())
   2385 
   2386 
   2387 if __name__ == '__main__':
   2388  main()