summaryrefslogtreecommitdiffstats
path: root/python/mozboot/mozboot/android.py
diff options
context:
space:
mode:
Diffstat (limited to 'python/mozboot/mozboot/android.py')
-rw-r--r--python/mozboot/mozboot/android.py886
1 files changed, 886 insertions, 0 deletions
diff --git a/python/mozboot/mozboot/android.py b/python/mozboot/mozboot/android.py
new file mode 100644
index 0000000000..26929da696
--- /dev/null
+++ b/python/mozboot/mozboot/android.py
@@ -0,0 +1,886 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this,
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+import errno
+import json
+import os
+import stat
+import subprocess
+import sys
+import time
+from pathlib import Path
+from typing import Optional, Union
+
+import requests
+from tqdm import tqdm
+
+# We need the NDK version in multiple different places, and it's inconvenient
+# to pass down the NDK version to all relevant places, so we have this global
+# variable.
+from mozboot.bootstrap import MOZCONFIG_SUGGESTION_TEMPLATE
+
+NDK_VERSION = "r23c"
+CMDLINE_TOOLS_VERSION_STRING = "9.0"
+CMDLINE_TOOLS_VERSION = "9477386"
+
+BUNDLETOOL_VERSION = "1.14.1"
+
+# We expect the emulator AVD definitions to be platform agnostic
+LINUX_X86_64_ANDROID_AVD = "linux64-android-avd-x86_64-repack"
+LINUX_ARM_ANDROID_AVD = "linux64-android-avd-arm-repack"
+
+MACOS_X86_64_ANDROID_AVD = "linux64-android-avd-x86_64-repack"
+MACOS_ARM_ANDROID_AVD = "linux64-android-avd-arm-repack"
+MACOS_ARM64_ANDROID_AVD = "linux64-android-avd-arm64-repack"
+
+WINDOWS_X86_64_ANDROID_AVD = "linux64-android-avd-x86_64-repack"
+WINDOWS_ARM_ANDROID_AVD = "linux64-android-avd-arm-repack"
+
+AVD_MANIFEST_X86_64 = Path(__file__).resolve().parent / "android-avds/x86_64.json"
+AVD_MANIFEST_ARM = Path(__file__).resolve().parent / "android-avds/arm.json"
+AVD_MANIFEST_ARM64 = Path(__file__).resolve().parent / "android-avds/arm64.json"
+
+JAVA_VERSION_MAJOR = "17"
+JAVA_VERSION_MINOR = "0.7"
+JAVA_VERSION_PATCH = "7"
+
+ANDROID_NDK_EXISTS = """
+Looks like you have the correct version of the Android NDK installed at:
+%s
+"""
+
+ANDROID_SDK_EXISTS = """
+Looks like you have the Android SDK installed at:
+%s
+We will install all required Android packages.
+"""
+
+ANDROID_SDK_TOO_OLD = """
+Looks like you have an outdated Android SDK installed at:
+%s
+I can't update outdated Android SDKs to have the required 'sdkmanager'
+tool. Move it out of the way (or remove it entirely) and then run
+bootstrap again.
+"""
+
+INSTALLING_ANDROID_PACKAGES = """
+We are now installing the following Android packages:
+%s
+You may be prompted to agree to the Android license. You may see some of
+output as packages are downloaded and installed.
+"""
+
+MOBILE_ANDROID_MOZCONFIG_TEMPLATE = """
+# Build GeckoView/Firefox for Android:
+ac_add_options --enable-project=mobile/android
+
+# Targeting the following architecture.
+# For regular phones, no --target is needed.
+# For x86 emulators (and x86 devices, which are uncommon):
+# ac_add_options --target=i686
+# For newer phones or Apple silicon
+# ac_add_options --target=aarch64
+# For x86_64 emulators (and x86_64 devices, which are even less common):
+# ac_add_options --target=x86_64
+
+{extra_lines}
+"""
+
+MOBILE_ANDROID_ARTIFACT_MODE_MOZCONFIG_TEMPLATE = """
+# Build GeckoView/Firefox for Android Artifact Mode:
+ac_add_options --enable-project=mobile/android
+ac_add_options --enable-artifact-builds
+
+{extra_lines}
+# Write build artifacts to:
+mk_add_options MOZ_OBJDIR=./objdir-frontend
+"""
+
+
+class GetNdkVersionError(Exception):
+ pass
+
+
+def install_mobile_android_sdk_or_ndk(url, path: Path):
+ """
+ Fetch an Android SDK or NDK from |url| and unpack it into the given |path|.
+
+ We use, and 'requests' respects, https. We could also include SHAs for a
+ small improvement in the integrity guarantee we give. But this script is
+ bootstrapped over https anyway, so it's a really minor improvement.
+
+ We keep a cache of the downloaded artifacts, writing into |path|/mozboot.
+ We don't yet clean the cache; it's better to waste some disk space and
+ not require a long re-download than to wipe the cache prematurely.
+ """
+
+ download_path = path / "mozboot"
+ try:
+ download_path.mkdir(parents=True)
+ except OSError as e:
+ if e.errno == errno.EEXIST and download_path.is_dir():
+ pass
+ else:
+ raise
+
+ file_name = url.split("/")[-1]
+ download_file_path = download_path / file_name
+ download(url, download_file_path)
+
+ if file_name.endswith(".tar.gz") or file_name.endswith(".tgz"):
+ cmd = ["tar", "zxf", str(download_file_path)]
+ elif file_name.endswith(".tar.bz2"):
+ cmd = ["tar", "jxf", str(download_file_path)]
+ elif file_name.endswith(".zip"):
+ cmd = ["unzip", "-q", str(download_file_path)]
+ elif file_name.endswith(".bin"):
+ # Execute the .bin file, which unpacks the content.
+ mode = os.stat(path).st_mode
+ download_file_path.chmod(mode | stat.S_IXUSR)
+ cmd = [str(download_file_path)]
+ else:
+ raise NotImplementedError(f"Don't know how to unpack file: {file_name}")
+
+ print(f"Unpacking {download_file_path}...")
+
+ with open(os.devnull, "w") as stdout:
+ # These unpack commands produce a ton of output; ignore it. The
+ # .bin files are 7z archives; there's no command line flag to quiet
+ # output, so we use this hammer.
+ subprocess.check_call(cmd, stdout=stdout, cwd=str(path))
+
+ print(f"Unpacking {download_file_path}... DONE")
+ # Now delete the archive
+ download_file_path.unlink()
+
+
+def download(
+ url,
+ download_file_path: Path,
+):
+ with requests.Session() as session:
+ request = session.head(url, allow_redirects=True)
+ request.raise_for_status()
+ remote_file_size = int(request.headers["content-length"])
+
+ if download_file_path.is_file():
+ local_file_size = download_file_path.stat().st_size
+
+ if local_file_size == remote_file_size:
+ print(
+ f"{download_file_path.name} already downloaded. Skipping download..."
+ )
+ else:
+ print(f"Partial download detected. Resuming download of {url}...")
+ download_internal(
+ download_file_path,
+ session,
+ url,
+ remote_file_size,
+ local_file_size,
+ )
+ else:
+ print(f"Downloading {url}...")
+ download_internal(download_file_path, session, url, remote_file_size)
+
+
+def download_internal(
+ download_file_path: Path,
+ session,
+ url,
+ remote_file_size,
+ resume_from_byte_pos: int = None,
+):
+ """
+ Handles both a fresh SDK/NDK download, as well as resuming a partial one
+ """
+ # "ab" will behave same as "wb" if file does not exist
+ with open(download_file_path, "ab") as file:
+ # 64 KB/s should be fine on even the slowest internet connections
+ chunk_size = 1024 * 64
+ # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Range#directives
+ resume_header = (
+ {"Range": f"bytes={resume_from_byte_pos}-"}
+ if resume_from_byte_pos
+ else None
+ )
+
+ request = session.get(
+ url, stream=True, allow_redirects=True, headers=resume_header
+ )
+
+ with tqdm(
+ total=int(remote_file_size),
+ unit="B",
+ unit_scale=True,
+ unit_divisor=1024,
+ desc=download_file_path.name,
+ initial=resume_from_byte_pos if resume_from_byte_pos else 0,
+ ) as progress_bar:
+ for chunk in request.iter_content(chunk_size):
+ file.write(chunk)
+ progress_bar.update(len(chunk))
+
+
+def get_ndk_version(ndk_path: Union[str, Path]):
+ """Given the path to the NDK, return the version as a 3-tuple of (major,
+ minor, human).
+ """
+ ndk_path = Path(ndk_path)
+ with open(ndk_path / "source.properties", "r") as f:
+ revision = [line for line in f if line.startswith("Pkg.Revision")]
+ if not revision:
+ raise GetNdkVersionError(
+ "Cannot determine NDK version from source.properties"
+ )
+ if len(revision) != 1:
+ raise GetNdkVersionError("Too many Pkg.Revision lines in source.properties")
+
+ (_, version) = revision[0].split("=")
+ if not version:
+ raise GetNdkVersionError(
+ "Unexpected Pkg.Revision line in source.properties"
+ )
+
+ (major, minor, revision) = version.strip().split(".")
+ if not major or not minor:
+ raise GetNdkVersionError("Unexpected NDK version string: " + version)
+
+ # source.properties contains a $MAJOR.$MINOR.$PATCH revision number,
+ # but the more common nomenclature that Google uses is alphanumeric
+ # version strings like "r20" or "r19c". Convert the source.properties
+ # notation into an alphanumeric string.
+ int_minor = int(minor)
+ alphas = "abcdefghijklmnop"
+ ascii_minor = alphas[int_minor] if int_minor > 0 else ""
+ human = "r%s%s" % (major, ascii_minor)
+ return (major, minor, human)
+
+
+def get_paths(os_name):
+ mozbuild_path = Path(
+ os.environ.get("MOZBUILD_STATE_PATH", Path("~/.mozbuild").expanduser())
+ )
+ sdk_path = Path(
+ os.environ.get("ANDROID_SDK_HOME", mozbuild_path / f"android-sdk-{os_name}"),
+ )
+ ndk_path = Path(
+ os.environ.get(
+ "ANDROID_NDK_HOME", mozbuild_path / f"android-ndk-{NDK_VERSION}"
+ ),
+ )
+ avd_home_path = Path(
+ os.environ.get("ANDROID_AVD_HOME", mozbuild_path / "android-device" / "avd")
+ )
+ return mozbuild_path, sdk_path, ndk_path, avd_home_path
+
+
+def sdkmanager_tool(sdk_path: Path):
+ # sys.platform is win32 even if Python/Win64.
+ sdkmanager = "sdkmanager.bat" if sys.platform.startswith("win") else "sdkmanager"
+ return (
+ sdk_path / "cmdline-tools" / CMDLINE_TOOLS_VERSION_STRING / "bin" / sdkmanager
+ )
+
+
+def avdmanager_tool(sdk_path: Path):
+ # sys.platform is win32 even if Python/Win64.
+ sdkmanager = "avdmanager.bat" if sys.platform.startswith("win") else "avdmanager"
+ return (
+ sdk_path / "cmdline-tools" / CMDLINE_TOOLS_VERSION_STRING / "bin" / sdkmanager
+ )
+
+
+def adb_tool(sdk_path: Path):
+ adb = "adb.bat" if sys.platform.startswith("win") else "adb"
+ return sdk_path / "platform-tools" / adb
+
+
+def emulator_tool(sdk_path: Path):
+ emulator = "emulator.bat" if sys.platform.startswith("win") else "emulator"
+ return sdk_path / "emulator" / emulator
+
+
+def ensure_android(
+ os_name,
+ os_arch,
+ artifact_mode=False,
+ ndk_only=False,
+ system_images_only=False,
+ emulator_only=False,
+ avd_manifest_path: Optional[Path] = None,
+ prewarm_avd=False,
+ no_interactive=False,
+ list_packages=False,
+):
+ """
+ Ensure the Android SDK (and NDK, if `artifact_mode` is falsy) are
+ installed. If not, fetch and unpack the SDK and/or NDK from the
+ given URLs. Ensure the required Android SDK packages are
+ installed.
+
+ `os_name` can be 'linux', 'macosx' or 'windows'.
+ """
+ # The user may have an external Android SDK (in which case we
+ # save them a lengthy download), or they may have already
+ # completed the download. We unpack to
+ # ~/.mozbuild/{android-sdk-$OS_NAME, android-ndk-$VER}.
+ mozbuild_path, sdk_path, ndk_path, avd_home_path = get_paths(os_name)
+
+ if os_name == "macosx":
+ os_tag = "mac"
+ elif os_name == "windows":
+ os_tag = "win"
+ else:
+ os_tag = os_name
+
+ sdk_url = "https://dl.google.com/android/repository/commandlinetools-{0}-{1}_latest.zip".format( # NOQA: E501
+ os_tag, CMDLINE_TOOLS_VERSION
+ )
+ ndk_url = android_ndk_url(os_name)
+ bundletool_url = "https://github.com/google/bundletool/releases/download/{v}/bundletool-all-{v}.jar".format( # NOQA: E501
+ v=BUNDLETOOL_VERSION
+ )
+
+ ensure_android_sdk_and_ndk(
+ mozbuild_path,
+ os_name,
+ sdk_path=sdk_path,
+ sdk_url=sdk_url,
+ ndk_path=ndk_path,
+ ndk_url=ndk_url,
+ bundletool_url=bundletool_url,
+ artifact_mode=artifact_mode,
+ ndk_only=ndk_only,
+ emulator_only=emulator_only,
+ )
+
+ if ndk_only:
+ return
+
+ avd_manifest = None
+ if avd_manifest_path is not None:
+ with open(avd_manifest_path) as f:
+ avd_manifest = json.load(f)
+ # Some AVDs cannot be prewarmed in CI because they cannot run on linux64
+ # (like the arm64 AVD).
+ if "emulator_prewarm" in avd_manifest:
+ prewarm_avd = prewarm_avd and avd_manifest["emulator_prewarm"]
+
+ # We expect the |sdkmanager| tool to be at
+ # ~/.mozbuild/android-sdk-$OS_NAME/tools/cmdline-tools/$CMDLINE_TOOLS_VERSION_STRING/bin/sdkmanager. # NOQA: E501
+ ensure_android_packages(
+ os_name,
+ os_arch,
+ sdkmanager_tool=sdkmanager_tool(sdk_path),
+ emulator_only=emulator_only,
+ system_images_only=system_images_only,
+ avd_manifest=avd_manifest,
+ no_interactive=no_interactive,
+ list_packages=list_packages,
+ )
+
+ if emulator_only or system_images_only:
+ return
+
+ ensure_android_avd(
+ avdmanager_tool=avdmanager_tool(sdk_path),
+ adb_tool=adb_tool(sdk_path),
+ emulator_tool=emulator_tool(sdk_path),
+ avd_home_path=avd_home_path,
+ sdk_path=sdk_path,
+ no_interactive=no_interactive,
+ avd_manifest=avd_manifest,
+ prewarm_avd=prewarm_avd,
+ )
+
+
+def ensure_android_sdk_and_ndk(
+ mozbuild_path: Path,
+ os_name,
+ sdk_path: Path,
+ sdk_url,
+ ndk_path: Path,
+ ndk_url,
+ bundletool_url,
+ artifact_mode,
+ ndk_only,
+ emulator_only,
+):
+ """
+ Ensure the Android SDK and NDK are found at the given paths. If not, fetch
+ and unpack the SDK and/or NDK from the given URLs into
+ |mozbuild_path/{android-sdk-$OS_NAME,android-ndk-$VER}|.
+ """
+
+ # It's not particularly bad to overwrite the NDK toolchain, but it does take
+ # a while to unpack, so let's avoid the disk activity if possible. The SDK
+ # may prompt about licensing, so we do this first.
+ # Check for Android NDK only if we are not in artifact mode.
+ if not artifact_mode and not emulator_only:
+ install_ndk = True
+ if ndk_path.is_dir():
+ try:
+ _, _, human = get_ndk_version(ndk_path)
+ if human == NDK_VERSION:
+ print(ANDROID_NDK_EXISTS % ndk_path)
+ install_ndk = False
+ except GetNdkVersionError:
+ pass # Just do the install.
+ if install_ndk:
+ # The NDK archive unpacks into a top-level android-ndk-$VER directory.
+ install_mobile_android_sdk_or_ndk(ndk_url, mozbuild_path)
+
+ if ndk_only:
+ return
+
+ # We don't want to blindly overwrite, since we use the
+ # |sdkmanager| tool to install additional parts of the Android
+ # toolchain. If we overwrite, we lose whatever Android packages
+ # the user may have already installed.
+ if sdkmanager_tool(sdk_path).is_file():
+ print(ANDROID_SDK_EXISTS % sdk_path)
+ elif sdk_path.is_dir():
+ raise NotImplementedError(ANDROID_SDK_TOO_OLD % sdk_path)
+ else:
+ # The SDK archive used to include a top-level
+ # android-sdk-$OS_NAME directory; it no longer does so. We
+ # preserve the old convention to smooth detecting existing SDK
+ # installations.
+ cmdline_tools_path = mozbuild_path / f"android-sdk-{os_name}" / "cmdline-tools"
+ install_mobile_android_sdk_or_ndk(sdk_url, cmdline_tools_path)
+ # The tools package *really* wants to be in
+ # <sdk>/cmdline-tools/$CMDLINE_TOOLS_VERSION_STRING
+ (cmdline_tools_path / "cmdline-tools").rename(
+ cmdline_tools_path / CMDLINE_TOOLS_VERSION_STRING
+ )
+ download(bundletool_url, mozbuild_path / "bundletool.jar")
+
+
+def get_packages_to_install(packages_file_content, avd_manifest):
+ packages = []
+ packages += map(lambda package: package.strip(), packages_file_content)
+ if avd_manifest is not None:
+ packages += [avd_manifest["emulator_package"]]
+ return packages
+
+
+def ensure_android_avd(
+ avdmanager_tool: Path,
+ adb_tool: Path,
+ emulator_tool: Path,
+ avd_home_path: Path,
+ sdk_path: Path,
+ no_interactive=False,
+ avd_manifest=None,
+ prewarm_avd=False,
+):
+ """
+ Use the given sdkmanager tool (like 'sdkmanager') to install required
+ Android packages.
+ """
+ if avd_manifest is None:
+ return
+
+ avd_home_path.mkdir(parents=True, exist_ok=True)
+ # The AVD needs this folder to boot, so make sure it exists here.
+ (sdk_path / "platforms").mkdir(parents=True, exist_ok=True)
+
+ avd_name = avd_manifest["emulator_avd_name"]
+ args = [
+ str(avdmanager_tool),
+ "--verbose",
+ "create",
+ "avd",
+ "--force",
+ "--name",
+ avd_name,
+ "--package",
+ avd_manifest["emulator_package"],
+ ]
+
+ if not no_interactive:
+ subprocess.check_call(args)
+ return
+
+ # Flush outputs before running sdkmanager.
+ sys.stdout.flush()
+ env = os.environ.copy()
+ env["ANDROID_AVD_HOME"] = str(avd_home_path)
+ proc = subprocess.Popen(args, stdin=subprocess.PIPE, env=env)
+ proc.communicate("no\n".encode("UTF-8"))
+
+ retcode = proc.poll()
+ if retcode:
+ cmd = args[0]
+ e = subprocess.CalledProcessError(retcode, cmd)
+ raise e
+
+ avd_path = avd_home_path / (str(avd_name) + ".avd")
+ config_file_name = avd_path / "config.ini"
+
+ print(f"Writing config at {config_file_name}")
+
+ if config_file_name.is_file():
+ with open(config_file_name, "a") as config:
+ for key, value in avd_manifest["emulator_extra_config"].items():
+ config.write("%s=%s\n" % (key, value))
+ else:
+ raise NotImplementedError(
+ f"Could not find config file at {config_file_name}, something went wrong"
+ )
+ if prewarm_avd:
+ run_prewarm_avd(adb_tool, emulator_tool, env, avd_name, avd_manifest)
+ # When running in headless mode, the emulator does not run the cleanup
+ # step, and thus doesn't delete lock files. On some platforms, left-over
+ # lock files can cause the emulator to not start, so we remove them here.
+ for lock_file in ["hardware-qemu.ini.lock", "multiinstance.lock"]:
+ lock_file_path = avd_path / lock_file
+ try:
+ lock_file_path.unlink()
+ print(f"Removed lock file {lock_file_path}")
+ except OSError:
+ # The lock file is not there, nothing to do.
+ pass
+
+
+def run_prewarm_avd(
+ adb_tool: Path,
+ emulator_tool: Path,
+ env,
+ avd_name,
+ avd_manifest,
+):
+ """
+ Ensures the emulator is fully booted to save time on future iterations.
+ """
+ args = [str(emulator_tool), "-avd", avd_name] + avd_manifest["emulator_extra_args"]
+
+ # Flush outputs before running emulator.
+ sys.stdout.flush()
+ proc = subprocess.Popen(args, env=env)
+
+ booted = False
+ for i in range(100):
+ boot_completed_cmd = [str(adb_tool), "shell", "getprop", "sys.boot_completed"]
+ completed_proc = subprocess.Popen(
+ boot_completed_cmd, env=env, stdout=subprocess.PIPE
+ )
+ try:
+ out, err = completed_proc.communicate(timeout=30)
+ boot_completed = out.decode("UTF-8").strip()
+ print("sys.boot_completed = %s" % boot_completed)
+ time.sleep(30)
+ if boot_completed == "1":
+ booted = True
+ break
+ except subprocess.TimeoutExpired:
+ # Sometimes the adb command hangs, that's ok
+ print("sys.boot_completed = Timeout")
+
+ if not booted:
+ raise NotImplementedError("Could not prewarm emulator")
+
+ # Wait until the emulator completely shuts down
+ subprocess.Popen([str(adb_tool), "emu", "kill"], env=env).wait()
+ proc.wait()
+
+
+def ensure_android_packages(
+ os_name,
+ os_arch,
+ sdkmanager_tool: Path,
+ emulator_only=False,
+ system_images_only=False,
+ avd_manifest=None,
+ no_interactive=False,
+ list_packages=False,
+):
+ """
+ Use the given sdkmanager tool (like 'sdkmanager') to install required
+ Android packages.
+ """
+
+ # This tries to install all the required Android packages. The user
+ # may be prompted to agree to the Android license.
+ if system_images_only:
+ packages_file_name = "android-system-images-packages.txt"
+ elif emulator_only:
+ packages_file_name = "android-emulator-packages.txt"
+ else:
+ packages_file_name = "android-packages.txt"
+
+ packages_file_path = (Path(__file__).parent / packages_file_name).resolve()
+
+ with open(packages_file_path) as packages_file:
+ packages_file_content = packages_file.readlines()
+
+ packages = get_packages_to_install(packages_file_content, avd_manifest)
+ print(INSTALLING_ANDROID_PACKAGES % "\n".join(packages))
+
+ args = [str(sdkmanager_tool)]
+ if os_name == "macosx" and os_arch == "arm64":
+ # Support for Apple Silicon is still in nightly
+ args.append("--channel=3")
+ args.extend(packages)
+
+ # sdkmanager needs JAVA_HOME
+ java_bin_path = ensure_java(os_name, os_arch)
+ env = os.environ.copy()
+ env["JAVA_HOME"] = str(java_bin_path.parent)
+
+ if not no_interactive:
+ subprocess.check_call(args, env=env)
+ return
+
+ # Flush outputs before running sdkmanager.
+ sys.stdout.flush()
+ sys.stderr.flush()
+ # Emulate yes. For a discussion of passing input to check_output,
+ # see https://stackoverflow.com/q/10103551.
+ yes = "\n".join(["y"] * 100).encode("UTF-8")
+ proc = subprocess.Popen(args, stdin=subprocess.PIPE, env=env)
+ proc.communicate(yes)
+
+ retcode = proc.poll()
+ if retcode:
+ cmd = args[0]
+ e = subprocess.CalledProcessError(retcode, cmd)
+ raise e
+ if list_packages:
+ subprocess.check_call([str(sdkmanager_tool), "--list"])
+
+
+def generate_mozconfig(os_name, artifact_mode=False):
+ moz_state_dir, sdk_path, ndk_path, avd_home_path = get_paths(os_name)
+
+ extra_lines = []
+ if extra_lines:
+ extra_lines.append("")
+
+ if artifact_mode:
+ template = MOBILE_ANDROID_ARTIFACT_MODE_MOZCONFIG_TEMPLATE
+ else:
+ template = MOBILE_ANDROID_MOZCONFIG_TEMPLATE
+
+ kwargs = dict(
+ sdk_path=sdk_path,
+ ndk_path=ndk_path,
+ avd_home_path=avd_home_path,
+ moz_state_dir=moz_state_dir,
+ extra_lines="\n".join(extra_lines),
+ )
+ return template.format(**kwargs).strip()
+
+
+def android_ndk_url(os_name, ver=NDK_VERSION):
+ # Produce a URL like
+ # 'https://dl.google.com/android/repository/android-ndk-$VER-linux.zip
+ base_url = "https://dl.google.com/android/repository/android-ndk"
+
+ if os_name == "macosx":
+ # |mach bootstrap| uses 'macosx', but Google uses 'darwin'.
+ os_name = "darwin"
+
+ return "%s-%s-%s.zip" % (base_url, ver, os_name)
+
+
+def main(argv):
+ import optparse # No argparse, which is new in Python 2.7.
+ import platform
+
+ parser = optparse.OptionParser()
+ parser.add_option(
+ "-a",
+ "--artifact-mode",
+ dest="artifact_mode",
+ action="store_true",
+ help="If true, install only the Android SDK (and not the Android NDK).",
+ )
+ parser.add_option(
+ "--jdk-only",
+ dest="jdk_only",
+ action="store_true",
+ help="If true, install only the Java JDK.",
+ )
+ parser.add_option(
+ "--ndk-only",
+ dest="ndk_only",
+ action="store_true",
+ help="If true, install only the Android NDK (and not the Android SDK).",
+ )
+ parser.add_option(
+ "--system-images-only",
+ dest="system_images_only",
+ action="store_true",
+ help="If true, install only the system images for the AVDs.",
+ )
+ parser.add_option(
+ "--no-interactive",
+ dest="no_interactive",
+ action="store_true",
+ help="Accept the Android SDK licenses without user interaction.",
+ )
+ parser.add_option(
+ "--emulator-only",
+ dest="emulator_only",
+ action="store_true",
+ help="If true, install only the Android emulator (and not the SDK or NDK).",
+ )
+ parser.add_option(
+ "--avd-manifest",
+ dest="avd_manifest_path",
+ help="If present, generate AVD from the manifest pointed by this argument.",
+ )
+ parser.add_option(
+ "--prewarm-avd",
+ dest="prewarm_avd",
+ action="store_true",
+ help="If true, boot the AVD and wait until completed to speed up subsequent boots.",
+ )
+ parser.add_option(
+ "--list-packages",
+ dest="list_packages",
+ action="store_true",
+ help="If true, list installed packages.",
+ )
+
+ options, _ = parser.parse_args(argv)
+
+ if options.artifact_mode and options.ndk_only:
+ raise NotImplementedError("Use no options to install the NDK and the SDK.")
+
+ if options.artifact_mode and options.emulator_only:
+ raise NotImplementedError("Use no options to install the SDK and emulators.")
+
+ os_name = None
+ if platform.system() == "Darwin":
+ os_name = "macosx"
+ elif platform.system() == "Linux":
+ os_name = "linux"
+ elif platform.system() == "Windows":
+ os_name = "windows"
+ else:
+ raise NotImplementedError(
+ "We don't support bootstrapping the Android SDK (or Android "
+ "NDK) on {0} yet!".format(platform.system())
+ )
+
+ os_arch = platform.machine()
+
+ if options.jdk_only:
+ ensure_java(os_name, os_arch)
+ return 0
+
+ avd_manifest_path = (
+ Path(options.avd_manifest_path) if options.avd_manifest_path else None
+ )
+
+ ensure_android(
+ os_name,
+ os_arch,
+ artifact_mode=options.artifact_mode,
+ ndk_only=options.ndk_only,
+ system_images_only=options.system_images_only,
+ emulator_only=options.emulator_only,
+ avd_manifest_path=avd_manifest_path,
+ prewarm_avd=options.prewarm_avd,
+ no_interactive=options.no_interactive,
+ list_packages=options.list_packages,
+ )
+ mozconfig = generate_mozconfig(os_name, options.artifact_mode)
+
+ # |./mach bootstrap| automatically creates a mozconfig file for you if it doesn't
+ # exist. However, here, we don't know where the "topsrcdir" is, and it's not worth
+ # pulling in CommandContext (and its dependencies) to find out.
+ # So, instead, we'll politely ask users to create (or update) the file themselves.
+ suggestion = MOZCONFIG_SUGGESTION_TEMPLATE % ("$topsrcdir/mozconfig", mozconfig)
+ print("\n" + suggestion)
+
+ return 0
+
+
+def ensure_java(os_name, os_arch):
+ mozbuild_path, _, _, _ = get_paths(os_name)
+
+ if os_name == "macosx":
+ os_tag = "mac"
+ else:
+ os_tag = os_name
+
+ if os_arch == "x86_64":
+ arch = "x64"
+ elif os_arch == "arm64":
+ arch = "aarch64"
+ else:
+ arch = os_arch
+
+ ext = "zip" if os_name == "windows" else "tar.gz"
+
+ java_path = java_bin_path(os_name, mozbuild_path)
+ if not java_path:
+ raise NotImplementedError(f"Could not bootstrap java for {os_name}.")
+
+ if not java_path.exists():
+ # e.g. https://github.com/adoptium/temurin17-binaries/releases/
+ # download/jdk-17.0.7%2B7/OpenJDK17U-jre_x64_linux_hotspot_17.0.7_7.tar.gz
+ java_url = (
+ "https://github.com/adoptium/temurin{major}-binaries/releases/"
+ "download/jdk-{major}.{minor}%2B{patch}/"
+ "OpenJDK{major}U-jdk_{arch}_{os}_hotspot_{major}.{minor}_{patch}.{ext}"
+ ).format(
+ major=JAVA_VERSION_MAJOR,
+ minor=JAVA_VERSION_MINOR,
+ patch=JAVA_VERSION_PATCH,
+ os=os_tag,
+ arch=arch,
+ ext=ext,
+ )
+ install_mobile_android_sdk_or_ndk(java_url, mozbuild_path / "jdk")
+ return java_path
+
+
+def java_bin_path(os_name, toolchain_path: Path):
+ # Like jdk-17.0.7+7
+ jdk_folder = "jdk-{major}.{minor}+{patch}".format(
+ major=JAVA_VERSION_MAJOR, minor=JAVA_VERSION_MINOR, patch=JAVA_VERSION_PATCH
+ )
+
+ java_path = toolchain_path / "jdk" / jdk_folder
+
+ if os_name == "macosx":
+ return java_path / "Contents" / "Home" / "bin"
+ elif os_name == "linux":
+ return java_path / "bin"
+ elif os_name == "windows":
+ return java_path / "bin"
+ else:
+ return None
+
+
+def locate_java_bin_path(host_kernel, toolchain_path: Union[str, Path]):
+ if host_kernel == "WINNT":
+ os_name = "windows"
+ elif host_kernel == "Darwin":
+ os_name = "macosx"
+ elif host_kernel == "Linux":
+ os_name = "linux"
+ else:
+ # Default to Linux
+ os_name = "linux"
+ path = java_bin_path(os_name, Path(toolchain_path))
+ if not path.is_dir():
+ raise JavaLocationFailedException(
+ f"Could not locate Java at {path}, please run "
+ "./mach bootstrap --no-system-changes"
+ )
+ return str(path)
+
+
+class JavaLocationFailedException(Exception):
+ pass
+
+
+if __name__ == "__main__":
+ sys.exit(main(sys.argv))