# 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 os
import platform
import stat
import subprocess
import sys
import time
from configparser
import RawConfigParser
from enum
import Enum
from io
import StringIO
from pathlib
import Path
from typing
import Optional, Union
import requests
from mach.util
import get_state_dir
from mozfile
import json
from tqdm
import tqdm
from mozboot.bootstrap
import MOZCONFIG_SUGGESTION_TEMPLATE
# 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.
NDK_VERSION =
"r29"
CMDLINE_TOOLS_VERSION_STRING =
"20.0"
CMDLINE_TOOLS_VERSION =
"14742923"
BUNDLETOOL_VERSION =
"1.18.3"
BUNDLETOOL_URL = f
"https://github.com/google/bundletool/releases/download/{BUNDLETOOL_VERSION}/bundletool-all-{BUNDLETOOL_VERSION}.jar"
# We expect the emulator AVD definitions to be platform agnostic
X86_64_ANDROID_AVD =
"linux64-android-avd-x86_64-repack"
ARM64_ANDROID_AVD =
"linux64-android-avd-arm64-repack"
AVD_MANIFEST_X86_64 = (
Path(__file__).resolve().parent /
"android-avds/android34-x86_64.json"
)
AVD_MANIFEST_ARM64 = Path(__file__).resolve().parent /
"android-avds/arm64.json"
MOZBUILD_PATH = Path(get_state_dir())
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")
)
JAVA_VERSION_MAJOR =
"17"
JAVA_VERSION_MINOR =
"0.18"
JAVA_VERSION_PATCH =
"8"
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_UPDATE_IN_PLACE =
"""
Looks like you have an outdated Android SDK installed at:
%s
I can update outdated Android SDKs to have the required
'sdkmanager' tool.
If
this fails, 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
# If --target is not specified it will default to host architecture for fast
# emulation (x86_64 or aarch64). For testing on physical phones you most likely
# want to use an aarch64 (ARM64) target.
# ac_add_options --target=aarch64
{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
"""
SUGGEST_ADD_PLATFORM_TOOLS_PATH =
"""
If you plan to use adb
or other platform tools directly on the command line, it may
be useful to add them to your PATH. Edit your shell initialization script to prepend
{platform_tools} to your PATH.
For example:
export PATH=
"{platform_tools}:$PATH"
Then restart your shell.
"""
class GetNdkVersionError(Exception):
pass
def install_mobile_android_sdk_or_ndk(url: str, 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.rsplit(
"/",
1)[-
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: str,
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: str,
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/Reference/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")
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 = f
"r{major}{ascii_minor}"
return (major, minor, human)
def get_sdk_path(os_name: str) -> Path:
# 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.
return Path(
os.environ.get(
"ANDROID_SDK_HOME", MOZBUILD_PATH / f
"android-sdk-{os_name}"),
)
def get_sdkmanager_tool_path(sdk_path: Path) -> Path:
# sys.platform is win32 even if Python/Win64.
sdkmanager =
"sdkmanager.bat" if sys.platform.startswith(
"win")
else "sdkmanager"
# We expect the |sdkmanager| tool to be at
# ~/.mozbuild/android-sdk-$OS_NAME/tools/cmdline-tools/$CMDLINE_TOOLS_VERSION_STRING/bin/sdkmanager # NOQA: E501
return (
sdk_path /
"cmdline-tools" / CMDLINE_TOOLS_VERSION_STRING /
"bin" / sdkmanager
)
def get_avdmanager_tool_path(sdk_path: 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 get_adb_tool_path(sdk_path: Path) -> Path:
adb =
"adb.bat" if sys.platform.startswith(
"win")
else "adb"
return sdk_path /
"platform-tools" / adb
def get_emulator_tool_path(sdk_path: Path) -> Path:
emulator =
"emulator.bat" if sys.platform.startswith(
"win")
else "emulator"
return sdk_path /
"emulator" / emulator
def get_avd_manifest(avd_manifest_path: Path):
avd_manifest =
None
if avd_manifest_path
is not None:
with open(avd_manifest_path)
as f:
avd_manifest = json.load(f)
return avd_manifest
def get_os_name_for_android():
os_name_map = {
"Darwin":
"macosx",
"Linux":
"linux",
"Windows":
"windows",
}
os_name = os_name_map.get(platform.system())
if os_name
is None:
raise NotImplementedError(f
"Unsupported platform: {platform.system()}")
return os_name
def get_os_tag_for_android(os_name: str):
os_tag_map = {
"macosx":
"mac",
"windows":
"win",
}
return os_tag_map.get(os_name, os_name)
def ensure_android(
os_name: str,
os_arch: str,
packages: Optional[set[str]] =
None,
artifact_mode=
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 false) are
installed.
If not, fetch
and unpack the SDK
and/
or NDK
from the
expected URLs. Ensure the required Android SDK packages are
installed.
`os_name` can be
'linux',
'macosx' or 'windows'.
"""
if os_name ==
"windows" and os_arch ==
"ARM64":
raise NotImplementedError(
"Building for Android is not supported on ARM64 Windows because "
"Google does not distribute emulator binary for ARM64 Windows. "
"See also https://issuetracker.google.com/issues/264614669."
)
os_tag = get_os_tag_for_android(os_name)
# Check for Android NDK only if we are not in artifact mode.
if not artifact_mode:
ensure_android_ndk(os_name)
ensure_android_sdk(os_name, os_tag)
ensure_bundletool()
avd_manifest = get_avd_manifest(avd_manifest_path)
ensure_android_packages(
os_name,
os_arch,
packages=packages,
avd_manifest=avd_manifest,
no_interactive=no_interactive,
list_packages=list_packages,
)
ensure_android_avd(
os_name,
os_arch,
no_interactive=no_interactive,
avd_manifest=avd_manifest,
prewarm_avd=prewarm_avd,
)
def ensure_android_ndk(os_name: str):
"""
Ensure the Android NDK
is installed.
If it
is not found,
fetch
and unpack the NDK
from the URL into
'mozbuild_path/{android-ndk-$VER}'.
"""
ndk_url = android_ndk_url(os_name)
# 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.
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)
def ensure_android_sdk(os_name: str, os_tag: str):
"""
Ensure the Android SDK
is installed.
If it
is not found,
fetch
and unpack the SDK
from the URL into
'mozbuild_path/{android-sdk-$OS_NAME}'.
"""
sdk_path = get_sdk_path(os_name)
sdk_url = f
"https://dl.google.com/android/repository/commandlinetools-{os_tag}-{CMDLINE_TOOLS_VERSION}_latest.zip"
# 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 get_sdkmanager_tool_path(sdk_path).is_file():
print(ANDROID_SDK_EXISTS % sdk_path)
else:
if sdk_path.is_dir():
print(ANDROID_SDK_TOO_OLD_UPDATE_IN_PLACE % sdk_path)
# 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
)
def ensure_bundletool():
bundletool_path = os.environ.get(
"ANDROID_BUNDLETOOL_PATH")
if bundletool_path:
print(
f
"ANDROID_BUNDLETOOL_PATH specified. Using {bundletool_path}. Bundletool will not be bootstrapped."
)
return
download(BUNDLETOOL_URL, MOZBUILD_PATH /
"bundletool.jar")
def ensure_android_avd(
os_name: str,
os_arch: str,
no_interactive=
False,
avd_manifest=
None,
prewarm_avd=
False,
sdk_path: Optional[Path] =
None,
):
"""
Use the given sdkmanager tool (like
'sdkmanager') to install required
Android packages.
"""
avd_path = os.environ.get(
"ANDROID_AVD_PATH")
if avd_path:
print(
f
"ANDROID_AVD_PATH specified. Using {avd_path}. Android AVD will not be bootstrapped."
)
return
if avd_manifest
is None:
return
# avdmanager needs Java
ensure_java(os_name, os_arch)
if sdk_path
is None:
sdk_path = get_sdk_path(os_name)
avdmanager_tool = get_avdmanager_tool_path(sdk_path)
adb_tool = get_adb_tool_path(sdk_path)
emulator_tool = get_emulator_tool_path(sdk_path)
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)
java_bin_path = get_java_bin_path(os_name, MOZBUILD_PATH)
env = os.environ.copy()
env[
"JAVA_HOME"] = str(java_bin_path.parent)
env[
"ANDROID_AVD_HOME"] = str(AVD_HOME_PATH)
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, env=env)
return
# Flush outputs before running sdkmanager.
sys.stdout.flush()
proc = subprocess.Popen(args, stdin=subprocess.PIPE, env=env)
proc.communicate(b
"no\n")
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(f
"{key}={value}\n")
else:
raise NotImplementedError(
f
"Could not find config file at {config_file_name}, something went wrong"
)
# Some AVDs cannot be prewarmed in CI because they cannot run on linux64
# (like the arm64 AVD).
if avd_manifest
and "emulator_prewarm" in avd_manifest:
prewarm_avd = prewarm_avd
and avd_manifest[
"emulator_prewarm"]
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"]
try:
result = subprocess.run(
boot_completed_cmd,
env=env,
capture_output=
True,
timeout=
30,
text=
True,
check=
False,
)
boot_completed = result.stdout.strip()
print(f
"sys.boot_completed = {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()
class AndroidPackageList(Enum):
ALL =
"android-packages.txt"
SYSTEM =
"android-system-images-packages.txt"
EMULATOR =
"android-emulator-packages.txt"
def get_android_packages(
package_list_type: AndroidPackageList = AndroidPackageList.ALL,
) -> set[str]:
packages_file_path = (Path(__file__).parent / package_list_type.value).resolve()
content = packages_file_path.read_text()
packages = {line.strip()
for line
in content.splitlines()}
return packages
def ensure_android_packages(
os_name: str,
os_arch: str,
packages: Optional[set[str]],
avd_manifest=
None,
no_interactive=
False,
list_packages=
False,
):
"""
Use the given sdkmanager tool (like
'sdkmanager') to install required
Android packages.
"""
if not packages:
packages = get_android_packages(AndroidPackageList.ALL)
sdk_path = get_sdk_path(os_name)
sdkmanager_tool = get_sdkmanager_tool_path(sdk_path=sdk_path)
if avd_manifest
is not None:
packages.add(avd_manifest[
"emulator_package"])
print(INSTALLING_ANDROID_PACKAGES %
"\n".join(packages))
# This tries to install all the required Android packages. The user
# may be prompted to agree to the Android license.
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
ensure_java(os_name, os_arch)
java_bin_path = get_java_bin_path(os_name, MOZBUILD_PATH)
env = os.environ.copy()
env[
"JAVA_HOME"] = str(java_bin_path.parent)
if not no_interactive:
subprocess.check_call(args, env=env)
suggest_platform_tools_path(packages, sdk_path)
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"], env=env)
suggest_platform_tools_path(packages, sdk_path)
def suggest_platform_tools_path(packages: set, sdk_path: Path):
if "platform-tools" in packages:
platform_tools_dir = (sdk_path /
"platform-tools").resolve()
path_entries = os.environ.get(
"PATH",
"").split(os.pathsep)
normalized_entries = {
os.path.normpath(
os.path.normcase(os.path.expanduser(os.path.expandvars(p)))
)
for p
in path_entries
}
normalized_platform_tools_dir = os.path.normpath(
os.path.normcase(platform_tools_dir)
)
if normalized_platform_tools_dir
not in normalized_entries:
print(
SUGGEST_ADD_PLATFORM_TOOLS_PATH.format(
platform_tools=normalized_platform_tools_dir
)
)
def generate_mozconfig(os_name: str, artifact_mode=
False):
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=get_sdk_path(os_name),
ndk_path=NDK_PATH,
avd_home_path=AVD_HOME_PATH,
moz_state_dir=MOZBUILD_PATH,
extra_lines=
"\n".join(extra_lines),
)
return template.format(**kwargs).strip()
def android_ndk_url(os_name: str, 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 f
"{base_url}-{ver}-{os_name}.zip"
def main():
import argparse
parser = argparse.ArgumentParser(
description=
"Install Android SDK/NDK and other components based on the given flags."
)
parser.add_argument(
"-a",
"--artifact-mode",
dest=
"artifact_mode",
action=
"store_true",
help=
"If true, install only the Android SDK (and not the Android NDK).",
)
parser.add_argument(
"--system-images-only",
dest=
"system_images_only",
action=
"store_true",
help=
"If true, install only the system images for the AVDs.",
)
parser.add_argument(
"--no-interactive",
dest=
"no_interactive",
action=
"store_true",
help=
"Accept the Android SDK licenses without user interaction.",
)
parser.add_argument(
"--avd-manifest",
dest=
"avd_manifest_path",
help=
"If present, generate AVD from the manifest pointed by this argument.",
)
parser.add_argument(
"--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_argument(
"--list-packages",
dest=
"list_packages",
action=
"store_true",
help=
"If true, list installed packages.",
)
exclusive_group = parser.add_mutually_exclusive_group()
exclusive_group.add_argument(
"--emulator-only",
dest=
"emulator_only",
action=
"store_true",
help=
"If true, install only the Android emulator (and not the SDK or NDK).",
)
exclusive_group.add_argument(
"--jdk-only",
dest=
"jdk_only",
action=
"store_true",
help=
"If true, install only the Java JDK.",
)
exclusive_group.add_argument(
"--ndk-only",
dest=
"ndk_only",
action=
"store_true",
help=
"If true, install only the Android NDK (and not the Android SDK).",
)
options = parser.parse_args()
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 = get_os_name_for_android()
os_tag = get_os_tag_for_android(os_name)
os_arch = platform.machine()
avd_manifest_path = (
Path(options.avd_manifest_path)
if options.avd_manifest_path
else None
)
if options.emulator_only:
# We need the sdk to install the emulator
ensure_android_sdk(os_name, os_tag)
ensure_bundletool()
packages = get_android_packages(AndroidPackageList.EMULATOR)
ensure_android_packages(
os_name,
os_arch,
packages=packages,
avd_manifest=get_avd_manifest(avd_manifest_path),
no_interactive=options.no_interactive,
list_packages=options.list_packages,
)
return 0
if options.jdk_only:
java_path = ensure_java(os_name, os_arch)
ensure_gradle_jdk_installations(java_path.parent)
return 0
if options.ndk_only:
ensure_android_ndk(os_name)
return 0
packages =
None
if options.system_images_only:
packages = get_android_packages(AndroidPackageList.SYSTEM)
ensure_android(
os_name,
os_arch,
packages=packages,
artifact_mode=options.artifact_mode,
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: str, os_arch: str):
if os_name ==
"macosx":
os_tag =
"mac"
else:
os_tag = os_name
if os_arch ==
"x86_64":
arch =
"x64"
elif os_arch.lower() ==
"arm64":
arch =
"aarch64"
else:
arch = os_arch
java_path = get_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():
ext =
"zip" if os_name ==
"windows" else "tar.gz"
# e.g. https://github.com/adoptium/temurin17-binaries/releases/
# download/jdk-17.0.18%2B8/OpenJDK17U-jdk_x64_linux_hotspot_17.0.18_8.tar.gz
java_url = (
f
"https://github.com/adoptium/temurin{JAVA_VERSION_MAJOR}-binaries/releases/"
f
"download/jdk-{JAVA_VERSION_MAJOR}.{JAVA_VERSION_MINOR}%2B{JAVA_VERSION_PATCH}/"
f
"OpenJDK{JAVA_VERSION_MAJOR}U-jdk_{arch}_{os_tag}_hotspot_{JAVA_VERSION_MAJOR}.{JAVA_VERSION_MINOR}_{JAVA_VERSION_PATCH}.{ext}"
)
install_mobile_android_sdk_or_ndk(java_url, MOZBUILD_PATH /
"jdk")
return java_path
def ensure_gradle_jdk_installations(
new_jdk_home: Path, gradle_props: Optional[Path] =
None
):
"""Update ~/.gradle/gradle.properties so Gradle can find the .mozbuild JDK.
gradle/gradle-daemon-jvm.properties tells Gradle
and Android Studio
which JDK major version to use
for the daemon. Gradle looks
for a
matching JDK
in the paths listed under org.gradle.java.installations.paths
in ~/.gradle/gradle.properties.
This function keeps that property
in sync
with what bootstrap installs:
- Adds the newly installed JDK to the list of installation paths.
- Removes stale .mozbuild JDK paths that no longer exist on disk.
- Preserves any non-.mozbuild paths the user may have added.
Usually, we don
't want to adjust the user's configuration outside of
the object directory, to both respect the user
's choices and to allow
for multiple object directories.
In this case, however, we have
regular builds
and Android Studio builds using different JVMs, which
is causing significant issues
for developers.
And it
's exceedingly
rare to work
with object directories across multiple JVMs; most of
the time, that
's a mistake.
"""
if gradle_props
is None:
gradle_props = Path.home() /
".gradle" /
"gradle.properties"
key =
"org.gradle.java.installations.paths"
mozbuild_prefix = MOZBUILD_PATH.resolve().as_posix() +
"/"
# gradle.properties has no [section] header, so prepend a synthetic one
# to satisfy configparser, then strip it back out on write.
config = RawConfigParser()
if gradle_props.exists():
config.read_string(
"[DEFAULT]\n" + gradle_props.read_text(encoding=
"utf-8"))
existing_paths = [
p.strip()
for p
in config.get(
"DEFAULT", key, fallback=
"").split(
",")
if p.strip()
]
out_paths = []
for p
in existing_paths:
resolved_path = Path(p).resolve()
resolved = resolved_path.as_posix()
if resolved.startswith(mozbuild_prefix):
# Purge .mozbuild paths that no longer exist.
if resolved_path.is_dir():
out_paths.append(resolved)
else:
# Paths outside .mozbuild were probably added by the user, and we should preserve them.
out_paths.append(p)
out_paths.append(new_jdk_home.resolve().as_posix())
# Remove duplicates while preserving order.
out_paths = list(dict.fromkeys(out_paths))
config[
"DEFAULT"][key] =
",".join(out_paths)
gradle_props.parent.mkdir(parents=
True, exist_ok=
True)
buf = StringIO()
config.write(buf)
gradle_props.write_text(
buf.getvalue().removeprefix(
"[DEFAULT]\n"), encoding=
"utf-8"
)
def get_java_bin_path(os_name: str, toolchain_path: Path):
# Like jdk-17.0.18+8
jdk_folder = f
"jdk-{JAVA_VERSION_MAJOR}.{JAVA_VERSION_MINOR}+{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: str, 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 = get_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())