android.py (9893B)
1 # mypy: allow-untyped-defs 2 3 import argparse 4 import os 5 import platform 6 import signal 7 import shutil 8 import subprocess 9 import threading 10 11 import requests 12 from .wpt import venv_dir 13 14 android_device = None 15 16 here = os.path.abspath(os.path.dirname(__file__)) 17 wpt_root = os.path.abspath(os.path.join(here, os.pardir, os.pardir)) 18 19 CMDLINE_TOOLS_VERSION_STRING = "12.0" 20 CMDLINE_TOOLS_VERSION = "11076708" 21 22 AVD_MANIFEST_X86_64 = { 23 "emulator_package": "system-images;android-34;google_apis;x86_64", 24 "emulator_avd_name": "mozemulator-android34-x86_64" 25 } 26 27 28 def do_delayed_imports(paths): 29 global android_device 30 from mozrunner.devices import android_device 31 32 android_device.TOOLTOOL_PATH = os.path.join(os.path.dirname(__file__), 33 os.pardir, 34 "third_party", 35 "tooltool", 36 "tooltool.py") 37 android_device.EMULATOR_HOME_DIR = paths["emulator_home"] 38 android_device.AVD_DICT["x86_64"] = android_device.AvdInfo( 39 "Android x86_64", 40 "mozemulator-android34-x86_64", 41 [ 42 "-skip-adb-auth", 43 "-verbose", 44 "-show-kernel", 45 "-ranchu", 46 "-selinux", 47 "permissive", 48 "-memory", 49 "4096", 50 "-cores", 51 "4", 52 "-prop", 53 "ro.test_harness=true", 54 "-no-snapstorage", 55 "-no-snapshot", 56 "-skin", 57 "800x1280" 58 ], 59 True, 60 ) 61 62 63 def get_parser_install(): 64 parser = argparse.ArgumentParser() 65 parser.add_argument("--path", dest="dest", 66 help="Root path to use for emulator tooling") 67 parser.add_argument("--reinstall", action="store_true", 68 help="Force reinstall even if the emulator already exists") 69 parser.add_argument("--prompt", action="store_true", 70 help="Enable confirmation prompts") 71 parser.add_argument("--no-prompt", dest="prompt", action="store_false", 72 help="Skip confirmation prompts") 73 return parser 74 75 76 def get_parser_start(): 77 parser = get_parser_install() 78 parser.add_argument("--device-serial", 79 help="Device serial number for Android emulator, if not emulator-5554") 80 return parser 81 82 83 def get_paths(dest): 84 os_name = platform.system().lower() 85 86 if dest is None: 87 # os.getcwd() doesn't include the venv path 88 base_path = os.path.join(wpt_root, venv_dir(), "android") 89 else: 90 base_path = dest 91 92 sdk_path = os.environ.get("ANDROID_SDK_HOME", os.path.join(base_path, f"android-sdk-{os_name}")) 93 avd_path = os.environ.get("ANDROID_AVD_HOME", os.path.join(sdk_path, ".android", "avd")) 94 return { 95 "base": base_path, 96 "sdk": sdk_path, 97 "sdk_tools": os.path.join(sdk_path, "cmdline-tools", CMDLINE_TOOLS_VERSION_STRING), 98 "avd": avd_path, 99 "emulator_home": os.path.dirname(avd_path) 100 } 101 102 103 def get_sdk_manager_path(paths): 104 os_name = platform.system().lower() 105 file_name = "sdkmanager" 106 if os_name.startswith("win"): 107 file_name += ".bat" 108 return os.path.join(paths["sdk_tools"], "bin", file_name) 109 110 111 def get_avd_manager(paths): 112 os_name = platform.system().lower() 113 file_name = "avdmanager" 114 if os_name.startswith("win"): 115 file_name += ".bat" 116 return os.path.join(paths["sdk_tools"], "bin", file_name) 117 118 119 def uninstall_sdk(paths): 120 if os.path.exists(paths["sdk"]) and os.path.isdir(paths["sdk"]): 121 shutil.rmtree(paths["sdk"]) 122 123 124 def get_os_tag(logger): 125 os_name = platform.system().lower() 126 if os_name not in ["darwin", "linux", "windows"]: 127 logger.critical("Unsupported platform %s" % os_name) 128 raise NotImplementedError 129 130 if os_name == "macosx": 131 return "darwin" 132 if os_name == "windows": 133 return "win" 134 return "linux" 135 136 137 def download_and_extract(url, path): 138 if not os.path.exists(path): 139 os.makedirs(path) 140 temp_path = os.path.join(path, url.rsplit("/", 1)[1]) 141 try: 142 with open(temp_path, "wb") as f: 143 with requests.get(url, stream=True) as resp: 144 for chunk in resp.iter_content(2**16): 145 f.write(chunk) 146 if not os.path.exists(temp_path): 147 raise ValueError(f"Failed to download {url}, output path doesn't exist") 148 # Python's zipfile module doesn't seem to work here 149 subprocess.check_call(["unzip", temp_path], cwd=path) 150 finally: 151 if os.path.exists(temp_path): 152 os.unlink(temp_path) 153 154 155 def install_sdk(logger, paths): 156 if os.path.isdir(paths["sdk_tools"]): 157 logger.info("Using SDK installed at %s" % paths["sdk_tools"]) 158 return False 159 160 if not os.path.exists(paths["sdk"]): 161 os.makedirs(paths["sdk"]) 162 163 download_path = os.path.dirname(paths["sdk_tools"]) 164 165 url = f'https://dl.google.com/android/repository/commandlinetools-{get_os_tag(logger)}-{CMDLINE_TOOLS_VERSION}_latest.zip' 166 logger.info("Getting SDK from %s" % url) 167 168 download_and_extract(url, download_path) 169 os.rename(os.path.join(download_path, "cmdline-tools"), paths["sdk_tools"]) 170 171 return True 172 173 174 def install_android_packages(logger, paths, packages, prompt=True): 175 sdk_manager = get_sdk_manager_path(paths) 176 if not os.path.exists(sdk_manager): 177 raise OSError(f"Can't find sdkmanager at {sdk_manager}") 178 179 # TODO: make this work non-internactively 180 logger.info(f"Installing Android packages {' '.join(packages)}") 181 cmd = [sdk_manager] + packages 182 183 input_data = None if prompt else "\n".join(["y"] * 100).encode("UTF-8") 184 subprocess.run(cmd, check=True, input=input_data) 185 186 187 def install_avd(logger, paths, prompt=True): 188 avd_manager = get_avd_manager(paths) 189 avd_manifest = AVD_MANIFEST_X86_64 190 191 install_android_packages(logger, paths, [avd_manifest["emulator_package"]], prompt=prompt) 192 193 cmd = [avd_manager, 194 "--verbose", 195 "create", 196 "avd", 197 "--force", 198 "--name", 199 avd_manifest["emulator_avd_name"], 200 "--package", 201 avd_manifest["emulator_package"]] 202 input_data = None if prompt else b"no" 203 subprocess.run(cmd, check=True, input=input_data) 204 205 206 def get_emulator(paths, device_serial=None): 207 if android_device is None: 208 do_delayed_imports(paths) 209 210 substs = {"top_srcdir": wpt_root, "TARGET_CPU": "x86"} 211 emulator = android_device.AndroidEmulator(substs=substs, 212 device_serial=device_serial, 213 verbose=True) 214 emulator.emulator_path = os.path.join(paths["sdk"], "emulator", "emulator") 215 return emulator 216 217 218 class Environ: 219 def __init__(self, **kwargs): 220 self.environ = None 221 self.set_environ = kwargs 222 223 def __enter__(self): 224 self.environ = os.environ.copy() 225 for key, value in self.set_environ.items(): 226 if value is None: 227 if key in os.environ: 228 del os.environ[key] 229 else: 230 os.environ[key] = value 231 232 def __exit__(self, *args, **kwargs): 233 os.environ = self.environ 234 235 236 def android_environment(paths): 237 return Environ(ANDROID_EMULATOR_HOME=paths["emulator_home"], 238 ANDROID_AVD_HOME=paths["avd"], 239 ANDROID_SDK_ROOT=paths["sdk"], 240 ANDROID_SDK_HOME=paths["sdk"]) 241 242 243 def install(logger, dest=None, reinstall=False, prompt=True): 244 paths = get_paths(dest) 245 246 with android_environment(paths): 247 248 if reinstall: 249 uninstall_sdk(paths) 250 251 new_install = install_sdk(logger, paths) 252 253 if new_install: 254 packages = ["platform-tools", 255 "build-tools;36.1.0", 256 "platforms;android-36.1", 257 "emulator"] 258 259 install_android_packages(logger, paths, packages, prompt=prompt) 260 261 install_avd(logger, paths, prompt=prompt) 262 263 emulator = get_emulator(paths) 264 return emulator 265 266 267 def cancel_start(thread_id): 268 def cancel_func(): 269 raise signal.pthread_kill(thread_id, signal.SIGINT) 270 return cancel_func 271 272 273 def start(logger, dest=None, reinstall=False, prompt=True, device_serial=None): 274 paths = get_paths(dest) 275 276 with android_environment(paths): 277 install(logger, dest=dest, reinstall=reinstall, prompt=prompt) 278 279 emulator = get_emulator(paths, device_serial=device_serial) 280 281 if not emulator.check_avd(): 282 logger.critical("Android AVD not found, please run |wpt install-android-emulator|") 283 raise OSError 284 285 emulator.start() 286 timer = threading.Timer(300, cancel_start(threading.get_ident())) 287 timer.start() 288 for i in range(10): 289 logger.info(f"Wait for emulator to start attempt {i + 1}/10") 290 try: 291 emulator.wait_for_start() 292 except Exception: 293 import traceback 294 logger.warning(f"""emulator.wait_for_start() failed: 295 {traceback.format_exc()}""") 296 else: 297 break 298 timer.cancel() 299 return emulator 300 301 302 def run_install(venv, **kwargs): 303 try: 304 import logging 305 logging.basicConfig() 306 logger = logging.getLogger() 307 308 install(logger, **kwargs) 309 except Exception: 310 import traceback 311 traceback.print_exc() 312 import pdb 313 pdb.post_mortem() 314 315 316 def run_start(venv, **kwargs): 317 try: 318 import logging 319 logging.basicConfig() 320 logger = logging.getLogger() 321 322 start(logger, **kwargs) 323 except Exception: 324 import traceback 325 traceback.print_exc() 326 import pdb 327 pdb.post_mortem()