1# This Source Code Form is subject to the terms of the Mozilla Public 2# License, v. 2.0. If a copy of the MPL was not distributed with this, 3# file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 5from __future__ import absolute_import, print_function, unicode_literals 6 7import errno 8import json 9import os 10import stat 11import subprocess 12import sys 13import time 14import requests 15from typing import Optional, Union 16from pathlib import Path 17from tqdm import tqdm 18 19# We need the NDK version in multiple different places, and it's inconvenient 20# to pass down the NDK version to all relevant places, so we have this global 21# variable. 22from mozboot.bootstrap import MOZCONFIG_SUGGESTION_TEMPLATE 23 24NDK_VERSION = "r21d" 25CMDLINE_TOOLS_VERSION_STRING = "5.0" 26CMDLINE_TOOLS_VERSION = "7583922" 27 28BUNDLETOOL_VERSION = "1.8.0" 29 30# We expect the emulator AVD definitions to be platform agnostic 31LINUX_X86_64_ANDROID_AVD = "linux64-android-avd-x86_64-repack" 32LINUX_ARM_ANDROID_AVD = "linux64-android-avd-arm-repack" 33 34MACOS_X86_64_ANDROID_AVD = "linux64-android-avd-x86_64-repack" 35MACOS_ARM_ANDROID_AVD = "linux64-android-avd-arm-repack" 36MACOS_ARM64_ANDROID_AVD = "linux64-android-avd-arm64-repack" 37 38WINDOWS_X86_64_ANDROID_AVD = "linux64-android-avd-x86_64-repack" 39WINDOWS_ARM_ANDROID_AVD = "linux64-android-avd-arm-repack" 40 41AVD_MANIFEST_X86_64 = Path(__file__).resolve().parent / "android-avds/x86_64.json" 42AVD_MANIFEST_ARM = Path(__file__).resolve().parent / "android-avds/arm.json" 43AVD_MANIFEST_ARM64 = Path(__file__).resolve().parent / "android-avds/arm64.json" 44 45JAVA_VERSION_MAJOR = "17" 46JAVA_VERSION_MINOR = "0.1" 47JAVA_VERSION_PATCH = "12" 48 49ANDROID_NDK_EXISTS = """ 50Looks like you have the correct version of the Android NDK installed at: 51%s 52""" 53 54ANDROID_SDK_EXISTS = """ 55Looks like you have the Android SDK installed at: 56%s 57We will install all required Android packages. 58""" 59 60ANDROID_SDK_TOO_OLD = """ 61Looks like you have an outdated Android SDK installed at: 62%s 63I can't update outdated Android SDKs to have the required 'sdkmanager' 64tool. Move it out of the way (or remove it entirely) and then run 65bootstrap again. 66""" 67 68INSTALLING_ANDROID_PACKAGES = """ 69We are now installing the following Android packages: 70%s 71You may be prompted to agree to the Android license. You may see some of 72output as packages are downloaded and installed. 73""" 74 75MOBILE_ANDROID_MOZCONFIG_TEMPLATE = """ 76# Build GeckoView/Firefox for Android: 77ac_add_options --enable-application=mobile/android 78 79# Targeting the following architecture. 80# For regular phones, no --target is needed. 81# For x86 emulators (and x86 devices, which are uncommon): 82# ac_add_options --target=i686 83# For newer phones or Apple silicon 84# ac_add_options --target=aarch64 85# For x86_64 emulators (and x86_64 devices, which are even less common): 86# ac_add_options --target=x86_64 87 88{extra_lines} 89""" 90 91MOBILE_ANDROID_ARTIFACT_MODE_MOZCONFIG_TEMPLATE = """ 92# Build GeckoView/Firefox for Android Artifact Mode: 93ac_add_options --enable-application=mobile/android 94ac_add_options --target=arm-linux-androideabi 95ac_add_options --enable-artifact-builds 96 97{extra_lines} 98# Write build artifacts to: 99mk_add_options MOZ_OBJDIR=./objdir-frontend 100""" 101 102 103class GetNdkVersionError(Exception): 104 pass 105 106 107def install_bundletool(url, path: Path): 108 """ 109 Fetch bundletool to the desired directory. 110 """ 111 try: 112 subprocess.check_call( 113 ["wget", "--continue", url, "--output-document", "bundletool.jar"], 114 cwd=str(path), 115 ) 116 finally: 117 pass 118 119 120def install_mobile_android_sdk_or_ndk(url, path: Path): 121 """ 122 Fetch an Android SDK or NDK from |url| and unpack it into the given |path|. 123 124 We use, and 'requests' respects, https. We could also include SHAs for a 125 small improvement in the integrity guarantee we give. But this script is 126 bootstrapped over https anyway, so it's a really minor improvement. 127 128 We keep a cache of the downloaded artifacts, writing into |path|/mozboot. 129 We don't yet clean the cache; it's better to waste some disk space and 130 not require a long re-download than to wipe the cache prematurely. 131 """ 132 133 download_path = path / "mozboot" 134 try: 135 download_path.mkdir(parents=True) 136 except OSError as e: 137 if e.errno == errno.EEXIST and download_path.is_dir(): 138 pass 139 else: 140 raise 141 142 file_name = url.split("/")[-1] 143 download_file_path = download_path / file_name 144 145 with requests.Session() as session: 146 request = session.head(url) 147 remote_file_size = int(request.headers["content-length"]) 148 149 if download_file_path.is_file(): 150 local_file_size = download_file_path.stat().st_size 151 152 if local_file_size == remote_file_size: 153 print(f"{download_file_path} already downloaded. Skipping download...") 154 else: 155 print( 156 f"Partial download detected. Resuming download of {download_file_path}..." 157 ) 158 download( 159 download_file_path, 160 session, 161 url, 162 remote_file_size, 163 local_file_size, 164 ) 165 else: 166 print(f"Downloading {download_file_path}...") 167 download(download_file_path, session, url, remote_file_size) 168 169 if file_name.endswith(".tar.gz") or file_name.endswith(".tgz"): 170 cmd = ["tar", "zxf", str(download_file_path)] 171 elif file_name.endswith(".tar.bz2"): 172 cmd = ["tar", "jxf", str(download_file_path)] 173 elif file_name.endswith(".zip"): 174 cmd = ["unzip", "-q", str(download_file_path)] 175 elif file_name.endswith(".bin"): 176 # Execute the .bin file, which unpacks the content. 177 mode = os.stat(path).st_mode 178 download_file_path.chmod(mode | stat.S_IXUSR) 179 cmd = [str(download_file_path)] 180 else: 181 raise NotImplementedError(f"Don't know how to unpack file: {file_name}") 182 183 print(f"Unpacking {download_file_path}...") 184 185 with open(os.devnull, "w") as stdout: 186 # These unpack commands produce a ton of output; ignore it. The 187 # .bin files are 7z archives; there's no command line flag to quiet 188 # output, so we use this hammer. 189 subprocess.check_call(cmd, stdout=stdout, cwd=str(path)) 190 191 print(f"Unpacking {download_file_path}... DONE") 192 # Now delete the archive 193 download_file_path.unlink() 194 195 196def download( 197 download_file_path: Path, 198 session, 199 url, 200 remote_file_size, 201 resume_from_byte_pos: int = None, 202): 203 """ 204 Handles both a fresh SDK/NDK download, as well as resuming a partial one 205 """ 206 # "ab" will behave same as "wb" if file does not exist 207 with open(download_file_path, "ab") as file: 208 # 64 KB/s should be fine on even the slowest internet connections 209 chunk_size = 1024 * 64 210 # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Range#directives 211 resume_header = ( 212 {"Range": f"bytes={resume_from_byte_pos}-"} 213 if resume_from_byte_pos 214 else None 215 ) 216 217 request = session.get( 218 url, stream=True, allow_redirects=True, headers=resume_header 219 ) 220 221 with tqdm( 222 total=int(remote_file_size), 223 unit="B", 224 unit_scale=True, 225 unit_divisor=1024, 226 desc=download_file_path.name, 227 initial=resume_from_byte_pos if resume_from_byte_pos else 0, 228 ) as progress_bar: 229 for chunk in request.iter_content(chunk_size): 230 file.write(chunk) 231 progress_bar.update(len(chunk)) 232 233 234def get_ndk_version(ndk_path: Union[str, Path]): 235 """Given the path to the NDK, return the version as a 3-tuple of (major, 236 minor, human). 237 """ 238 ndk_path = Path(ndk_path) 239 with open(ndk_path / "source.properties", "r") as f: 240 revision = [line for line in f if line.startswith("Pkg.Revision")] 241 if not revision: 242 raise GetNdkVersionError( 243 "Cannot determine NDK version from source.properties" 244 ) 245 if len(revision) != 1: 246 raise GetNdkVersionError("Too many Pkg.Revision lines in source.properties") 247 248 (_, version) = revision[0].split("=") 249 if not version: 250 raise GetNdkVersionError( 251 "Unexpected Pkg.Revision line in source.properties" 252 ) 253 254 (major, minor, revision) = version.strip().split(".") 255 if not major or not minor: 256 raise GetNdkVersionError("Unexpected NDK version string: " + version) 257 258 # source.properties contains a $MAJOR.$MINOR.$PATCH revision number, 259 # but the more common nomenclature that Google uses is alphanumeric 260 # version strings like "r20" or "r19c". Convert the source.properties 261 # notation into an alphanumeric string. 262 int_minor = int(minor) 263 alphas = "abcdefghijklmnop" 264 ascii_minor = alphas[int_minor] if int_minor > 0 else "" 265 human = "r%s%s" % (major, ascii_minor) 266 return (major, minor, human) 267 268 269def get_paths(os_name): 270 mozbuild_path = Path( 271 os.environ.get("MOZBUILD_STATE_PATH", Path("~/.mozbuild").expanduser()) 272 ) 273 sdk_path = Path( 274 os.environ.get("ANDROID_SDK_HOME", mozbuild_path / f"android-sdk-{os_name}"), 275 ) 276 ndk_path = Path( 277 os.environ.get( 278 "ANDROID_NDK_HOME", mozbuild_path / f"android-ndk-{NDK_VERSION}" 279 ), 280 ) 281 avd_home_path = Path( 282 os.environ.get("ANDROID_AVD_HOME", mozbuild_path / "android-device" / "avd") 283 ) 284 return mozbuild_path, sdk_path, ndk_path, avd_home_path 285 286 287def sdkmanager_tool(sdk_path: Path): 288 # sys.platform is win32 even if Python/Win64. 289 sdkmanager = "sdkmanager.bat" if sys.platform.startswith("win") else "sdkmanager" 290 return ( 291 sdk_path / "cmdline-tools" / CMDLINE_TOOLS_VERSION_STRING / "bin" / sdkmanager 292 ) 293 294 295def avdmanager_tool(sdk_path: Path): 296 # sys.platform is win32 even if Python/Win64. 297 sdkmanager = "avdmanager.bat" if sys.platform.startswith("win") else "avdmanager" 298 return ( 299 sdk_path / "cmdline-tools" / CMDLINE_TOOLS_VERSION_STRING / "bin" / sdkmanager 300 ) 301 302 303def adb_tool(sdk_path: Path): 304 adb = "adb.bat" if sys.platform.startswith("win") else "adb" 305 return sdk_path / "platform-tools" / adb 306 307 308def emulator_tool(sdk_path: Path): 309 emulator = "emulator.bat" if sys.platform.startswith("win") else "emulator" 310 return sdk_path / "emulator" / emulator 311 312 313def ensure_android( 314 os_name, 315 os_arch, 316 artifact_mode=False, 317 ndk_only=False, 318 system_images_only=False, 319 emulator_only=False, 320 avd_manifest_path: Optional[Path] = None, 321 prewarm_avd=False, 322 no_interactive=False, 323 list_packages=False, 324): 325 """ 326 Ensure the Android SDK (and NDK, if `artifact_mode` is falsy) are 327 installed. If not, fetch and unpack the SDK and/or NDK from the 328 given URLs. Ensure the required Android SDK packages are 329 installed. 330 331 `os_name` can be 'linux', 'macosx' or 'windows'. 332 """ 333 # The user may have an external Android SDK (in which case we 334 # save them a lengthy download), or they may have already 335 # completed the download. We unpack to 336 # ~/.mozbuild/{android-sdk-$OS_NAME, android-ndk-$VER}. 337 mozbuild_path, sdk_path, ndk_path, avd_home_path = get_paths(os_name) 338 339 if os_name == "macosx": 340 os_tag = "mac" 341 elif os_name == "windows": 342 os_tag = "win" 343 else: 344 os_tag = os_name 345 346 sdk_url = "https://dl.google.com/android/repository/commandlinetools-{0}-{1}_latest.zip".format( # NOQA: E501 347 os_tag, CMDLINE_TOOLS_VERSION 348 ) 349 ndk_url = android_ndk_url(os_name) 350 bundletool_url = "https://github.com/google/bundletool/releases/download/{v}/bundletool-all-{v}.jar".format( # NOQA: E501 351 v=BUNDLETOOL_VERSION 352 ) 353 354 ensure_android_sdk_and_ndk( 355 mozbuild_path, 356 os_name, 357 sdk_path=sdk_path, 358 sdk_url=sdk_url, 359 ndk_path=ndk_path, 360 ndk_url=ndk_url, 361 bundletool_url=bundletool_url, 362 artifact_mode=artifact_mode, 363 ndk_only=ndk_only, 364 emulator_only=emulator_only, 365 ) 366 367 if ndk_only: 368 return 369 370 avd_manifest = None 371 if avd_manifest_path is not None: 372 with open(avd_manifest_path) as f: 373 avd_manifest = json.load(f) 374 # Some AVDs cannot be prewarmed in CI because they cannot run on linux64 375 # (like the arm64 AVD). 376 if "emulator_prewarm" in avd_manifest: 377 prewarm_avd = prewarm_avd and avd_manifest["emulator_prewarm"] 378 379 # We expect the |sdkmanager| tool to be at 380 # ~/.mozbuild/android-sdk-$OS_NAME/tools/cmdline-tools/$CMDLINE_TOOLS_VERSION_STRING/bin/sdkmanager. # NOQA: E501 381 ensure_android_packages( 382 os_name, 383 os_arch, 384 sdkmanager_tool=sdkmanager_tool(sdk_path), 385 emulator_only=emulator_only, 386 system_images_only=system_images_only, 387 avd_manifest=avd_manifest, 388 no_interactive=no_interactive, 389 list_packages=list_packages, 390 ) 391 392 if emulator_only or system_images_only: 393 return 394 395 ensure_android_avd( 396 avdmanager_tool=avdmanager_tool(sdk_path), 397 adb_tool=adb_tool(sdk_path), 398 emulator_tool=emulator_tool(sdk_path), 399 avd_home_path=avd_home_path, 400 sdk_path=sdk_path, 401 no_interactive=no_interactive, 402 avd_manifest=avd_manifest, 403 prewarm_avd=prewarm_avd, 404 ) 405 406 407def ensure_android_sdk_and_ndk( 408 mozbuild_path: Path, 409 os_name, 410 sdk_path: Path, 411 sdk_url, 412 ndk_path: Path, 413 ndk_url, 414 bundletool_url, 415 artifact_mode, 416 ndk_only, 417 emulator_only, 418): 419 """ 420 Ensure the Android SDK and NDK are found at the given paths. If not, fetch 421 and unpack the SDK and/or NDK from the given URLs into 422 |mozbuild_path/{android-sdk-$OS_NAME,android-ndk-$VER}|. 423 """ 424 425 # It's not particularly bad to overwrite the NDK toolchain, but it does take 426 # a while to unpack, so let's avoid the disk activity if possible. The SDK 427 # may prompt about licensing, so we do this first. 428 # Check for Android NDK only if we are not in artifact mode. 429 if not artifact_mode and not emulator_only: 430 install_ndk = True 431 if ndk_path.is_dir(): 432 try: 433 _, _, human = get_ndk_version(ndk_path) 434 if human == NDK_VERSION: 435 print(ANDROID_NDK_EXISTS % ndk_path) 436 install_ndk = False 437 except GetNdkVersionError: 438 pass # Just do the install. 439 if install_ndk: 440 # The NDK archive unpacks into a top-level android-ndk-$VER directory. 441 install_mobile_android_sdk_or_ndk(ndk_url, mozbuild_path) 442 443 if ndk_only: 444 return 445 446 # We don't want to blindly overwrite, since we use the 447 # |sdkmanager| tool to install additional parts of the Android 448 # toolchain. If we overwrite, we lose whatever Android packages 449 # the user may have already installed. 450 if sdkmanager_tool(sdk_path).is_file(): 451 print(ANDROID_SDK_EXISTS % sdk_path) 452 elif sdk_path.is_dir(): 453 raise NotImplementedError(ANDROID_SDK_TOO_OLD % sdk_path) 454 else: 455 # The SDK archive used to include a top-level 456 # android-sdk-$OS_NAME directory; it no longer does so. We 457 # preserve the old convention to smooth detecting existing SDK 458 # installations. 459 cmdline_tools_path = mozbuild_path / f"android-sdk-{os_name}" / "cmdline-tools" 460 install_mobile_android_sdk_or_ndk(sdk_url, cmdline_tools_path) 461 # The tools package *really* wants to be in 462 # <sdk>/cmdline-tools/$CMDLINE_TOOLS_VERSION_STRING 463 (cmdline_tools_path / "cmdline-tools").rename( 464 cmdline_tools_path / CMDLINE_TOOLS_VERSION_STRING 465 ) 466 install_bundletool(bundletool_url, mozbuild_path) 467 468 469def get_packages_to_install(packages_file_content, avd_manifest): 470 packages = [] 471 packages += map(lambda package: package.strip(), packages_file_content) 472 if avd_manifest is not None: 473 packages += [avd_manifest["emulator_package"]] 474 return packages 475 476 477def ensure_android_avd( 478 avdmanager_tool: Path, 479 adb_tool: Path, 480 emulator_tool: Path, 481 avd_home_path: Path, 482 sdk_path: Path, 483 no_interactive=False, 484 avd_manifest=None, 485 prewarm_avd=False, 486): 487 """ 488 Use the given sdkmanager tool (like 'sdkmanager') to install required 489 Android packages. 490 """ 491 if avd_manifest is None: 492 return 493 494 avd_home_path.mkdir(parents=True, exist_ok=True) 495 # The AVD needs this folder to boot, so make sure it exists here. 496 (sdk_path / "platforms").mkdir(parents=True, exist_ok=True) 497 498 avd_name = avd_manifest["emulator_avd_name"] 499 args = [ 500 str(avdmanager_tool), 501 "--verbose", 502 "create", 503 "avd", 504 "--force", 505 "--name", 506 avd_name, 507 "--package", 508 avd_manifest["emulator_package"], 509 ] 510 511 if not no_interactive: 512 subprocess.check_call(args) 513 return 514 515 # Flush outputs before running sdkmanager. 516 sys.stdout.flush() 517 env = os.environ.copy() 518 env["ANDROID_AVD_HOME"] = str(avd_home_path) 519 proc = subprocess.Popen(args, stdin=subprocess.PIPE, env=env) 520 proc.communicate("no\n".encode("UTF-8")) 521 522 retcode = proc.poll() 523 if retcode: 524 cmd = args[0] 525 e = subprocess.CalledProcessError(retcode, cmd) 526 raise e 527 528 avd_path = avd_home_path / (str(avd_name) + ".avd") 529 config_file_name = avd_path / "config.ini" 530 531 print(f"Writing config at {config_file_name}") 532 533 if config_file_name.is_file(): 534 with open(config_file_name, "a") as config: 535 for key, value in avd_manifest["emulator_extra_config"].items(): 536 config.write("%s=%s\n" % (key, value)) 537 else: 538 raise NotImplementedError( 539 f"Could not find config file at {config_file_name}, something went wrong" 540 ) 541 if prewarm_avd: 542 run_prewarm_avd(adb_tool, emulator_tool, env, avd_name, avd_manifest) 543 # When running in headless mode, the emulator does not run the cleanup 544 # step, and thus doesn't delete lock files. On some platforms, left-over 545 # lock files can cause the emulator to not start, so we remove them here. 546 for lock_file in ["hardware-qemu.ini.lock", "multiinstance.lock"]: 547 lock_file_path = avd_path / lock_file 548 try: 549 lock_file_path.unlink() 550 print(f"Removed lock file {lock_file_path}") 551 except OSError: 552 # The lock file is not there, nothing to do. 553 pass 554 555 556def run_prewarm_avd( 557 adb_tool: Path, 558 emulator_tool: Path, 559 env, 560 avd_name, 561 avd_manifest, 562): 563 """ 564 Ensures the emulator is fully booted to save time on future iterations. 565 """ 566 args = [str(emulator_tool), "-avd", avd_name] + avd_manifest["emulator_extra_args"] 567 568 # Flush outputs before running emulator. 569 sys.stdout.flush() 570 proc = subprocess.Popen(args, env=env) 571 572 booted = False 573 for i in range(100): 574 boot_completed_cmd = [str(adb_tool), "shell", "getprop", "sys.boot_completed"] 575 completed_proc = subprocess.Popen( 576 boot_completed_cmd, env=env, stdout=subprocess.PIPE 577 ) 578 try: 579 out, err = completed_proc.communicate(timeout=30) 580 boot_completed = out.decode("UTF-8").strip() 581 print("sys.boot_completed = %s" % boot_completed) 582 time.sleep(30) 583 if boot_completed == "1": 584 booted = True 585 break 586 except subprocess.TimeoutExpired: 587 # Sometimes the adb command hangs, that's ok 588 print("sys.boot_completed = Timeout") 589 590 if not booted: 591 raise NotImplementedError("Could not prewarm emulator") 592 593 # Wait until the emulator completely shuts down 594 subprocess.Popen([str(adb_tool), "emu", "kill"], env=env).wait() 595 proc.wait() 596 597 598def ensure_android_packages( 599 os_name, 600 os_arch, 601 sdkmanager_tool: Path, 602 emulator_only=False, 603 system_images_only=False, 604 avd_manifest=None, 605 no_interactive=False, 606 list_packages=False, 607): 608 """ 609 Use the given sdkmanager tool (like 'sdkmanager') to install required 610 Android packages. 611 """ 612 613 # This tries to install all the required Android packages. The user 614 # may be prompted to agree to the Android license. 615 if system_images_only: 616 packages_file_name = "android-system-images-packages.txt" 617 elif emulator_only: 618 packages_file_name = "android-emulator-packages.txt" 619 else: 620 packages_file_name = "android-packages.txt" 621 622 packages_file_path = (Path(__file__).parent / packages_file_name).resolve() 623 624 with open(packages_file_path) as packages_file: 625 packages_file_content = packages_file.readlines() 626 627 packages = get_packages_to_install(packages_file_content, avd_manifest) 628 print(INSTALLING_ANDROID_PACKAGES % "\n".join(packages)) 629 630 args = [str(sdkmanager_tool)] 631 if os_name == "macosx" and os_arch == "arm64": 632 # Support for Apple Silicon is still in nightly 633 args.append("--channel=3") 634 args.extend(packages) 635 636 # sdkmanager needs JAVA_HOME 637 java_bin_path = ensure_java(os_name, os_arch) 638 env = os.environ.copy() 639 env["JAVA_HOME"] = str(java_bin_path.parent) 640 641 if not no_interactive: 642 subprocess.check_call(args, env=env) 643 return 644 645 # Flush outputs before running sdkmanager. 646 sys.stdout.flush() 647 sys.stderr.flush() 648 # Emulate yes. For a discussion of passing input to check_output, 649 # see https://stackoverflow.com/q/10103551. 650 yes = "\n".join(["y"] * 100).encode("UTF-8") 651 proc = subprocess.Popen(args, stdin=subprocess.PIPE, env=env) 652 proc.communicate(yes) 653 654 retcode = proc.poll() 655 if retcode: 656 cmd = args[0] 657 e = subprocess.CalledProcessError(retcode, cmd) 658 raise e 659 if list_packages: 660 subprocess.check_call([str(sdkmanager_tool), "--list"]) 661 662 663def generate_mozconfig(os_name, artifact_mode=False): 664 moz_state_dir, sdk_path, ndk_path, avd_home_path = get_paths(os_name) 665 666 extra_lines = [] 667 if extra_lines: 668 extra_lines.append("") 669 670 if artifact_mode: 671 template = MOBILE_ANDROID_ARTIFACT_MODE_MOZCONFIG_TEMPLATE 672 else: 673 template = MOBILE_ANDROID_MOZCONFIG_TEMPLATE 674 675 kwargs = dict( 676 sdk_path=sdk_path, 677 ndk_path=ndk_path, 678 avd_home_path=avd_home_path, 679 moz_state_dir=moz_state_dir, 680 extra_lines="\n".join(extra_lines), 681 ) 682 return template.format(**kwargs).strip() 683 684 685def android_ndk_url(os_name, ver=NDK_VERSION): 686 # Produce a URL like 687 # 'https://dl.google.com/android/repository/android-ndk-$VER-linux-x86_64.zip 688 base_url = "https://dl.google.com/android/repository/android-ndk" 689 690 if os_name == "macosx": 691 # |mach bootstrap| uses 'macosx', but Google uses 'darwin'. 692 os_name = "darwin" 693 694 if sys.maxsize > 2 ** 32: 695 arch = "x86_64" 696 else: 697 arch = "x86" 698 699 return "%s-%s-%s-%s.zip" % (base_url, ver, os_name, arch) 700 701 702def main(argv): 703 import optparse # No argparse, which is new in Python 2.7. 704 import platform 705 706 parser = optparse.OptionParser() 707 parser.add_option( 708 "-a", 709 "--artifact-mode", 710 dest="artifact_mode", 711 action="store_true", 712 help="If true, install only the Android SDK (and not the Android NDK).", 713 ) 714 parser.add_option( 715 "--jdk-only", 716 dest="jdk_only", 717 action="store_true", 718 help="If true, install only the Java JDK.", 719 ) 720 parser.add_option( 721 "--ndk-only", 722 dest="ndk_only", 723 action="store_true", 724 help="If true, install only the Android NDK (and not the Android SDK).", 725 ) 726 parser.add_option( 727 "--system-images-only", 728 dest="system_images_only", 729 action="store_true", 730 help="If true, install only the system images for the AVDs.", 731 ) 732 parser.add_option( 733 "--no-interactive", 734 dest="no_interactive", 735 action="store_true", 736 help="Accept the Android SDK licenses without user interaction.", 737 ) 738 parser.add_option( 739 "--emulator-only", 740 dest="emulator_only", 741 action="store_true", 742 help="If true, install only the Android emulator (and not the SDK or NDK).", 743 ) 744 parser.add_option( 745 "--avd-manifest", 746 dest="avd_manifest_path", 747 help="If present, generate AVD from the manifest pointed by this argument.", 748 ) 749 parser.add_option( 750 "--prewarm-avd", 751 dest="prewarm_avd", 752 action="store_true", 753 help="If true, boot the AVD and wait until completed to speed up subsequent boots.", 754 ) 755 parser.add_option( 756 "--list-packages", 757 dest="list_packages", 758 action="store_true", 759 help="If true, list installed packages.", 760 ) 761 762 options, _ = parser.parse_args(argv) 763 764 if options.artifact_mode and options.ndk_only: 765 raise NotImplementedError("Use no options to install the NDK and the SDK.") 766 767 if options.artifact_mode and options.emulator_only: 768 raise NotImplementedError("Use no options to install the SDK and emulators.") 769 770 os_name = None 771 if platform.system() == "Darwin": 772 os_name = "macosx" 773 elif platform.system() == "Linux": 774 os_name = "linux" 775 elif platform.system() == "Windows": 776 os_name = "windows" 777 else: 778 raise NotImplementedError( 779 "We don't support bootstrapping the Android SDK (or Android " 780 "NDK) on {0} yet!".format(platform.system()) 781 ) 782 783 os_arch = platform.machine() 784 785 if options.jdk_only: 786 ensure_java(os_name, os_arch) 787 return 0 788 789 avd_manifest_path = ( 790 Path(options.avd_manifest_path) if options.avd_manifest_path else None 791 ) 792 793 ensure_android( 794 os_name, 795 os_arch, 796 artifact_mode=options.artifact_mode, 797 ndk_only=options.ndk_only, 798 system_images_only=options.system_images_only, 799 emulator_only=options.emulator_only, 800 avd_manifest_path=avd_manifest_path, 801 prewarm_avd=options.prewarm_avd, 802 no_interactive=options.no_interactive, 803 list_packages=options.list_packages, 804 ) 805 mozconfig = generate_mozconfig(os_name, options.artifact_mode) 806 807 # |./mach bootstrap| automatically creates a mozconfig file for you if it doesn't 808 # exist. However, here, we don't know where the "topsrcdir" is, and it's not worth 809 # pulling in CommandContext (and its dependencies) to find out. 810 # So, instead, we'll politely ask users to create (or update) the file themselves. 811 suggestion = MOZCONFIG_SUGGESTION_TEMPLATE % ("$topsrcdir/mozconfig", mozconfig) 812 print("\n" + suggestion) 813 814 return 0 815 816 817def ensure_java(os_name, os_arch): 818 mozbuild_path, _, _, _ = get_paths(os_name) 819 820 if os_name == "macosx": 821 os_tag = "mac" 822 else: 823 os_tag = os_name 824 825 if os_arch == "x86_64": 826 arch = "x64" 827 elif os_arch == "arm64": 828 arch = "aarch64" 829 else: 830 arch = os_arch 831 832 ext = "zip" if os_name == "windows" else "tar.gz" 833 834 java_path = java_bin_path(os_name, mozbuild_path) 835 if not java_path: 836 raise NotImplementedError(f"Could not bootstrap java for {os_name}.") 837 838 if not java_path.exists(): 839 # e.g. https://github.com/adoptium/temurin17-binaries/releases/ 840 # download/jdk-17.0.1%2B12/OpenJDK17U-jdk_x64_linux_hotspot_17.0.1_12.tar.gz 841 java_url = ( 842 "https://github.com/adoptium/temurin{major}-binaries/releases/" 843 "download/jdk-{major}.{minor}%2B{patch}/" 844 "OpenJDK{major}U-jdk_{arch}_{os}_hotspot_{major}.{minor}_{patch}.{ext}" 845 ).format( 846 major=JAVA_VERSION_MAJOR, 847 minor=JAVA_VERSION_MINOR, 848 patch=JAVA_VERSION_PATCH, 849 os=os_tag, 850 arch=arch, 851 ext=ext, 852 ) 853 install_mobile_android_sdk_or_ndk(java_url, mozbuild_path / "jdk") 854 return java_path 855 856 857def java_bin_path(os_name, toolchain_path: Path): 858 # Like jdk-17.0.1+12 859 jdk_folder = "jdk-{major}.{minor}+{patch}".format( 860 major=JAVA_VERSION_MAJOR, minor=JAVA_VERSION_MINOR, patch=JAVA_VERSION_PATCH 861 ) 862 863 java_path = toolchain_path / "jdk" / jdk_folder 864 865 if os_name == "macosx": 866 return java_path / "Contents" / "Home" / "bin" 867 elif os_name == "linux": 868 return java_path / "bin" 869 elif os_name == "windows": 870 return java_path / "bin" 871 else: 872 return None 873 874 875def locate_java_bin_path(host_kernel, toolchain_path: Union[str, Path]): 876 if host_kernel == "WINNT": 877 os_name = "windows" 878 elif host_kernel == "Darwin": 879 os_name = "macosx" 880 elif host_kernel == "Linux": 881 os_name = "linux" 882 else: 883 # Default to Linux 884 os_name = "linux" 885 path = java_bin_path(os_name, Path(toolchain_path)) 886 if not path.is_dir(): 887 raise JavaLocationFailedException( 888 f"Could not locate Java at {path}, please run " 889 "./mach bootstrap --no-system-changes" 890 ) 891 return str(path) 892 893 894class JavaLocationFailedException(Exception): 895 pass 896 897 898if __name__ == "__main__": 899 sys.exit(main(sys.argv)) 900