# Copyright 2014 The Chromium Authors. All rights reserved. # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. # pylint: disable=W0201 import copy import default_flavor """Android flavor utils, used for building for and running tests on Android.""" def get_device(api): builder_cfg = api.builder_name_schema.DictForBuilderName( api.vars.builder_name) if 'Android' in builder_cfg.get('extra_config', ''): # Compile bots. if 'NoNeon' in builder_cfg['extra_config']: return 'arm_v7' return { 'Arm64': 'arm64', 'x86': 'x86', 'x86_64': 'x86_64', 'Mips': 'mips', 'Mips64': 'mips64', 'MipsDSP2': 'mips_dsp2', }.get(builder_cfg['target_arch'], 'arm_v7_neon') else: # Test/Perf bots. return { 'AndroidOne': 'arm_v7_neon', 'GalaxyS3': 'arm_v7_neon', 'GalaxyS4': 'arm_v7_neon', 'NVIDIA_Shield': 'arm64', 'Nexus10': 'arm_v7_neon', 'Nexus5': 'arm_v7_neon', 'Nexus6': 'arm_v7_neon', 'Nexus6p': 'arm_v7_neon', 'Nexus7': 'arm_v7_neon', 'Nexus7v2': 'arm_v7_neon', 'Nexus9': 'arm64', 'NexusPlayer': 'x86', }[builder_cfg['model']] class _ADBWrapper(object): """Wrapper for the ADB recipe module. The ADB recipe module looks for the ADB binary at a path we don't have checked out on our bots. This wrapper ensures that we set a custom ADB path before attempting to use the module. """ def __init__(self, m, path_to_adb, serial_args, android_flavor): self.m = m self.m.adb.set_adb_path(path_to_adb) self._has_root = False # This is set in install(). self._serial_args = serial_args self._wait_count = 0 self._android_flavor = android_flavor def wait_for_device(self): """Run 'adb wait-for-device'.""" self._wait_count += 1 cmd = [ self._android_flavor.android_bin.join('adb_wait_for_device') ] + self._serial_args self.m.run( self.m.step, name='wait for device (%d)' % self._wait_count, cmd=cmd, env=self._android_flavor._default_env, infra_step=True) cmd = [ self._android_flavor.android_bin.join('adb_wait_for_charge'), ] + self._serial_args self.m.run( self.m.step, name='wait for charge (%d)' % self._wait_count, cmd=cmd, env=self._android_flavor._default_env, infra_step=True) def maybe_wait_for_device(self): """Run 'adb wait-for-device' if it hasn't already been run.""" if self._wait_count == 0: self.wait_for_device() def __call__(self, *args, **kwargs): self.maybe_wait_for_device() return self.m.run(self.m.adb, *args, **kwargs) class AndroidFlavorUtils(default_flavor.DefaultFlavorUtils): def __init__(self, m): super(AndroidFlavorUtils, self).__init__(m) self.device = get_device(m) self.android_bin = self.m.vars.skia_dir.join( 'platform_tools', 'android', 'bin') self._android_sdk_root = self.m.vars.slave_dir.join( 'android_sdk', 'android-sdk') self.serial = None self.serial_args = [] try: path_to_adb = self.m.step( 'which adb', cmd=['which', 'adb'], stdout=self.m.raw_io.output(), infra_step=True).stdout.rstrip() except self.m.step.StepFailure: path_to_adb = self.m.path.join(self._android_sdk_root, 'platform-tools', 'adb') self._adb = _ADBWrapper( self.m, path_to_adb, self.serial_args, self) self._default_env = {'ANDROID_SDK_ROOT': self._android_sdk_root, 'ANDROID_HOME': self._android_sdk_root, 'SKIA_ANDROID_VERBOSE_SETUP': 1} def step(self, name, cmd, env=None, **kwargs): self._adb.maybe_wait_for_device() args = [ self.android_bin.join('android_run_skia'), '--verbose', '--logcat', '-d', self.device, ] + self.serial_args + [ '-t', self.m.vars.configuration, ] env = dict(env or {}) env.update(self._default_env) return self.m.run(self.m.step, name=name, cmd=args + cmd, env=env, **kwargs) def compile(self, target, **kwargs): """Build the given target.""" env = kwargs.pop('env', {}) env.update(dict(self._default_env)) ccache = self.m.run.ccache() if ccache: env['ANDROID_MAKE_CCACHE'] = ccache cmd = [self.android_bin.join('android_ninja'), target, '-d', self.device] if 'Clang' in self.m.vars.builder_name: cmd.append('--clang') if 'GCC' in self.m.vars.builder_name: cmd.append('--gcc') if 'Vulkan' in self.m.vars.builder_name: cmd.append('--vulkan') self.m.run(self.m.step, 'build %s' % target, cmd=cmd, env=env, cwd=self.m.path['checkout'], **kwargs) def device_path_join(self, *args): """Like os.path.join(), but for paths on a connected Android device.""" return '/'.join(args) def device_path_exists(self, path): """Like os.path.exists(), but for paths on a connected device.""" exists_str = 'FILE_EXISTS' return exists_str in self._adb( name='exists %s' % self.m.path.basename(path), serial=self.serial, cmd=['shell', 'if', '[', '-e', path, '];', 'then', 'echo', exists_str + ';', 'fi'], stdout=self.m.raw_io.output(), infra_step=True ).stdout def _remove_device_dir(self, path): """Remove the directory on the device.""" self._adb(name='rmdir %s' % self.m.path.basename(path), serial=self.serial, cmd=['shell', 'rm', '-r', path], infra_step=True) # Sometimes the removal fails silently. Verify that it worked. if self.device_path_exists(path): raise Exception('Failed to remove %s!' % path) # pragma: no cover def _create_device_dir(self, path): """Create the directory on the device.""" self._adb(name='mkdir %s' % self.m.path.basename(path), serial=self.serial, cmd=['shell', 'mkdir', '-p', path], infra_step=True) def copy_directory_contents_to_device(self, host_dir, device_dir): """Like shutil.copytree(), but for copying to a connected device.""" self.m.run( self.m.step, name='push %s' % self.m.path.basename(host_dir), cmd=[ self.android_bin.join('adb_push_if_needed'), '--verbose', ] + self.serial_args + [ host_dir, device_dir, ], env=self._default_env, infra_step=True) def copy_directory_contents_to_host(self, device_dir, host_dir): """Like shutil.copytree(), but for copying from a connected device.""" self.m.run( self.m.step, name='pull %s' % self.m.path.basename(device_dir), cmd=[ self.android_bin.join('adb_pull_if_needed'), '--verbose', ] + self.serial_args + [ device_dir, host_dir, ], env=self._default_env, infra_step=True) def copy_file_to_device(self, host_path, device_path): """Like shutil.copyfile, but for copying to a connected device.""" self._adb(name='push %s' % self.m.path.basename(host_path), serial=self.serial, cmd=['push', host_path, device_path], infra_step=True) def create_clean_device_dir(self, path): """Like shutil.rmtree() + os.makedirs(), but on a connected device.""" self._remove_device_dir(path) self._create_device_dir(path) def has_root(self): """Determine if we have root access on this device.""" # Special case: GalaxyS3 hangs on `adb root`. Don't bother. if 'GalaxyS3' in self.m.vars.builder_name: return False # Determine if we have root access. has_root = False try: output = self._adb(name='adb root', serial=self.serial, cmd=['root'], stdout=self.m.raw_io.output(), infra_step=True).stdout.rstrip() if ('restarting adbd as root' in output or 'adbd is already running as root' in output): has_root = True except self.m.step.StepFailure: # pragma: nocover pass # Wait for the device to reconnect. self.m.run( self.m.step, name='wait', cmd=['sleep', '10'], infra_step=True) self._adb.wait_for_device() return has_root def install(self): """Run device-specific installation steps.""" device_scratch_dir = self._adb( name='get EXTERNAL_STORAGE dir', serial=self.serial, cmd=['shell', 'echo', '$EXTERNAL_STORAGE'], stdout=self.m.raw_io.output(), infra_step=True, ).stdout.rstrip() prefix = self.device_path_join(device_scratch_dir, 'skiabot', 'skia_') self.device_dirs = default_flavor.DeviceDirs( dm_dir=prefix + 'dm', perf_data_dir=prefix + 'perf', resource_dir=prefix + 'resources', images_dir=prefix + 'images', skp_dir=prefix + 'skp/skps', svg_dir=prefix + 'svg/svgs', tmp_dir=prefix + 'tmp_dir') self._has_root = self.has_root() self.m.run(self.m.step, name='kill skia', cmd=[ self.android_bin.join('android_kill_skia'), '--verbose', ] + self.serial_args, env=self._default_env, infra_step=True) if self._has_root: self._adb(name='stop shell', serial=self.serial, cmd=['shell', 'stop'], infra_step=True) # Print out battery stats. self._adb(name='starting battery stats', serial=self.serial, cmd=['shell', 'dumpsys', 'batteryproperties'], infra_step=True) # Print out CPU scale info. if self._has_root: self._adb(name='cat scaling_governor', serial=self.serial, cmd=['shell', 'cat', '/sys/devices/system/cpu/cpu0/cpufreq/scaling_governor'], infra_step=True) self._adb(name='cat cpu_freq', serial=self.serial, cmd=['shell', 'cat', '/sys/devices/system/cpu/cpu0/cpufreq/scaling_cur_freq'], infra_step=True) def cleanup_steps(self): """Run any device-specific cleanup steps.""" if self.m.vars.role in (self.m.builder_name_schema.BUILDER_ROLE_TEST, self.m.builder_name_schema.BUILDER_ROLE_PERF): self._adb(name='final battery stats', serial=self.serial, cmd=['shell', 'dumpsys', 'batteryproperties'], infra_step=True) self._adb(name='reboot', serial=self.serial, cmd=['reboot'], infra_step=True) self.m.run( self.m.step, name='wait for reboot', cmd=['sleep', '10'], infra_step=True) self._adb.wait_for_device() # The ADB binary conflicts with py-adb used by swarming. Kill it # when finished to play nice. self._adb(name='kill-server', serial=self.serial, cmd=['kill-server'], infra_step=True) def read_file_on_device(self, path, *args, **kwargs): """Read the given file.""" return self._adb(name='read %s' % self.m.path.basename(path), serial=self.serial, cmd=['shell', 'cat', path], stdout=self.m.raw_io.output(), infra_step=True).stdout.rstrip() def remove_file_on_device(self, path, *args, **kwargs): """Delete the given file.""" return self._adb(name='rm %s' % self.m.path.basename(path), serial=self.serial, cmd=['shell', 'rm', '-f', path], infra_step=True, *args, **kwargs)