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