chromium/tools/android/avd/avd.py

#! /usr/bin/env vpython3
# Copyright 2019 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

import argparse
import os
import logging
import pathlib
import sys

_SRC_ROOT = os.path.abspath(
    os.path.join(os.path.dirname(__file__), '..', '..', '..'))

sys.path.append(
    os.path.join(_SRC_ROOT, 'third_party', 'catapult', 'devil'))
from devil.android.tools import script_common
from devil.utils import logging_common

sys.path.append(
    os.path.join(_SRC_ROOT, 'build', 'android'))
import devil_chromium
from pylib.local.emulator import avd


def _add_avd_config_argument(parser):
  parser.add_argument('--avd-config',
                      type=os.path.realpath,
                      metavar='PATH',
                      required=True,
                      help='Path to an AVD config text protobuf.')


def _add_common_arguments(parser):
  logging_common.AddLoggingArguments(parser)
  script_common.AddEnvironmentArguments(parser)


def main(raw_args):

  parser = argparse.ArgumentParser()

  subparsers = parser.add_subparsers()
  subparser = subparsers.add_parser(
      'install',
      help='Install the CIPD packages specified in the given config.')
  _add_common_arguments(subparser)
  _add_avd_config_argument(subparser)

  def install_cmd(args):
    avd.AvdConfig(args.avd_config).Install()
    return 0

  subparser.set_defaults(func=install_cmd)

  subparser = subparsers.add_parser(
      'uninstall',
      help='Uninstall all the artifacts associated with the given config.')
  _add_common_arguments(subparser)
  _add_avd_config_argument(subparser)

  def uninstall_cmd(args):
    avd.AvdConfig(args.avd_config).Uninstall()
    return 0

  subparser.set_defaults(func=uninstall_cmd)

  subparser = subparsers.add_parser(
      'create',
      help='Create an AVD CIPD package according to the given config.')
  _add_common_arguments(subparser)
  _add_avd_config_argument(subparser)
  subparser.add_argument(
      '--snapshot',
      action='store_true',
      help='Snapshot the AVD before creating the CIPD package.')
  subparser.add_argument('--force',
                         action='store_true',
                         help='Pass --force to AVD creation.')
  subparser.add_argument('--keep',
                         action='store_true',
                         help='Keep the AVD after creating the CIPD package.')
  subparser.add_argument(
      '--privileged-apk',
      action='append',
      default=[],
      dest='privileged_apk_pairs',
      nargs=2,
      metavar=('APK_PATH', 'DEVICE_PARTITION'),
      help='Privileged apks to be installed during AVD launching. Expecting '
      'two strings where the first element being the path to the APK, and the '
      'second element being the system image partition on device where the APK '
      'will be pushed to. Example: --privileged-apk path/to/some.apk /system')
  subparser.add_argument(
      '--additional-apk',
      action='append',
      default=[],
      dest='additional_apks',
      metavar='APK_PATH',
      type=os.path.realpath,
      help='Additional apk to be installed during AVD launching')
  subparser.add_argument(
      '--cipd-json-output',
      type=os.path.realpath,
      metavar='PATH',
      help='Path to which `cipd create` should dump json output '
      'via -json-output.')
  subparser.add_argument(
      '--dry-run',
      action='store_true',
      help='Skip the CIPD package creation after creating the AVD.')

  def create_cmd(args):
    avd.AvdConfig(args.avd_config).Create(
        force=args.force,
        snapshot=args.snapshot,
        keep=args.keep,
        additional_apks=args.additional_apks,
        privileged_apk_tuples=[tuple(p) for p in args.privileged_apk_pairs],
        cipd_json_output=args.cipd_json_output,
        dry_run=args.dry_run)
    return 0

  subparser.set_defaults(func=create_cmd)

  subparser = subparsers.add_parser(
      'start',
      help='Start an AVD instance with the given config.',
      formatter_class=argparse.ArgumentDefaultsHelpFormatter)
  _add_common_arguments(subparser)
  _add_avd_config_argument(subparser)
  subparser.add_argument(
      '--wipe-data',
      action='store_true',
      default=False,
      help='Reset user data image for this emulator. Note that when set, all '
      'the customization, e.g. wifi, additional apks, privileged apks will be '
      'gone')
  subparser.add_argument(
      '--read-only',
      action='store_true',
      help='Allowing running multiple instances of emulators on the same AVD, '
      'but cannot save snapshot. This will be forced to False if emulator '
      'has a system snapshot.')
  subparser.add_argument('--no-read-only',
                         action='store_false',
                         dest='read_only')
  # TODO(crbug.com/40208043): Default to False when AVDs with sideloaded
  # system apks are rolled.
  subparser.set_defaults(read_only=True)
  subparser.add_argument(
      '--writable-system',
      action='store_true',
      default=False,
      help='Makes system & vendor image writable after adb remount. This will '
      'be forced to True, if emulator has a system snapshot.')
  subparser.add_argument(
      '--emulator-window',
      action='store_true',
      default=False,
      help='Enable graphical window display on the emulator.')
  subparser.add_argument(
      '--gpu-mode',
      help='Override the mode of hardware OpenGL ES emulation indicated by the '
      'AVD. See "emulator -help-gpu" for a full list of modes. Note when set '
      'to "host", it needs a valid DISPLAY env, even if "--emulator-window" is '
      'false, and it will not work under remote sessions like chrome remote '
      'desktop.')
  subparser.add_argument(
      '--debug-tags',
      help='Comma-separated list of debug tags. This can be used to enable or '
      'disable debug messages from specific parts of the emulator, e.g. '
      'init,snapshot. See "emulator -help-debug-tags" '
      'for a full list of tags.')
  subparser.add_argument(
      '--disk-size',
      help='Override the default disk size for the emulator instance.')
  subparser.add_argument(
      '--enable-network',
      action='store_true',
      help='Enable the network (WiFi and mobile data) on the emulator.')
  subparser.add_argument(
      '--require-fast-start',
      action='store_true',
      help='Shortens the start-up timeout and turns off the customization for '
      'local AVD run, e.g. larger disk space. Should be set when used by bots '
      'for AVD create or start.')

  def start_cmd(args):
    avd_config = avd.AvdConfig(args.avd_config)
    if not avd_config.IsAvailable():
      logging.warning('Emulator not up-to-date, installing (takes a minute)...')
      avd_config.Install()
      logging.warning('Starting emulator...')

    debug_tags = args.debug_tags
    if not debug_tags and args.verbose:
      debug_tags = 'time,init'

    inst = avd_config.CreateInstance()
    inst.Start(read_only=args.read_only,
               window=args.emulator_window,
               writable_system=args.writable_system,
               gpu_mode=args.gpu_mode,
               wipe_data=args.wipe_data,
               debug_tags=debug_tags,
               disk_size=args.disk_size,
               enable_network=args.enable_network,
               require_fast_start=args.require_fast_start)
    print('%s started (pid: %d)' % (str(inst), inst._emulator_proc.pid))
    return 0

  subparser.set_defaults(func=start_cmd)

  subparser = subparsers.add_parser(
      'list',
      help='Shows possible values for --avd-config.',
      formatter_class=argparse.ArgumentDefaultsHelpFormatter)
  _add_common_arguments(subparser)

  def list_cmd(args):
    files = pathlib.Path(__file__).parent.glob('proto/*.textpb')
    avd_configs = [avd.AvdConfig(os.path.relpath(f)) for f in files]
    avd_configs.sort(key=lambda c: c.avd_proto_path)
    fmt_string = '{:70} {:40} {}'
    print(
        fmt_string.format('Possible values for --avd-config:', 'Name',
                          'Active & Up-to-date'))
    for avd_config in avd_configs:
      print(
          fmt_string.format(avd_config.avd_proto_path, avd_config.avd_name,
                            avd_config.IsAvailable()))
    print()
    print('Warning: playstore images currently require --wipe-data. '
          'See: https://crbug.com/1116196')
    return 0

  subparser.set_defaults(func=list_cmd)

  if len(sys.argv) == 1:
    parser.print_help()
    return 1

  args = parser.parse_args(raw_args)

  logging_common.InitializeLogging(args)
  devil_chromium.Initialize(adb_path=args.adb_path)
  return args.func(args)


if __name__ == '__main__':
  sys.exit(main(sys.argv[1:]))