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