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