1#!/usr/bin/env python 2# 3# Copyright 2016 Google Inc. 4# 5# Use of this source code is governed by a BSD-style license that can be 6# found in the LICENSE file. 7 8 9import android_devices 10import default_flavor 11import os 12 13 14"""Android flavor utils, used for building for and running tests on Android.""" 15 16 17class _ADBWrapper(object): 18 """Wrapper for ADB.""" 19 def __init__(self, path_to_adb, serial, android_flavor): 20 self._adb = path_to_adb 21 self._serial = serial 22 self._wait_count = 0 23 self._android_flavor = android_flavor 24 25 def wait_for_device(self): 26 """Run 'adb wait-for-device'.""" 27 self._wait_count += 1 28 cmd = [ 29 os.path.join(self._android_flavor.android_bin, 'adb_wait_for_device'), 30 '-s', self._serial, 31 ] 32 self._android_flavor._bot_info.run( 33 cmd, env=self._android_flavor._default_env) 34 35 def maybe_wait_for_device(self): 36 """Run 'adb wait-for-device' if it hasn't already been run.""" 37 if self._wait_count == 0: 38 self.wait_for_device() 39 40 def __call__(self, *args, **kwargs): 41 self.maybe_wait_for_device() 42 return self._android_flavor._bot_info.run(self._adb + args, **kwargs) 43 44 45class AndroidFlavorUtils(default_flavor.DefaultFlavorUtils): 46 def __init__(self, skia_api): 47 super(AndroidFlavorUtils, self).__init__(skia_api) 48 self.device = self._bot_info.spec['device_cfg'] 49 slave_info = android_devices.SLAVE_INFO.get( 50 self._bot_info.slave_name, 51 android_devices.SLAVE_INFO['default']) 52 self.serial = slave_info.serial 53 self.android_bin = os.path.join( 54 self._bot_info.skia_dir, 'platform_tools', 'android', 'bin') 55 self._android_sdk_root = slave_info.android_sdk_root 56 self._adb = _ADBWrapper( 57 os.path.join(self._android_sdk_root, 'platform-tools', 'adb'), 58 self.serial, 59 self) 60 self._has_root = slave_info.has_root 61 self._default_env = {'ANDROID_SDK_ROOT': self._android_sdk_root, 62 'ANDROID_HOME': self._android_sdk_root, 63 'SKIA_ANDROID_VERBOSE_SETUP': '1'} 64 65 def step(self, name, cmd, env=None, **kwargs): 66 self._adb.maybe_wait_for_device() 67 args = [self.android_bin.join('android_run_skia'), 68 '--verbose', 69 '--logcat', 70 '-d', self.device, 71 '-s', self.serial, 72 '-t', self._bot_info.configuration, 73 ] 74 env = dict(env or {}) 75 env.update(self._default_env) 76 77 return self._bot_info.run(self._bot_info.m.step, name=name, cmd=args + cmd, 78 env=env, **kwargs) 79 80 def compile(self, target): 81 """Build the given target.""" 82 env = dict(self._default_env) 83 ccache = self._bot_info.ccache 84 if ccache: 85 env['ANDROID_MAKE_CCACHE'] = ccache 86 87 cmd = [os.path.join(self.android_bin, 'android_ninja'), target, 88 '-d', self.device] 89 if 'Clang' in self._bot_info.name: 90 cmd.append('--clang') 91 self._bot_info.run(cmd, env=env) 92 93 def device_path_join(self, *args): 94 """Like os.path.join(), but for paths on a connected Android device.""" 95 return '/'.join(args) 96 97 def device_path_exists(self, path): 98 """Like os.path.exists(), but for paths on a connected device.""" 99 exists_str = 'FILE_EXISTS' 100 return exists_str in self._adb( 101 name='exists %s' % self._bot_info.m.path.basename(path), 102 serial=self.serial, 103 cmd=['shell', 'if', '[', '-e', path, '];', 104 'then', 'echo', exists_str + ';', 'fi'], 105 stdout=self._bot_info.m.raw_io.output(), 106 infra_step=True 107 ).stdout 108 109 def _remove_device_dir(self, path): 110 """Remove the directory on the device.""" 111 self._adb(name='rmdir %s' % self._bot_info.m.path.basename(path), 112 serial=self.serial, 113 cmd=['shell', 'rm', '-r', path], 114 infra_step=True) 115 # Sometimes the removal fails silently. Verify that it worked. 116 if self.device_path_exists(path): 117 raise Exception('Failed to remove %s!' % path) # pragma: no cover 118 119 def _create_device_dir(self, path): 120 """Create the directory on the device.""" 121 self._adb(name='mkdir %s' % self._bot_info.m.path.basename(path), 122 serial=self.serial, 123 cmd=['shell', 'mkdir', '-p', path], 124 infra_step=True) 125 126 def copy_directory_contents_to_device(self, host_dir, device_dir): 127 """Like shutil.copytree(), but for copying to a connected device.""" 128 self._bot_info.run( 129 self._bot_info.m.step, 130 name='push %s' % self._bot_info.m.path.basename(host_dir), 131 cmd=[self.android_bin.join('adb_push_if_needed'), '--verbose', 132 '-s', self.serial, host_dir, device_dir], 133 env=self._default_env, 134 infra_step=True) 135 136 def copy_directory_contents_to_host(self, device_dir, host_dir): 137 """Like shutil.copytree(), but for copying from a connected device.""" 138 self._bot_info.run( 139 self._bot_info.m.step, 140 name='pull %s' % self._bot_info.m.path.basename(device_dir), 141 cmd=[self.android_bin.join('adb_pull_if_needed'), '--verbose', 142 '-s', self.serial, device_dir, host_dir], 143 env=self._default_env, 144 infra_step=True) 145 146 def copy_file_to_device(self, host_path, device_path): 147 """Like shutil.copyfile, but for copying to a connected device.""" 148 self._adb(name='push %s' % self._bot_info.m.path.basename(host_path), 149 serial=self.serial, 150 cmd=['push', host_path, device_path], 151 infra_step=True) 152 153 def create_clean_device_dir(self, path): 154 """Like shutil.rmtree() + os.makedirs(), but on a connected device.""" 155 self._remove_device_dir(path) 156 self._create_device_dir(path) 157 158 def install(self): 159 """Run device-specific installation steps.""" 160 if self._has_root: 161 self._adb(name='adb root', 162 serial=self.serial, 163 cmd=['root'], 164 infra_step=True) 165 # Wait for the device to reconnect. 166 self._bot_info.run( 167 self._bot_info.m.step, 168 name='wait', 169 cmd=['sleep', '10'], 170 infra_step=True) 171 self._adb.wait_for_device() 172 173 # TODO(borenet): Set CPU scaling mode to 'performance'. 174 self._bot_info.run(self._bot_info.m.step, 175 name='kill skia', 176 cmd=[self.android_bin.join('android_kill_skia'), 177 '--verbose', '-s', self.serial], 178 env=self._default_env, 179 infra_step=True) 180 if self._has_root: 181 self._adb(name='stop shell', 182 serial=self.serial, 183 cmd=['shell', 'stop'], 184 infra_step=True) 185 186 # Print out battery stats. 187 self._adb(name='starting battery stats', 188 serial=self.serial, 189 cmd=['shell', 'dumpsys', 'batteryproperties'], 190 infra_step=True) 191 192 def cleanup_steps(self): 193 """Run any device-specific cleanup steps.""" 194 self._adb(name='final battery stats', 195 serial=self.serial, 196 cmd=['shell', 'dumpsys', 'batteryproperties'], 197 infra_step=True) 198 self._adb(name='reboot', 199 serial=self.serial, 200 cmd=['reboot'], 201 infra_step=True) 202 self._bot_info.run( 203 self._bot_info.m.step, 204 name='wait for reboot', 205 cmd=['sleep', '10'], 206 infra_step=True) 207 self._adb.wait_for_device() 208 209 def read_file_on_device(self, path, *args, **kwargs): 210 """Read the given file.""" 211 return self._adb(name='read %s' % self._bot_info.m.path.basename(path), 212 serial=self.serial, 213 cmd=['shell', 'cat', path], 214 stdout=self._bot_info.m.raw_io.output(), 215 infra_step=True).stdout.rstrip() 216 217 def remove_file_on_device(self, path, *args, **kwargs): 218 """Delete the given file.""" 219 return self._adb(name='rm %s' % self._bot_info.m.path.basename(path), 220 serial=self.serial, 221 cmd=['shell', 'rm', '-f', path], 222 infra_step=True, 223 *args, 224 **kwargs) 225 226 def get_device_dirs(self): 227 """ Set the directories which will be used by the build steps.""" 228 device_scratch_dir = self._adb( 229 name='get EXTERNAL_STORAGE dir', 230 serial=self.serial, 231 cmd=['shell', 'echo', '$EXTERNAL_STORAGE'], 232 ) 233 prefix = self.device_path_join(device_scratch_dir, 'skiabot', 'skia_') 234 return default_flavor.DeviceDirs( 235 dm_dir=prefix + 'dm', 236 perf_data_dir=prefix + 'perf', 237 resource_dir=prefix + 'resources', 238 images_dir=prefix + 'images', 239 skp_dir=prefix + 'skp/skps', 240 tmp_dir=prefix + 'tmp_dir') 241 242