xref: /aosp_15_r20/external/angle/build/android/apk_operations.py (revision 8975f5c5ed3d1c378011245431ada316dfb6f244)
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
10import argparse
11import collections
12import json
13import logging
14import os
15import posixpath
16import random
17import re
18import shlex
19import shutil
20import subprocess
21import sys
22import tempfile
23import textwrap
24import zipfile
25
26import adb_command_line
27import devil_chromium
28from devil import devil_env
29from devil.android import apk_helper
30from devil.android import device_errors
31from devil.android import device_utils
32from devil.android.sdk import adb_wrapper
33from devil.android.sdk import build_tools
34from devil.android.sdk import intent
35from devil.android.sdk import version_codes
36from 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
42with devil_env.SysPath(
43    os.path.join(_DIR_SOURCE_ROOT, 'third_party', 'colorama', 'src')):
44  import colorama
45
46from incremental_install import installer
47from pylib import constants
48from pylib.symbols import deobfuscator
49from pylib.utils import simpleperf
50from pylib.utils import app_bundle_utils
51
52with devil_env.SysPath(
53    os.path.join(_DIR_SOURCE_ROOT, 'build', 'android', 'gyp')):
54  import bundletool
55
56BASE_MODULE = 'base'
57
58
59def _Colorize(text, style=''):
60  return (style
61      + text
62      + colorama.Style.RESET_ALL)
63
64
65def _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.
86BundleGenerationInfo = collections.namedtuple(
87    'BundleGenerationInfo',
88    'bundle_path,bundle_apks_path,aapt2_path,keystore_path,keystore_password,'
89    'keystore_alias,system_image_locales')
90
91
92def _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
125def _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
156def _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
165def _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
171def _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
183def _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
193def _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
260def _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
275def _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
303def _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
350def _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
358def _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
391def _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
438def _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
449def _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
473def _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
520def _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@[email protected]@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@[email protected]@[email protected]
541  #  /data/dalvik-cache/arm/data@[email protected]@[email protected]
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
674class _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
974def _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
1003def _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
1011def _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
1026def _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
1047def _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
1054def _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
1083class _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
1145def _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.
1155def _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
1160def _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
1167def _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
1172def _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
1187def _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
1197class _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      else:  # No common supported ABIs between the device and app.
1378        if device_primary_abi == 'x86':
1379          target_cpu = 'x86'
1380        elif device_primary_abi == 'x86_64':
1381          target_cpu = 'x64'
1382        elif device_primary_abi.startswith('arm64'):
1383          target_cpu = 'arm64'
1384        elif device_primary_abi.startswith('armeabi'):
1385          target_cpu = 'arm'
1386        else:
1387          target_cpu = '<something else>'
1388        # pylint: disable=line-too-long
1389        native_lib_link = 'https://chromium.googlesource.com/chromium/src/+/main/docs/android_native_libraries.md'
1390        not_supported_reasons[device.serial] = (
1391            f"none of the app's ABIs ({','.join(app_abis)}) match this "
1392            f"device's ABIs ({','.join(device_abis)}), you may need to set "
1393            f'target_cpu="{target_cpu}" in your args.gn. If you already set '
1394            'the target_cpu arg, you may need to use one of the _64 or _64_32 '
1395            f'targets, see {native_lib_link} for more details.')
1396    return fully_supported, not_supported_reasons
1397
1398  def ProcessArgs(self, args):
1399    self.args = args
1400    # Ensure these keys always exist. They are set by wrapper scripts, but not
1401    # always added when not using wrapper scripts.
1402    args.__dict__.setdefault('apk_path', None)
1403    args.__dict__.setdefault('incremental_json', None)
1404
1405    self.incremental_apk_path = None
1406    install_dict = None
1407    if args.incremental_json and not (self.supports_incremental and
1408                                      args.non_incremental):
1409      with open(args.incremental_json) as f:
1410        install_dict = json.load(f)
1411        self.incremental_apk_path = os.path.join(args.output_directory,
1412                                                 install_dict['apk_path'])
1413        if not os.path.exists(self.incremental_apk_path):
1414          self.incremental_apk_path = None
1415
1416    if self.supports_incremental:
1417      if args.incremental and args.non_incremental:
1418        self._parser.error('Must use only one of --incremental and '
1419                           '--non-incremental')
1420      elif args.non_incremental:
1421        if not args.apk_path:
1422          self._parser.error('Apk has not been built.')
1423      elif args.incremental:
1424        if not self.incremental_apk_path:
1425          self._parser.error('Incremental apk has not been built.')
1426        args.apk_path = None
1427
1428      if args.apk_path and self.incremental_apk_path:
1429        self._parser.error('Both incremental and non-incremental apks exist. '
1430                           'Select using --incremental or --non-incremental')
1431
1432
1433    # Gate apk_helper creation with _CreateApkHelpers since for bundles it takes
1434    # a while to unpack the apks file from the aab file, so avoid this slowdown
1435    # for simple commands that don't need apk_helper.
1436    if self.needs_apk_helper:
1437      if not self._CreateApkHelpers(args, self.incremental_apk_path,
1438                                    install_dict):
1439        self._parser.error('App is not built.')
1440
1441    if self.needs_package_name and not args.package_name:
1442      if self._CreateApkHelpers(args, self.incremental_apk_path, install_dict):
1443        args.package_name = self.apk_helper.GetPackageName()
1444      elif self._from_wrapper_script:
1445        self._parser.error('App is not built.')
1446      else:
1447        self._parser.error('One of --package-name or --apk-path is required.')
1448
1449    self.devices = []
1450    if self.need_device_args:
1451      # Avoid filtering by ABIs with catapult since some x86 or x86_64 emulators
1452      # can still work with the right target_cpu GN arg and the right targets.
1453      # Doing this manually allows us to output more informative warnings to
1454      # help devs towards the right course, see: https://crbug.com/1335139
1455      available_devices = device_utils.DeviceUtils.HealthyDevices(
1456          device_arg=args.devices,
1457          enable_device_files_cache=bool(args.output_directory),
1458          default_retries=0)
1459      if not available_devices:
1460        raise Exception('Cannot find any available devices.')
1461
1462      if not self._CreateApkHelpers(args, self.incremental_apk_path,
1463                                    install_dict):
1464        self.devices = available_devices
1465      else:
1466        fully_supported, not_supported_reasons = self._FindSupportedDevices(
1467            available_devices)
1468        reason_string = '\n'.join(
1469            'The device (serial={}) is not supported because {}'.format(
1470                serial, reason)
1471            for serial, reason in not_supported_reasons.items())
1472        if args.devices:
1473          if reason_string:
1474            logging.warning('Devices not supported: %s', reason_string)
1475          self.devices = available_devices
1476        elif fully_supported:
1477          self.devices = fully_supported
1478        else:
1479          raise Exception('Cannot find any supported devices for this app.\n\n'
1480                          f'{reason_string}')
1481
1482      # TODO(agrieve): Device cache should not depend on output directory.
1483      #     Maybe put into /tmp?
1484      _LoadDeviceCaches(self.devices, args.output_directory)
1485
1486      try:
1487        if len(self.devices) > 1:
1488          if not self.supports_multiple_devices:
1489            self._parser.error(device_errors.MultipleDevicesError(self.devices))
1490          if not args.all and not args.devices:
1491            self._parser.error(_GenerateMissingAllFlagMessage(self.devices))
1492        # Save cache now if command will not get a chance to afterwards.
1493        if self.calls_exec:
1494          _SaveDeviceCaches(self.devices, args.output_directory)
1495      except:
1496        _SaveDeviceCaches(self.devices, args.output_directory)
1497        raise
1498
1499
1500class _DevicesCommand(_Command):
1501  name = 'devices'
1502  description = 'Describe attached devices.'
1503  all_devices_by_default = True
1504
1505  def Run(self):
1506    print(_GenerateAvailableDevicesMessage(self.devices))
1507
1508
1509class _PackageInfoCommand(_Command):
1510  name = 'package-info'
1511  description = 'Show various attributes of this app.'
1512  need_device_args = False
1513  needs_package_name = True
1514  needs_apk_helper = True
1515
1516  def Run(self):
1517    # Format all (even ints) as strings, to handle cases where APIs return None
1518    print('Package name: "%s"' % self.args.package_name)
1519    print('versionCode: %s' % self.apk_helper.GetVersionCode())
1520    print('versionName: "%s"' % self.apk_helper.GetVersionName())
1521    print('minSdkVersion: %s' % self.apk_helper.GetMinSdkVersion())
1522    print('targetSdkVersion: %s' % self.apk_helper.GetTargetSdkVersion())
1523    print('Supported ABIs: %r' % self.apk_helper.GetAbis())
1524
1525
1526class _InstallCommand(_Command):
1527  name = 'install'
1528  description = 'Installs the APK or bundle to one or more devices.'
1529  needs_package_name = True
1530  needs_apk_helper = True
1531  supports_incremental = True
1532  default_modules = []
1533
1534  def _RegisterExtraArgs(self, group):
1535    if self.is_bundle:
1536      group.add_argument(
1537          '--locales',
1538          action='append',
1539          help=
1540          'Locale splits to install (english is in base, so always installed).')
1541      group.add_argument(
1542          '-m',
1543          '--module',
1544          action='append',
1545          default=self.default_modules,
1546          help='Module to install. Can be specified multiple times.')
1547      group.add_argument(
1548          '-f',
1549          '--fake',
1550          action='append',
1551          default=[],
1552          help='Fake bundle module install. Can be specified multiple times. '
1553          'Requires \'-m {0}\' to be given, and \'-f {0}\' is illegal.'.format(
1554              BASE_MODULE))
1555      # Add even if |self.default_modules| is empty, for consistency.
1556      group.add_argument('--no-module',
1557                         action='append',
1558                         choices=self.default_modules,
1559                         default=[],
1560                         help='Module to exclude from default install.')
1561
1562  def Run(self):
1563    if self.additional_apk_helpers:
1564      for additional_apk_helper in self.additional_apk_helpers:
1565        _InstallApk(self.devices, additional_apk_helper, None)
1566    if self.is_bundle:
1567      modules = list(
1568          set(self.args.module) - set(self.args.no_module) -
1569          set(self.args.fake))
1570      _InstallBundle(self.devices, self.apk_helper, modules, self.args.fake,
1571                     self.args.locales)
1572    else:
1573      _InstallApk(self.devices, self.apk_helper, self.install_dict)
1574    if self.args.is_official_build:
1575      _RunCompileDex(self.devices, self.args.package_name, 'speed')
1576
1577
1578class _UninstallCommand(_Command):
1579  name = 'uninstall'
1580  description = 'Removes the APK or bundle from one or more devices.'
1581  needs_package_name = True
1582
1583  def Run(self):
1584    _UninstallApk(self.devices, self.install_dict, self.args.package_name)
1585
1586
1587class _SetWebViewProviderCommand(_Command):
1588  name = 'set-webview-provider'
1589  description = ("Sets the device's WebView provider to this APK's "
1590                 "package name.")
1591  needs_package_name = True
1592  needs_apk_helper = True
1593
1594  def Run(self):
1595    if not _IsWebViewProvider(self.apk_helper):
1596      raise Exception('This package does not have a WebViewLibrary meta-data '
1597                      'tag. Are you sure it contains a WebView implementation?')
1598    _SetWebViewProvider(self.devices, self.args.package_name)
1599
1600
1601class _LaunchCommand(_Command):
1602  name = 'launch'
1603  description = ('Sends a launch intent for the APK or bundle after first '
1604                 'writing the command-line flags file.')
1605  needs_package_name = True
1606  accepts_command_line_flags = True
1607  all_devices_by_default = True
1608
1609  def _RegisterExtraArgs(self, group):
1610    group.add_argument('-w', '--wait-for-java-debugger', action='store_true',
1611                       help='Pause execution until debugger attaches. Applies '
1612                            'only to the main process. To have renderers wait, '
1613                            'use --args="--renderer-wait-for-java-debugger"')
1614    group.add_argument('--debug-process-name',
1615                       help='Name of the process to debug. '
1616                            'E.g. "privileged_process0", or "foo.bar:baz"')
1617    group.add_argument('--nokill', action='store_true',
1618                       help='Do not set the debug-app, nor set command-line '
1619                            'flags. Useful to load a URL without having the '
1620                             'app restart.')
1621    group.add_argument('url', nargs='?', help='A URL to launch with.')
1622
1623  def Run(self):
1624    if self.is_test_apk:
1625      raise Exception('Use the bin/run_* scripts to run test apks.')
1626    _LaunchUrl(self.devices,
1627               self.args.package_name,
1628               argv=self.args.args,
1629               command_line_flags_file=self.args.command_line_flags_file,
1630               url=self.args.url,
1631               wait_for_java_debugger=self.args.wait_for_java_debugger,
1632               debug_process_name=self.args.debug_process_name,
1633               nokill=self.args.nokill)
1634
1635
1636class _StopCommand(_Command):
1637  name = 'stop'
1638  description = 'Force-stops the app.'
1639  needs_package_name = True
1640  all_devices_by_default = True
1641
1642  def Run(self):
1643    device_utils.DeviceUtils.parallel(self.devices).ForceStop(
1644        self.args.package_name)
1645
1646
1647class _ClearDataCommand(_Command):
1648  name = 'clear-data'
1649  descriptions = 'Clears all app data.'
1650  needs_package_name = True
1651  all_devices_by_default = True
1652
1653  def Run(self):
1654    device_utils.DeviceUtils.parallel(self.devices).ClearApplicationState(
1655        self.args.package_name)
1656
1657
1658class _ArgvCommand(_Command):
1659  name = 'argv'
1660  description = 'Display and optionally update command-line flags file.'
1661  needs_package_name = True
1662  accepts_command_line_flags = True
1663  all_devices_by_default = True
1664
1665  def Run(self):
1666    command_line_flags_file = self.args.command_line_flags_file
1667    argv = self.args.args
1668    devices = self.devices
1669    parallel_devices = device_utils.DeviceUtils.parallel(devices)
1670
1671    if argv is not None:
1672      parallel_devices.pMap(
1673          lambda d: _UpdateDeviceFlags(d, command_line_flags_file, argv))
1674
1675    outputs = parallel_devices.pMap(
1676        lambda d: _ReadDeviceFlags(d, command_line_flags_file) or '').pGet(None)
1677
1678    print(f'Showing flags via /data/local/tmp/{command_line_flags_file}:')
1679    for flags in _PrintPerDeviceOutput(devices, outputs, single_line=True):
1680      print(flags or 'No flags set.')
1681
1682
1683class _GdbCommand(_Command):
1684  name = 'gdb'
1685  description = 'Runs //build/android/adb_gdb with apk-specific args.'
1686  long_description = description + """
1687
1688To attach to a process other than the APK's main process, use --pid=1234.
1689To list all PIDs, use the "ps" command.
1690
1691If no apk process is currently running, sends a launch intent.
1692"""
1693  needs_package_name = True
1694  needs_output_directory = True
1695  calls_exec = True
1696  supports_multiple_devices = False
1697
1698  def Run(self):
1699    _RunGdb(self.devices[0], self.args.package_name,
1700            self.args.debug_process_name, self.args.pid,
1701            self.args.output_directory, self.args.target_cpu, self.args.port,
1702            self.args.ide, bool(self.args.verbose_count))
1703
1704  def _RegisterExtraArgs(self, group):
1705    pid_group = group.add_mutually_exclusive_group()
1706    pid_group.add_argument('--debug-process-name',
1707                           help='Name of the process to attach to. '
1708                                'E.g. "privileged_process0", or "foo.bar:baz"')
1709    pid_group.add_argument('--pid',
1710                           help='The process ID to attach to. Defaults to '
1711                                'the main process for the package.')
1712    group.add_argument('--ide', action='store_true',
1713                       help='Rather than enter a gdb prompt, set up the '
1714                            'gdb connection and wait for an IDE to '
1715                            'connect.')
1716    # Same default port that ndk-gdb.py uses.
1717    group.add_argument('--port', type=int, default=5039,
1718                       help='Use the given port for the GDB connection')
1719
1720
1721class _LldbCommand(_Command):
1722  name = 'lldb'
1723  description = 'Runs //build/android/connect_lldb.sh with apk-specific args.'
1724  long_description = description + """
1725
1726To attach to a process other than the APK's main process, use --pid=1234.
1727To list all PIDs, use the "ps" command.
1728
1729If no apk process is currently running, sends a launch intent.
1730"""
1731  needs_package_name = True
1732  needs_output_directory = True
1733  calls_exec = True
1734  supports_multiple_devices = False
1735
1736  def Run(self):
1737    _RunLldb(device=self.devices[0],
1738             package_name=self.args.package_name,
1739             debug_process_name=self.args.debug_process_name,
1740             pid=self.args.pid,
1741             output_directory=self.args.output_directory,
1742             port=self.args.port,
1743             target_cpu=self.args.target_cpu,
1744             ndk_dir=self.args.ndk_dir,
1745             lldb_server=self.args.lldb_server,
1746             lldb=self.args.lldb,
1747             verbose=bool(self.args.verbose_count))
1748
1749  def _RegisterExtraArgs(self, group):
1750    pid_group = group.add_mutually_exclusive_group()
1751    pid_group.add_argument('--debug-process-name',
1752                           help='Name of the process to attach to. '
1753                           'E.g. "privileged_process0", or "foo.bar:baz"')
1754    pid_group.add_argument('--pid',
1755                           help='The process ID to attach to. Defaults to '
1756                           'the main process for the package.')
1757    group.add_argument('--ndk-dir',
1758                       help='Select alternative NDK root directory.')
1759    group.add_argument('--lldb-server',
1760                       help='Select alternative on-device lldb-server.')
1761    group.add_argument('--lldb', help='Select alternative client lldb.sh.')
1762    # Same default port that ndk-gdb.py uses.
1763    group.add_argument('--port',
1764                       type=int,
1765                       default=5039,
1766                       help='Use the given port for the LLDB connection')
1767
1768
1769class _LogcatCommand(_Command):
1770  name = 'logcat'
1771  description = 'Runs "adb logcat" with filters relevant the current APK.'
1772  long_description = description + """
1773
1774"Relevant filters" means:
1775  * Log messages from processes belonging to the apk,
1776  * Plus log messages from log tags: ActivityManager|DEBUG,
1777  * Plus fatal logs from any process,
1778  * Minus spamy dalvikvm logs (for pre-L devices).
1779
1780Colors:
1781  * Primary process is white
1782  * Other processes (gpu, renderer) are yellow
1783  * Non-apk processes are grey
1784  * UI thread has a bolded Thread-ID
1785
1786Java stack traces are detected and deobfuscated (for release builds).
1787
1788To disable filtering, (but keep coloring), use --verbose.
1789"""
1790  needs_package_name = True
1791  supports_multiple_devices = False
1792
1793  def Run(self):
1794    deobfuscate = None
1795    if self.args.proguard_mapping_path and not self.args.no_deobfuscate:
1796      deobfuscate = deobfuscator.Deobfuscator(self.args.proguard_mapping_path)
1797    apk_path = self.args.apk_path or self.incremental_apk_path
1798    if apk_path or self.bundle_generation_info:
1799      stack_script_context = _StackScriptContext(self.args.output_directory,
1800                                                 apk_path,
1801                                                 self.bundle_generation_info,
1802                                                 quiet=True)
1803    else:
1804      stack_script_context = None
1805
1806    extra_package_names = []
1807    if self.is_test_apk and self.additional_apk_helpers:
1808      for additional_apk_helper in self.additional_apk_helpers:
1809        extra_package_names.append(additional_apk_helper.GetPackageName())
1810
1811    try:
1812      _RunLogcat(self.devices[0],
1813                 self.args.package_name,
1814                 stack_script_context,
1815                 deobfuscate,
1816                 bool(self.args.verbose_count),
1817                 self.args.exit_on_match,
1818                 extra_package_names=extra_package_names)
1819    except KeyboardInterrupt:
1820      pass  # Don't show stack trace upon Ctrl-C
1821    finally:
1822      if stack_script_context:
1823        stack_script_context.Close()
1824      if deobfuscate:
1825        deobfuscate.Close()
1826
1827  def _RegisterExtraArgs(self, group):
1828    if self._from_wrapper_script:
1829      group.add_argument('--no-deobfuscate', action='store_true',
1830          help='Disables ProGuard deobfuscation of logcat.')
1831    else:
1832      group.set_defaults(no_deobfuscate=False)
1833      group.add_argument('--proguard-mapping-path',
1834          help='Path to ProGuard map (enables deobfuscation)')
1835    group.add_argument('--exit-on-match',
1836                       help='Exits logcat when a message matches this regex.')
1837
1838
1839class _PsCommand(_Command):
1840  name = 'ps'
1841  description = 'Show PIDs of any APK processes currently running.'
1842  needs_package_name = True
1843  all_devices_by_default = True
1844
1845  def Run(self):
1846    _RunPs(self.devices, self.args.package_name)
1847
1848
1849class _DiskUsageCommand(_Command):
1850  name = 'disk-usage'
1851  description = 'Show how much device storage is being consumed by the app.'
1852  needs_package_name = True
1853  all_devices_by_default = True
1854
1855  def Run(self):
1856    _RunDiskUsage(self.devices, self.args.package_name)
1857
1858
1859class _MemUsageCommand(_Command):
1860  name = 'mem-usage'
1861  description = 'Show memory usage of currently running APK processes.'
1862  needs_package_name = True
1863  all_devices_by_default = True
1864
1865  def _RegisterExtraArgs(self, group):
1866    group.add_argument('--query-app', action='store_true',
1867        help='Do not add --local to "dumpsys meminfo". This will output '
1868             'additional metrics (e.g. Context count), but also cause memory '
1869             'to be used in order to gather the metrics.')
1870
1871  def Run(self):
1872    _RunMemUsage(self.devices, self.args.package_name,
1873                 query_app=self.args.query_app)
1874
1875
1876class _ShellCommand(_Command):
1877  name = 'shell'
1878  description = ('Same as "adb shell <command>", but runs as the apk\'s uid '
1879                 '(via run-as). Useful for inspecting the app\'s data '
1880                 'directory.')
1881  needs_package_name = True
1882
1883  @property
1884  def calls_exec(self):
1885    return not self.args.cmd
1886
1887  @property
1888  def supports_multiple_devices(self):
1889    return not self.args.cmd
1890
1891  def _RegisterExtraArgs(self, group):
1892    group.add_argument(
1893        'cmd', nargs=argparse.REMAINDER, help='Command to run.')
1894
1895  def Run(self):
1896    _RunShell(self.devices, self.args.package_name, self.args.cmd)
1897
1898
1899class _CompileDexCommand(_Command):
1900  name = 'compile-dex'
1901  description = ('Applicable only for Android N+. Forces .odex files to be '
1902                 'compiled with the given compilation filter. To see existing '
1903                 'filter, use "disk-usage" command.')
1904  needs_package_name = True
1905  all_devices_by_default = True
1906
1907  def _RegisterExtraArgs(self, group):
1908    group.add_argument(
1909        'compilation_filter',
1910        choices=['verify', 'quicken', 'space-profile', 'space',
1911                 'speed-profile', 'speed'],
1912        help='For WebView/Monochrome, use "speed". For other apks, use '
1913             '"speed-profile".')
1914
1915  def Run(self):
1916    outputs = _RunCompileDex(self.devices, self.args.package_name,
1917                             self.args.compilation_filter)
1918    for output in _PrintPerDeviceOutput(self.devices, outputs):
1919      for line in output:
1920        print(line)
1921
1922
1923class _PrintCertsCommand(_Command):
1924  name = 'print-certs'
1925  description = 'Print info about certificates used to sign this APK.'
1926  need_device_args = False
1927  needs_apk_helper = True
1928
1929  def _RegisterExtraArgs(self, group):
1930    group.add_argument(
1931        '--full-cert',
1932        action='store_true',
1933        help=("Print the certificate's full signature, Base64-encoded. "
1934              "Useful when configuring an Android image's "
1935              "config_webview_packages.xml."))
1936
1937  def Run(self):
1938    keytool = os.path.join(_JAVA_HOME, 'bin', 'keytool')
1939    pem_certificate_pattern = re.compile(
1940        r'-+BEGIN CERTIFICATE-+([\r\n0-9A-Za-z+/=]+)-+END CERTIFICATE-+[\r\n]*')
1941    if self.is_bundle:
1942      # Bundles are not signed until converted to .apks. The wrapper scripts
1943      # record which key will be used to sign though.
1944      with tempfile.NamedTemporaryFile() as f:
1945        logging.warning('Bundles are not signed until turned into .apk files.')
1946        logging.warning('Showing signing info based on associated keystore.')
1947        cmd = [
1948            keytool, '-exportcert', '-keystore',
1949            self.bundle_generation_info.keystore_path, '-storepass',
1950            self.bundle_generation_info.keystore_password, '-alias',
1951            self.bundle_generation_info.keystore_alias, '-file', f.name
1952        ]
1953        logging.warning('Running: %s', shlex.join(cmd))
1954        subprocess.check_output(cmd, stderr=subprocess.STDOUT)
1955        cmd = [keytool, '-printcert', '-file', f.name]
1956        logging.warning('Running: %s', shlex.join(cmd))
1957        subprocess.check_call(cmd)
1958        if self.args.full_cert:
1959          # Redirect stderr to hide a keytool warning about using non-standard
1960          # keystore format.
1961          cmd += ['-rfc']
1962          logging.warning('Running: %s', shlex.join(cmd))
1963          pem_encoded_certificate = subprocess.check_output(
1964              cmd, stderr=subprocess.STDOUT).decode()
1965    else:
1966
1967      def run_apksigner(min_sdk_version):
1968        cmd = [
1969            build_tools.GetPath('apksigner'), 'verify', '--min-sdk-version',
1970            str(min_sdk_version), '--print-certs-pem', '--verbose',
1971            self.apk_helper.path
1972        ]
1973        logging.warning('Running: %s', shlex.join(cmd))
1974        env = os.environ.copy()
1975        env['PATH'] = os.path.pathsep.join(
1976            [os.path.join(_JAVA_HOME, 'bin'),
1977             env.get('PATH')])
1978        # Redirect stderr to hide verification failures (see explanation below).
1979        return subprocess.check_output(cmd,
1980                                       env=env,
1981                                       universal_newlines=True,
1982                                       stderr=subprocess.STDOUT)
1983
1984      # apksigner's default behavior is nonintuitive: it will print "Verified
1985      # using <scheme number>...: false" for any scheme which is obsolete for
1986      # the APK's minSdkVersion even if it actually was signed with that scheme
1987      # (ex. it prints "Verified using v1 scheme: false" for Monochrome because
1988      # v1 was obsolete by N). To workaround this, we force apksigner to use the
1989      # lowest possible minSdkVersion. We need to fallback to higher
1990      # minSdkVersions in case the APK fails to verify for that minSdkVersion
1991      # (which means the APK is genuinely not signed with that scheme). These
1992      # SDK values are the highest SDK version before the next scheme is
1993      # available:
1994      versions = [
1995          version_codes.MARSHMALLOW,  # before v2 launched in N
1996          version_codes.OREO_MR1,  # before v3 launched in P
1997          version_codes.Q,  # before v4 launched in R
1998          version_codes.R,
1999      ]
2000      stdout = None
2001      for min_sdk_version in versions:
2002        try:
2003          stdout = run_apksigner(min_sdk_version)
2004          break
2005        except subprocess.CalledProcessError:
2006          # Doesn't verify with this min-sdk-version, so try again with a higher
2007          # one
2008          continue
2009      if not stdout:
2010        raise RuntimeError('apksigner was not able to verify APK')
2011
2012      # Separate what the '--print-certs' flag would output vs. the additional
2013      # signature output included by '--print-certs-pem'. The additional PEM
2014      # output is only printed when self.args.full_cert is specified.
2015      verification_hash_info = pem_certificate_pattern.sub('', stdout)
2016      print(verification_hash_info)
2017      if self.args.full_cert:
2018        m = pem_certificate_pattern.search(stdout)
2019        if not m:
2020          raise Exception('apksigner did not print a certificate')
2021        pem_encoded_certificate = m.group(0)
2022
2023
2024    if self.args.full_cert:
2025      m = pem_certificate_pattern.search(pem_encoded_certificate)
2026      if not m:
2027        raise Exception(
2028            'Unable to parse certificate:\n{}'.format(pem_encoded_certificate))
2029      signature = re.sub(r'[\r\n]+', '', m.group(1))
2030      print()
2031      print('Full Signature:')
2032      print(signature)
2033
2034
2035class _ProfileCommand(_Command):
2036  name = 'profile'
2037  description = ('Run the simpleperf sampling CPU profiler on the currently-'
2038                 'running APK. If --args is used, the extra arguments will be '
2039                 'passed on to simpleperf; otherwise, the following default '
2040                 'arguments are used: -g -f 1000 -o /data/local/tmp/perf.data')
2041  needs_package_name = True
2042  needs_output_directory = True
2043  supports_multiple_devices = False
2044  accepts_args = True
2045
2046  def _RegisterExtraArgs(self, group):
2047    group.add_argument(
2048        '--profile-process', default='browser',
2049        help=('Which process to profile. This may be a process name or pid '
2050              'such as you would get from running `%s ps`; or '
2051              'it can be one of (browser, renderer, gpu).' % sys.argv[0]))
2052    group.add_argument(
2053        '--profile-thread', default=None,
2054        help=('(Optional) Profile only a single thread. This may be either a '
2055              'thread ID such as you would get by running `adb shell ps -t` '
2056              '(pre-Oreo) or `adb shell ps -e -T` (Oreo and later); or it may '
2057              'be one of (io, compositor, main, render), in which case '
2058              '--profile-process is also required. (Note that "render" thread '
2059              'refers to a thread in the browser process that manages a '
2060              'renderer; to profile the main thread of the renderer process, '
2061              'use --profile-thread=main).'))
2062    group.add_argument('--profile-output', default='profile.pb',
2063                       help='Output file for profiling data')
2064    group.add_argument('--profile-events', default='cpu-cycles',
2065                      help=('A comma separated list of perf events to capture '
2066                      '(e.g. \'cpu-cycles,branch-misses\'). Run '
2067                      '`simpleperf list` on your device to see available '
2068                      'events.'))
2069
2070  def Run(self):
2071    extra_args = shlex.split(self.args.args or '')
2072    _RunProfile(self.devices[0], self.args.package_name,
2073                self.args.output_directory, self.args.profile_output,
2074                self.args.profile_process, self.args.profile_thread,
2075                self.args.profile_events, extra_args)
2076
2077
2078class _RunCommand(_InstallCommand, _LaunchCommand, _LogcatCommand):
2079  name = 'run'
2080  description = 'Install, launch, and show logcat (when targeting one device).'
2081  all_devices_by_default = False
2082  supports_multiple_devices = True
2083
2084  def _RegisterExtraArgs(self, group):
2085    _InstallCommand._RegisterExtraArgs(self, group)
2086    _LaunchCommand._RegisterExtraArgs(self, group)
2087    _LogcatCommand._RegisterExtraArgs(self, group)
2088    group.add_argument('--no-logcat', action='store_true',
2089                       help='Install and launch, but do not enter logcat.')
2090
2091  def Run(self):
2092    if self.is_test_apk:
2093      raise Exception('Use the bin/run_* scripts to run test apks.')
2094    logging.warning('Installing...')
2095    _InstallCommand.Run(self)
2096    logging.warning('Sending launch intent...')
2097    _LaunchCommand.Run(self)
2098    if len(self.devices) == 1 and not self.args.no_logcat:
2099      logging.warning('Entering logcat...')
2100      _LogcatCommand.Run(self)
2101
2102
2103class _BuildBundleApks(_Command):
2104  name = 'build-bundle-apks'
2105  description = ('Build the .apks archive from an Android app bundle, and '
2106                 'optionally copy it to a specific destination.')
2107  need_device_args = False
2108
2109  def _RegisterExtraArgs(self, group):
2110    group.add_argument(
2111        '--output-apks', required=True, help='Destination path for .apks file.')
2112    group.add_argument(
2113        '--minimal',
2114        action='store_true',
2115        help='Build .apks archive that targets the bundle\'s minSdkVersion and '
2116        'contains only english splits. It still contains optional splits.')
2117    group.add_argument(
2118        '--sdk-version', help='The sdkVersion to build the .apks for.')
2119    group.add_argument(
2120        '--build-mode',
2121        choices=app_bundle_utils.BUILD_APKS_MODES,
2122        help='Specify which type of APKs archive to build. "default" '
2123        'generates regular splits, "universal" generates an archive with a '
2124        'single universal APK, "system" generates an archive with a system '
2125        'image APK, while "system_compressed" generates a compressed system '
2126        'APK, with an additional stub APK for the system image.')
2127    group.add_argument(
2128        '--optimize-for',
2129        choices=app_bundle_utils.OPTIMIZE_FOR_OPTIONS,
2130        help='Override split configuration.')
2131
2132  def Run(self):
2133    _GenerateBundleApks(
2134        self.bundle_generation_info,
2135        output_path=self.args.output_apks,
2136        minimal=self.args.minimal,
2137        minimal_sdk_version=self.args.sdk_version,
2138        mode=self.args.build_mode,
2139        optimize_for=self.args.optimize_for)
2140
2141
2142class _ManifestCommand(_Command):
2143  name = 'dump-manifest'
2144  description = 'Dump the android manifest as XML, to stdout.'
2145  need_device_args = False
2146  needs_apk_helper = True
2147
2148  def Run(self):
2149    if self.is_bundle:
2150      sys.stdout.write(
2151          bundletool.RunBundleTool([
2152              'dump', 'manifest', '--bundle',
2153              self.bundle_generation_info.bundle_path
2154          ]))
2155    else:
2156      apkanalyzer = os.path.join(_DIR_SOURCE_ROOT, 'third_party', 'android_sdk',
2157                                 'public', 'cmdline-tools', 'latest', 'bin',
2158                                 'apkanalyzer')
2159      cmd = [apkanalyzer, 'manifest', 'print', self.apk_helper.path]
2160      logging.info('Running: %s', shlex.join(cmd))
2161      subprocess.check_call(cmd)
2162
2163
2164class _StackCommand(_Command):
2165  name = 'stack'
2166  description = 'Decodes an Android stack.'
2167  need_device_args = False
2168
2169  def _RegisterExtraArgs(self, group):
2170    group.add_argument(
2171        'file',
2172        nargs='?',
2173        help='File to decode. If not specified, stdin is processed.')
2174
2175  def Run(self):
2176    apk_path = self.args.apk_path or self.incremental_apk_path
2177    context = _StackScriptContext(self.args.output_directory, apk_path,
2178                                  self.bundle_generation_info)
2179    try:
2180      proc = context.Popen(input_file=self.args.file)
2181      if proc.wait():
2182        raise Exception('stack script returned {}'.format(proc.returncode))
2183    finally:
2184      context.Close()
2185
2186
2187# Shared commands for regular APKs and app bundles.
2188_COMMANDS = [
2189    _DevicesCommand,
2190    _PackageInfoCommand,
2191    _InstallCommand,
2192    _UninstallCommand,
2193    _SetWebViewProviderCommand,
2194    _LaunchCommand,
2195    _StopCommand,
2196    _ClearDataCommand,
2197    _ArgvCommand,
2198    _GdbCommand,
2199    _LldbCommand,
2200    _LogcatCommand,
2201    _PsCommand,
2202    _DiskUsageCommand,
2203    _MemUsageCommand,
2204    _ShellCommand,
2205    _CompileDexCommand,
2206    _PrintCertsCommand,
2207    _ProfileCommand,
2208    _RunCommand,
2209    _StackCommand,
2210    _ManifestCommand,
2211]
2212
2213# Commands specific to app bundles.
2214_BUNDLE_COMMANDS = [
2215    _BuildBundleApks,
2216]
2217
2218
2219def _ParseArgs(parser, from_wrapper_script, is_bundle, is_test_apk):
2220  subparsers = parser.add_subparsers()
2221  command_list = _COMMANDS + (_BUNDLE_COMMANDS if is_bundle else [])
2222  commands = [
2223      clazz(from_wrapper_script, is_bundle, is_test_apk)
2224      for clazz in command_list
2225  ]
2226
2227  for command in commands:
2228    if from_wrapper_script or not command.needs_output_directory:
2229      command.RegisterArgs(subparsers)
2230
2231  # Show extended help when no command is passed.
2232  argv = sys.argv[1:]
2233  if not argv:
2234    argv = ['--help']
2235
2236  return parser.parse_args(argv)
2237
2238
2239def _RunInternal(parser,
2240                 output_directory=None,
2241                 additional_apk_paths=None,
2242                 bundle_generation_info=None,
2243                 is_test_apk=False):
2244  colorama.init()
2245  parser.set_defaults(
2246      additional_apk_paths=additional_apk_paths,
2247      output_directory=output_directory)
2248  from_wrapper_script = bool(output_directory)
2249  args = _ParseArgs(parser,
2250                    from_wrapper_script,
2251                    is_bundle=bool(bundle_generation_info),
2252                    is_test_apk=is_test_apk)
2253  run_tests_helper.SetLogLevel(args.verbose_count)
2254  if bundle_generation_info:
2255    args.command.RegisterBundleGenerationInfo(bundle_generation_info)
2256  if args.additional_apk_paths:
2257    for i, path in enumerate(args.additional_apk_paths):
2258      if path and not os.path.exists(path):
2259        inc_path = path.replace('.apk', '_incremental.apk')
2260        if os.path.exists(inc_path):
2261          args.additional_apk_paths[i] = inc_path
2262          path = inc_path
2263      if not path or not os.path.exists(path):
2264        raise Exception('Invalid additional APK path "{}"'.format(path))
2265  args.command.ProcessArgs(args)
2266  args.command.Run()
2267  # Incremental install depends on the cache being cleared when uninstalling.
2268  if args.command.name != 'uninstall':
2269    _SaveDeviceCaches(args.command.devices, output_directory)
2270
2271
2272def Run(output_directory, apk_path, additional_apk_paths, incremental_json,
2273        command_line_flags_file, target_cpu, proguard_mapping_path):
2274  """Entry point for generated wrapper scripts."""
2275  constants.SetOutputDirectory(output_directory)
2276  devil_chromium.Initialize(output_directory=output_directory)
2277  parser = argparse.ArgumentParser()
2278  exists_or_none = lambda p: p if p and os.path.exists(p) else None
2279
2280  parser.set_defaults(
2281      command_line_flags_file=command_line_flags_file,
2282      target_cpu=target_cpu,
2283      apk_path=exists_or_none(apk_path),
2284      incremental_json=exists_or_none(incremental_json),
2285      proguard_mapping_path=proguard_mapping_path)
2286  _RunInternal(
2287      parser,
2288      output_directory=output_directory,
2289      additional_apk_paths=additional_apk_paths)
2290
2291
2292def RunForBundle(output_directory, bundle_path, bundle_apks_path,
2293                 additional_apk_paths, aapt2_path, keystore_path,
2294                 keystore_password, keystore_alias, package_name,
2295                 command_line_flags_file, proguard_mapping_path, target_cpu,
2296                 system_image_locales, default_modules, is_official_build):
2297  """Entry point for generated app bundle wrapper scripts.
2298
2299  Args:
2300    output_dir: Chromium output directory path.
2301    bundle_path: Input bundle path.
2302    bundle_apks_path: Output bundle .apks archive path.
2303    additional_apk_paths: Additional APKs to install prior to bundle install.
2304    aapt2_path: Aapt2 tool path.
2305    keystore_path: Keystore file path.
2306    keystore_password: Keystore password.
2307    keystore_alias: Signing key name alias in keystore file.
2308    package_name: Application's package name.
2309    command_line_flags_file: Optional. Name of an on-device file that will be
2310      used to store command-line flags for this bundle.
2311    proguard_mapping_path: Input path to the Proguard mapping file, used to
2312      deobfuscate Java stack traces.
2313    target_cpu: Chromium target CPU name, used by the 'gdb' command.
2314    system_image_locales: List of Chromium locales that should be included in
2315      system image APKs.
2316    default_modules: List of modules that are installed in addition to those
2317      given by the '-m' switch.
2318  """
2319  constants.SetOutputDirectory(output_directory)
2320  devil_chromium.Initialize(output_directory=output_directory)
2321  bundle_generation_info = BundleGenerationInfo(
2322      bundle_path=bundle_path,
2323      bundle_apks_path=bundle_apks_path,
2324      aapt2_path=aapt2_path,
2325      keystore_path=keystore_path,
2326      keystore_password=keystore_password,
2327      keystore_alias=keystore_alias,
2328      system_image_locales=system_image_locales)
2329  _InstallCommand.default_modules = default_modules
2330
2331  parser = argparse.ArgumentParser()
2332  parser.set_defaults(package_name=package_name,
2333                      command_line_flags_file=command_line_flags_file,
2334                      proguard_mapping_path=proguard_mapping_path,
2335                      target_cpu=target_cpu,
2336                      is_official_build=is_official_build)
2337  _RunInternal(parser,
2338               output_directory=output_directory,
2339               additional_apk_paths=additional_apk_paths,
2340               bundle_generation_info=bundle_generation_info)
2341
2342
2343def RunForTestApk(*, output_directory, package_name, test_apk_path,
2344                  test_apk_json, proguard_mapping_path, additional_apk_paths):
2345  """Entry point for generated test apk wrapper scripts.
2346
2347  This is intended to make commands like logcat (with proguard deobfuscation)
2348  available. The run_* scripts should be used to actually run tests.
2349
2350  Args:
2351    output_dir: Chromium output directory path.
2352    package_name: The package name for the test apk.
2353    test_apk_path: The test apk to install.
2354    test_apk_json: The incremental json dict for the test apk.
2355    proguard_mapping_path: Input path to the Proguard mapping file, used to
2356      deobfuscate Java stack traces.
2357    additional_apk_paths: Additional APKs to install.
2358  """
2359  constants.SetOutputDirectory(output_directory)
2360  devil_chromium.Initialize(output_directory=output_directory)
2361
2362  parser = argparse.ArgumentParser()
2363  exists_or_none = lambda p: p if p and os.path.exists(p) else None
2364
2365  parser.set_defaults(apk_path=exists_or_none(test_apk_path),
2366                      incremental_json=exists_or_none(test_apk_json),
2367                      package_name=package_name,
2368                      proguard_mapping_path=proguard_mapping_path)
2369
2370  _RunInternal(parser,
2371               output_directory=output_directory,
2372               additional_apk_paths=additional_apk_paths,
2373               is_test_apk=True)
2374
2375
2376def main():
2377  devil_chromium.Initialize()
2378  _RunInternal(argparse.ArgumentParser())
2379
2380
2381if __name__ == '__main__':
2382  main()
2383