1# Copyright 2017 The Chromium Authors. All rights reserved. 2# Use of this source code is governed by a BSD-style license that can be 3# found in the LICENSE file. 4 5 6from . import default 7 8 9"""iOS flavor, used for running code on iOS.""" 10 11 12class iOSFlavor(default.DefaultFlavor): 13 def __init__(self, m, app_name): 14 super(iOSFlavor, self).__init__(m, app_name) 15 self.device_dirs = default.DeviceDirs( 16 bin_dir='[unused]', 17 dm_dir='dm', 18 perf_data_dir='perf', 19 resource_dir='resources', 20 fonts_dir = 'NOT_SUPPORTED', 21 images_dir='images', 22 lotties_dir='lotties', 23 skp_dir='skps', 24 svg_dir='svgs', 25 tmp_dir='tmp', 26 texttraces_dir='') 27 28 @property 29 def env(self): 30 return { 31 'IOS_BUNDLE_ID': 'com.google.%s' % self.app_name, 32 'IOS_MOUNT_POINT': self.m.vars.workdir.joinpath('mnt_iosdevice'), 33 } 34 35 def context(self): 36 return self.m.context(env=self.env) 37 38 def _run(self, title, *cmd, **kwargs): 39 def sleep(attempt): 40 self.m.step( 41 'sleep before attempt %d' % attempt, 42 cmd=['sleep', '2']) # pragma: nocover 43 44 return self.m.run.with_retry(self.m.step, title, 3, cmd=list(cmd), 45 between_attempts_fn=sleep, **kwargs) 46 47 def install(self): 48 with self.context(): 49 self._install() 50 51 def _install(self): 52 # We assume a single device is connected. 53 54 # Kill usbmuxd. This tends to help with connection problems. 55 if self.m.platform.is_mac: 56 self.m.step('killall usbmuxd', ['sudo', '/usr/bin/killall', '-v', 'usbmuxd']) 57 self.m.step('sleep 10', ['sleep', '10']) 58 59 # Pair the device. 60 try: 61 self.m.run(self.m.step, 'check if device is paired', 62 cmd=['idevicepair', 'validate'], 63 infra_step=True, abort_on_failure=True, 64 fail_build_on_failure=False) 65 except self.m.step.StepFailure: # pragma: nocover 66 self._run('pair device', 'idevicepair', 'pair') # pragma: nocover 67 68 # Mount developer image. 69 image_info = self._run('list mounted image', 70 'ideviceimagemounter', '--list', 71 stdout=self.m.raw_io.output()) 72 image_info_out = ( 73 image_info.stdout.decode('utf-8').strip() if image_info.stdout else '') 74 75 if 'ImageSignature' not in image_info_out: 76 image_pkgs = self.m.file.glob_paths('locate ios-dev-image package', 77 self.m.path.start_dir, 78 'ios-dev-image*', 79 test_data=['ios-dev-image-13.2']) 80 if len(image_pkgs) != 1: 81 raise Exception('glob for ios-dev-image* returned %s' 82 % image_pkgs) # pragma: nocover 83 84 image_pkg = image_pkgs[0] 85 contents = self.m.file.listdir( 86 'locate image and signature', image_pkg, 87 test_data=['DeveloperDiskImage.dmg', 88 'DeveloperDiskImage.dmg.signature']) 89 image = None 90 sig = None 91 for f in contents: 92 if str(f).endswith('.dmg'): 93 image = f 94 if str(f).endswith('.dmg.signature'): 95 sig = f 96 if not image or not sig: 97 raise Exception('%s does not contain *.dmg and *.dmg.signature' % 98 image_pkg) # pragma: nocover 99 100 self._run('mount developer image', 'ideviceimagemounter', image, sig) 101 102 # Install XCode. 103 if self.m.platform.is_mac: 104 self.m.xcode.install() 105 106 # Install app (necessary before copying any resources to the device). 107 if self.app_name: 108 app_package = self.host_dirs.bin_dir.joinpath('%s.app' % self.app_name) 109 110 def uninstall_app(attempt): 111 # If app ID changes, upgrade will fail, so try uninstalling. 112 self.m.run(self.m.step, 113 'uninstall %s' % self.app_name, 114 cmd=['ideviceinstaller', '-U', 115 'com.google.%s' % self.app_name], 116 infra_step=True, 117 # App may not be installed. 118 abort_on_failure=False, fail_build_on_failure=False) 119 120 num_attempts = 2 121 self.m.run.with_retry(self.m.step, 'install %s' % self.app_name, 122 num_attempts, 123 cmd=['ideviceinstaller', '-i', app_package], 124 between_attempts_fn=uninstall_app, 125 infra_step=True) 126 127 def step(self, name, cmd, **kwargs): 128 app_name = cmd[0] 129 app_package = self.host_dirs.bin_dir.joinpath('%s.app' % app_name) 130 bundle_id = 'com.google.%s' % app_name 131 if self.m.platform.is_mac: 132 args = [self.m.xcode.path, app_package, bundle_id] + [str(ele) for ele in cmd[1:]] 133 with self.context(): 134 self.m.run( 135 self.m.step, name, 136 cmd=['python3', self.module.resource('ios_xcode_run.py')] + args) 137 else: 138 args = [bundle_id] + [str(ele) for ele in cmd[1:]] 139 success = False 140 with self.context(): 141 try: 142 self.m.run(self.m.step, name, cmd=['idevicedebug', 'run'] + args) 143 success = True 144 finally: 145 if not success: 146 self.m.run( 147 self.m.step, '%s with full debug output' % name, 148 cmd=['python3', self.module.resource('ios_debug_cmd.py')] + args) 149 150 def _run_ios_script(self, script, first, *rest): 151 with self.context(): 152 full = self.m.path.start_dir.joinpath( 153 'skia', 'platform_tools', 'ios', 'bin', 'ios_' + script) 154 self.m.run(self.m.step, 155 name = '%s %s' % (script, first), 156 cmd = [full, first] + list(rest), 157 infra_step=True) 158 159 def copy_file_to_device(self, host, device): 160 self._run_ios_script('push_file', host, device) 161 162 def copy_directory_contents_to_device(self, host, device): 163 self._run_ios_script('push_if_needed', host, device) 164 165 def copy_directory_contents_to_host(self, device, host): 166 self._run_ios_script('pull_if_needed', device, host) 167 168 def remove_file_on_device(self, path): 169 self._run_ios_script('rm', path) 170 171 def create_clean_device_dir(self, path): 172 self._run_ios_script('rm', path) 173 self._run_ios_script('mkdir', path) 174 175 def read_file_on_device(self, path, **kwargs): 176 with self.context(): 177 full = self.m.path.start_dir.joinpath( 178 'skia', 'platform_tools', 'ios', 'bin', 'ios_cat_file') 179 rv = self.m.run(self.m.step, 180 name = 'cat_file %s' % path, 181 cmd = [full, path], 182 stdout=self.m.raw_io.output(), 183 infra_step=True, 184 **kwargs) 185 return rv.stdout.decode('utf-8').rstrip() if rv and rv.stdout else None 186