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