tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

mach_commands.py (26697B)


      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 
      5 import argparse
      6 import logging
      7 import os
      8 import platform
      9 import shutil
     10 import sys
     11 import time
     12 import zipfile
     13 
     14 import mozpack.path as mozpath
     15 from mach.decorators import Command, CommandArgument, SubCommand
     16 from mozbuild.base import MachCommandConditions as conditions
     17 from mozshellutil import split as shell_split
     18 
     19 # Mach's conditions facility doesn't support subcommands.  Print a
     20 # deprecation message ourselves instead.
     21 LINT_DEPRECATION_MESSAGE = """
     22 Android lints are now integrated with mozlint.  Instead of
     23 `mach android {api-lint,checkstyle,lint,test}`, run
     24 `mach lint --linter android-{api-lint,checkstyle,lint,test}`.
     25 Or run `mach lint`.
     26 """
     27 
     28 
     29 # NOTE python/mach/mach/commands/commandinfo.py references this function
     30 #      by name. If this function is renamed or removed, that file should
     31 #      be updated accordingly as well.
     32 def REMOVED(cls):
     33    """Command no longer exists! Use the Gradle configuration rooted in the top source directory
     34    instead.
     35 
     36    See https://developer.mozilla.org/en-US/docs/Simple_Firefox_for_Android_build#Developing_Firefox_for_Android_in_Android_Studio_or_IDEA_IntelliJ.  # NOQA: E501
     37    """
     38    return False
     39 
     40 
     41 @Command(
     42    "android",
     43    category="devenv",
     44    description="Run Android-specific commands.",
     45    conditions=[conditions.is_android],
     46 )
     47 def android(command_context):
     48    pass
     49 
     50 
     51 @SubCommand(
     52    "android",
     53    "export",
     54    """Generate SDK bindings and GeckoView JNI wrappers used when building GeckoView.""",
     55 )
     56 @CommandArgument(
     57    "inputs",
     58    nargs="+",
     59    help="config files, like [/path/to/ClassName-classes.txt]+",
     60 )
     61 @CommandArgument("args", nargs=argparse.REMAINDER)
     62 def export(command_context, inputs, args):
     63    import itertools
     64 
     65    def stem(input):
     66        # Turn "/path/to/ClassName-classes.txt" into "ClassName".
     67        return os.path.basename(input).rsplit("-classes.txt", 1)[0]
     68 
     69    bindings_inputs = list(itertools.chain(*((input, stem(input)) for input in inputs)))
     70    bindings_args = "-Pgenerate_sdk_bindings_args={}".format(";".join(bindings_inputs))
     71 
     72    ret = gradle(
     73        command_context,
     74        command_context.substs["GRADLE_ANDROID_GENERATE_GENERATED_JNI_WRAPPERS_TASKS"]
     75        + command_context.substs["GRADLE_ANDROID_GENERATE_SDK_BINDINGS_TASKS"]
     76        + [bindings_args]
     77        + args,
     78        verbose=True,
     79    )
     80 
     81    return ret
     82 
     83 
     84 @SubCommand(
     85    "android",
     86    "api-lint",
     87    """Run Android api-lint.
     88 REMOVED/DEPRECATED: Use 'mach lint --linter android-api-lint'.""",
     89 )
     90 def android_apilint_REMOVED(command_context):
     91    print(LINT_DEPRECATION_MESSAGE)
     92    return 1
     93 
     94 
     95 @SubCommand(
     96    "android",
     97    "test",
     98    """Run Android test.
     99 REMOVED/DEPRECATED: Use 'mach lint --linter android-test'.""",
    100 )
    101 def android_test_REMOVED(command_context):
    102    print(LINT_DEPRECATION_MESSAGE)
    103    return 1
    104 
    105 
    106 @SubCommand(
    107    "android",
    108    "lint",
    109    """Run Android lint.
    110 REMOVED/DEPRECATED: Use 'mach lint --linter android-lint'.""",
    111 )
    112 def android_lint_REMOVED(command_context):
    113    print(LINT_DEPRECATION_MESSAGE)
    114    return 1
    115 
    116 
    117 @SubCommand(
    118    "android",
    119    "checkstyle",
    120    """Run Android checkstyle.
    121 REMOVED/DEPRECATED: Use 'mach lint --linter android-checkstyle'.""",
    122 )
    123 def android_checkstyle_REMOVED(command_context):
    124    print(LINT_DEPRECATION_MESSAGE)
    125    return 1
    126 
    127 
    128 @SubCommand(
    129    "android",
    130    "gradle-dependencies",
    131    """Collect Android Gradle dependencies.
    132    See http://firefox-source-docs.mozilla.org/build/buildsystem/toolchains.html#firefox-for-android-with-gradle""",  # NOQA: E501
    133 )
    134 @CommandArgument("args", nargs=argparse.REMAINDER)
    135 def android_gradle_dependencies(command_context, args):
    136    # We don't want to gate producing dependency archives on clean
    137    # lint or checkstyle, particularly because toolchain versions
    138    # can change the outputs for those processes.
    139    gradle(
    140        command_context,
    141        command_context.substs["GRADLE_ANDROID_DEPENDENCIES_TASKS"]
    142        + ["--continue"]
    143        + args,
    144        verbose=True,
    145    )
    146 
    147    return 0
    148 
    149 
    150 def get_maven_archive_paths(maven_folder):
    151    for subdir, _, files in os.walk(maven_folder):
    152        if "-SNAPSHOT" in subdir:
    153            continue
    154        for file in files:
    155            yield os.path.join(subdir, file)
    156 
    157 
    158 def create_maven_archive(topobjdir):
    159    gradle_folder = os.path.join(topobjdir, "gradle")
    160    maven_folder = os.path.join(gradle_folder, "maven")
    161 
    162    # Create the archive, with no compression: The archive contents are large
    163    # files which cannot be significantly compressed; attempting to compress
    164    # the archive is usually expensive in time and results in minimal
    165    # reduction in size.
    166    with zipfile.ZipFile(
    167        os.path.join(gradle_folder, "target.maven.zip"), "w"
    168    ) as target_zip:
    169        for abs_path in get_maven_archive_paths(maven_folder):
    170            target_zip.write(
    171                abs_path,
    172                arcname=os.path.join(
    173                    "geckoview", os.path.relpath(abs_path, maven_folder)
    174                ),
    175            )
    176 
    177 
    178 @SubCommand(
    179    "android",
    180    "archive-geckoview",
    181    """Create GeckoView archives.
    182    See http://firefox-source-docs.mozilla.org/build/buildsystem/toolchains.html#firefox-for-android-with-gradle""",  # NOQA: E501
    183 )
    184 @CommandArgument("args", nargs=argparse.REMAINDER)
    185 def android_archive_geckoview(command_context, args):
    186    tasks = command_context.substs["GRADLE_ANDROID_ARCHIVE_GECKOVIEW_TASKS"]
    187    subproject = command_context.substs.get("MOZ_ANDROID_SUBPROJECT")
    188    if subproject in (None, "geckoview_example"):
    189        tasks += command_context.substs[
    190            "GRADLE_ANDROID_ARCHIVE_GECKOVIEW_SUBPROJECT_TASKS"
    191        ]
    192    ret = gradle(
    193        command_context,
    194        tasks + args,
    195        verbose=True,
    196    )
    197 
    198    if ret != 0:
    199        return ret
    200    create_maven_archive(command_context.topobjdir)
    201 
    202    return 0
    203 
    204 
    205 @SubCommand("android", "build-geckoview_example", """Build geckoview_example """)
    206 @CommandArgument("args", nargs=argparse.REMAINDER)
    207 def android_build_geckoview_example(command_context, args):
    208    gradle(
    209        command_context,
    210        command_context.substs["GRADLE_ANDROID_BUILD_GECKOVIEW_EXAMPLE_TASKS"] + args,
    211        verbose=True,
    212    )
    213 
    214    print(
    215        "Execute `mach android install-geckoview_example` "
    216        "to push the geckoview_example and test APKs to a device."
    217    )
    218 
    219    return 0
    220 
    221 
    222 @SubCommand("android", "compile-all", """Build all source files""")
    223 @CommandArgument("args", nargs=argparse.REMAINDER)
    224 def android_compile_all(command_context, args):
    225    ret = gradle(
    226        command_context,
    227        command_context.substs["GRADLE_ANDROID_COMPILE_ALL_TASKS"] + args,
    228        verbose=True,
    229    )
    230 
    231    return ret
    232 
    233 
    234 def install_app_bundle(command_context, bundle):
    235    from mozdevice import ADBDeviceFactory
    236 
    237    bundletool = mozpath.join(command_context._mach_context.state_dir, "bundletool.jar")
    238    device = ADBDeviceFactory(verbose=True)
    239    bundle_path = mozpath.join(command_context.topobjdir, bundle)
    240    java_home = os.path.dirname(os.path.dirname(command_context.substs["JAVA"]))
    241    device.install_app_bundle(bundletool, bundle_path, java_home, timeout=120)
    242 
    243 
    244 @SubCommand("android", "install-geckoview_example", """Install geckoview_example """)
    245 @CommandArgument("args", nargs=argparse.REMAINDER)
    246 def android_install_geckoview_example(command_context, args):
    247    gradle(
    248        command_context,
    249        command_context.substs["GRADLE_ANDROID_INSTALL_GECKOVIEW_EXAMPLE_TASKS"] + args,
    250        verbose=True,
    251    )
    252 
    253    print(
    254        "Execute `mach android build-geckoview_example` "
    255        "to just build the geckoview_example and test APKs."
    256    )
    257 
    258    return 0
    259 
    260 
    261 @SubCommand("android", "install-fenix", """Install fenix """)
    262 @CommandArgument("args", nargs=argparse.REMAINDER)
    263 def android_install_fenix(command_context, args):
    264    gradle(
    265        command_context,
    266        ["fenix:installDebug"] + args,
    267        verbose=True,
    268    )
    269    return 0
    270 
    271 
    272 @SubCommand("android", "install-fenix-nightly", """Install fenix Nightly""")
    273 @CommandArgument("args", nargs=argparse.REMAINDER)
    274 def android_install_fenix_nightly(command_context, args):
    275    gradle(
    276        command_context,
    277        ["fenix:installNightly"] + args,
    278        verbose=True,
    279    )
    280    return 0
    281 
    282 
    283 @SubCommand("android", "install-fenix-beta", """Install fenix Beta""")
    284 @CommandArgument("args", nargs=argparse.REMAINDER)
    285 def android_install_fenix_beta(command_context, args):
    286    gradle(
    287        command_context,
    288        ["fenix:installBeta"] + args,
    289        verbose=True,
    290    )
    291    return 0
    292 
    293 
    294 @SubCommand("android", "install-fenix-release", """Install fenix Release""")
    295 @CommandArgument("args", nargs=argparse.REMAINDER)
    296 def android_install_fenix_release(command_context, args):
    297    gradle(
    298        command_context,
    299        ["fenix:installRelease"] + args,
    300        verbose=True,
    301    )
    302    return 0
    303 
    304 
    305 @SubCommand("android", "install-focus", """Install focus """)
    306 @CommandArgument("args", nargs=argparse.REMAINDER)
    307 def android_install_focus(command_context, args):
    308    gradle(
    309        command_context,
    310        ["focus-android:installFocusDebug"] + args,
    311        verbose=True,
    312    )
    313    return 0
    314 
    315 
    316 @SubCommand(
    317    "android", "install-geckoview-test_runner", """Install geckoview.test_runner """
    318 )
    319 @CommandArgument("args", nargs=argparse.REMAINDER)
    320 def android_install_geckoview_test_runner(command_context, args):
    321    gradle(
    322        command_context,
    323        command_context.substs["GRADLE_ANDROID_INSTALL_GECKOVIEW_TEST_RUNNER_TASKS"]
    324        + args,
    325        verbose=True,
    326    )
    327    return 0
    328 
    329 
    330 @SubCommand(
    331    "android",
    332    "install-geckoview-test_runner-aab",
    333    """Install geckoview.test_runner with AAB""",
    334 )
    335 @CommandArgument("args", nargs=argparse.REMAINDER)
    336 def android_install_geckoview_test_runner_aab(command_context, args):
    337    install_app_bundle(
    338        command_context,
    339        command_context.substs["GRADLE_ANDROID_GECKOVIEW_TEST_RUNNER_BUNDLE"],
    340    )
    341    return 0
    342 
    343 
    344 @SubCommand(
    345    "android",
    346    "install-geckoview_example-aab",
    347    """Install geckoview_example with AAB""",
    348 )
    349 @CommandArgument("args", nargs=argparse.REMAINDER)
    350 def android_install_geckoview_example_aab(command_context, args):
    351    install_app_bundle(
    352        command_context,
    353        command_context.substs["GRADLE_ANDROID_GECKOVIEW_EXAMPLE_BUNDLE"],
    354    )
    355    return 0
    356 
    357 
    358 @SubCommand("android", "install-geckoview-test", """Install geckoview.test """)
    359 @CommandArgument("args", nargs=argparse.REMAINDER)
    360 def android_install_geckoview_test(command_context, args):
    361    gradle(
    362        command_context,
    363        command_context.substs["GRADLE_ANDROID_INSTALL_GECKOVIEW_TEST_TASKS"] + args,
    364        verbose=True,
    365    )
    366    return 0
    367 
    368 
    369 @SubCommand(
    370    "android",
    371    "geckoview-docs",
    372    """Create GeckoView javadoc and optionally upload to Github""",
    373 )
    374 @CommandArgument("--archive", action="store_true", help="Generate a javadoc archive.")
    375 @CommandArgument(
    376    "--upload",
    377    metavar="USER/REPO",
    378    help="Upload geckoview documentation to Github, using the specified USER/REPO.",
    379 )
    380 @CommandArgument(
    381    "--upload-branch",
    382    metavar="BRANCH[/PATH]",
    383    default="gh-pages",
    384    help="Use the specified branch/path for documentation commits.",
    385 )
    386 @CommandArgument(
    387    "--javadoc-path",
    388    metavar="/PATH",
    389    default="javadoc",
    390    help="Use the specified path for javadoc commits.",
    391 )
    392 @CommandArgument(
    393    "--upload-message",
    394    metavar="MSG",
    395    default="GeckoView docs upload",
    396    help="Use the specified message for commits.",
    397 )
    398 def android_geckoview_docs(
    399    command_context,
    400    archive,
    401    upload,
    402    upload_branch,
    403    javadoc_path,
    404    upload_message,
    405 ):
    406    tasks = (
    407        command_context.substs["GRADLE_ANDROID_GECKOVIEW_DOCS_ARCHIVE_TASKS"]
    408        if archive or upload
    409        else command_context.substs["GRADLE_ANDROID_GECKOVIEW_DOCS_TASKS"]
    410    )
    411 
    412    ret = gradle(command_context, tasks, verbose=True)
    413    if ret or not upload:
    414        return ret
    415 
    416    # Upload to Github.
    417    fmt = {
    418        "level": os.environ.get("MOZ_SCM_LEVEL", "0"),
    419        "project": os.environ.get("MH_BRANCH", "unknown"),
    420        "revision": os.environ.get("GECKO_HEAD_REV", "tip"),
    421    }
    422    env = {}
    423 
    424    # In order to push to GitHub from TaskCluster, we store a private key
    425    # in the TaskCluster secrets store in the format {"content": "<KEY>"},
    426    # and the corresponding public key as a writable deploy key for the
    427    # destination repo on GitHub.
    428    secret = os.environ.get("GECKOVIEW_DOCS_UPLOAD_SECRET", "").format(**fmt)
    429    if secret:
    430        # Set up a private key from the secrets store if applicable.
    431        import requests
    432 
    433        req = requests.get("http://taskcluster/secrets/v1/secret/" + secret)
    434        req.raise_for_status()
    435 
    436        keyfile = mozpath.abspath("gv-docs-upload-key")
    437        with open(keyfile, "w") as f:
    438            os.chmod(keyfile, 0o600)
    439            f.write(req.json()["secret"]["content"])
    440 
    441        # Turn off strict host key checking so ssh does not complain about
    442        # unknown github.com host. We're not pushing anything sensitive, so
    443        # it's okay to not check GitHub's host keys.
    444        env["GIT_SSH_COMMAND"] = 'ssh -i "%s" -o StrictHostKeyChecking=no' % keyfile
    445 
    446    # Clone remote repo.
    447    branch = upload_branch.format(**fmt)
    448    repo_url = "git@github.com:%s.git" % upload
    449    repo_path = mozpath.abspath("gv-docs-repo")
    450    command_context.run_process(
    451        [
    452            "git",
    453            "clone",
    454            "--branch",
    455            upload_branch,
    456            "--depth",
    457            "1",
    458            repo_url,
    459            repo_path,
    460        ],
    461        append_env=env,
    462        pass_thru=True,
    463    )
    464    env["GIT_DIR"] = mozpath.join(repo_path, ".git")
    465    env["GIT_WORK_TREE"] = repo_path
    466    env["GIT_AUTHOR_NAME"] = env["GIT_COMMITTER_NAME"] = "GeckoView Docs Bot"
    467    env["GIT_AUTHOR_EMAIL"] = env["GIT_COMMITTER_EMAIL"] = "nobody@mozilla.com"
    468 
    469    # Copy over user documentation.
    470    import mozfile
    471 
    472    # Extract new javadoc to specified directory inside repo.
    473    src_tar = mozpath.join(
    474        command_context.topobjdir,
    475        "gradle",
    476        "build",
    477        "mobile",
    478        "android",
    479        "geckoview",
    480        "libs",
    481        "geckoview-javadoc.jar",
    482    )
    483    dst_path = mozpath.join(repo_path, javadoc_path.format(**fmt))
    484    mozfile.remove(dst_path)
    485    mozfile.extract_zip(src_tar, dst_path)
    486 
    487    # Commit and push.
    488    command_context.run_process(["git", "add", "--all"], append_env=env, pass_thru=True)
    489    if (
    490        command_context.run_process(
    491            ["git", "diff", "--cached", "--quiet"],
    492            append_env=env,
    493            pass_thru=True,
    494            ensure_exit_code=False,
    495        )
    496        != 0
    497    ):
    498        # We have something to commit.
    499        command_context.run_process(
    500            ["git", "commit", "--message", upload_message.format(**fmt)],
    501            append_env=env,
    502            pass_thru=True,
    503        )
    504        command_context.run_process(
    505            ["git", "push", "origin", branch], append_env=env, pass_thru=True
    506        )
    507 
    508    mozfile.remove(repo_path)
    509    if secret:
    510        mozfile.remove(keyfile)
    511    return 0
    512 
    513 
    514 @Command(
    515    "gradle",
    516    category="devenv",
    517    description="Run gradle.",
    518    conditions=[conditions.is_android],
    519 )
    520 @CommandArgument(
    521    "-v",
    522    "--verbose",
    523    action="store_true",
    524    help="Verbose output for what commands the build is running.",
    525 )
    526 @CommandArgument("args", nargs=argparse.REMAINDER)
    527 def gradle(command_context, args, verbose=False, gradle_path=None, topsrcdir=None):
    528    if not verbose:
    529        # Avoid logging the command
    530        command_context.log_manager.terminal_handler.setLevel(logging.CRITICAL)
    531 
    532    if not gradle_path:
    533        gradle_path = command_context.substs["GRADLE"]
    534 
    535    if not topsrcdir:
    536        topsrcdir = mozpath.join(command_context.topsrcdir)
    537 
    538    # In automation, JAVA_HOME is set via mozconfig, which needs
    539    # to be specially handled in each mach command. This turns
    540    # $JAVA_HOME/bin/java into $JAVA_HOME.
    541    java_home = os.path.dirname(os.path.dirname(command_context.substs["JAVA"]))
    542 
    543    gradle_flags = command_context.substs.get("GRADLE_FLAGS", "") or os.environ.get(
    544        "GRADLE_FLAGS", ""
    545    )
    546    gradle_flags = shell_split(gradle_flags)
    547 
    548    # We force the Gradle JVM to run with the UTF-8 encoding, since we
    549    # filter strings.xml, which is really UTF-8; the ellipsis character is
    550    # replaced with ??? in some encodings (including ASCII).  It's not yet
    551    # possible to filter with encodings in Gradle
    552    # (https://github.com/gradle/gradle/pull/520) and it's challenging to
    553    # do our filtering with Gradle's Ant support.  Moreover, all of the
    554    # Android tools expect UTF-8: see
    555    # http://tools.android.com/knownissues/encoding.  See
    556    # http://stackoverflow.com/a/21267635 for discussion of this approach.
    557    #
    558    # It's not even enough to set the encoding just for Gradle; it
    559    # needs to be for JVMs spawned by Gradle as well.  This
    560    # happens during the maven deployment generating the GeckoView
    561    # documents; this works around "error: unmappable character
    562    # for encoding ASCII" in exoplayer2.  See
    563    # https://discuss.gradle.org/t/unmappable-character-for-encoding-ascii-when-building-a-utf-8-project/10692/11  # NOQA: E501
    564    # and especially https://stackoverflow.com/a/21755671.
    565 
    566    if command_context.substs.get("MOZ_AUTOMATION"):
    567        gradle_flags += ["--console=plain"]
    568 
    569    env = os.environ.copy()
    570 
    571    env.update({
    572        "GRADLE_OPTS": "-Dfile.encoding=utf-8",
    573        "JAVA_HOME": java_home,
    574        "JAVA_TOOL_OPTIONS": "-Dfile.encoding=utf-8",
    575        # Let Gradle get the right Python path on Windows
    576        "GRADLE_MACH_PYTHON": sys.executable,
    577    })
    578    # Set ANDROID_SDK_ROOT if --with-android-sdk was set.
    579    # See https://bugzilla.mozilla.org/show_bug.cgi?id=1576471
    580    android_sdk_root = command_context.substs.get("ANDROID_SDK_ROOT", "")
    581    if android_sdk_root:
    582        env["ANDROID_HOME"] = android_sdk_root
    583        env["ANDROID_SDK_ROOT"] = android_sdk_root
    584 
    585    should_print_status = env.get("MACH") and not env.get("NO_BUILDSTATUS_MESSAGES")
    586    if should_print_status:
    587        print("BUILDSTATUS " + str(time.time()) + " START_Gradle " + args[0])
    588    rv = command_context.run_process(
    589        [gradle_path] + gradle_flags + args,
    590        explicit_env=env,
    591        pass_thru=True,  # Allow user to run gradle interactively.
    592        ensure_exit_code=False,  # Don't throw on non-zero exit code.
    593        cwd=topsrcdir,
    594    )
    595    if should_print_status:
    596        print("BUILDSTATUS " + str(time.time()) + " END_Gradle " + args[0])
    597    return rv
    598 
    599 
    600 @Command("gradle-install", category="devenv", conditions=[REMOVED])
    601 def gradle_install_REMOVED(command_context):
    602    pass
    603 
    604 
    605 @Command(
    606    "android-emulator",
    607    category="devenv",
    608    conditions=[],
    609    description="Run the Android emulator with an AVD from test automation. "
    610    "Environment variable MOZ_EMULATOR_COMMAND_ARGS, if present, will "
    611    "over-ride the command line arguments used to launch the emulator.",
    612 )
    613 @CommandArgument(
    614    "--version",
    615    metavar="VERSION",
    616    choices=["arm", "arm64", "x86_64"],
    617    help="Specify which AVD to run in emulator. "
    618    'One of "arm" (Android supporting armv7 binaries), '
    619    '"arm64" (for Apple Silicon), or '
    620    '"x86_64" (Android supporting x86 or x86_64 binaries, '
    621    "recommended for most applications). "
    622    "By default, the value will match the current build environment.",
    623 )
    624 @CommandArgument("--wait", action="store_true", help="Wait for emulator to be closed.")
    625 @CommandArgument("--gpu", help="Over-ride the emulator -gpu argument.")
    626 @CommandArgument(
    627    "--verbose", action="store_true", help="Log informative status messages."
    628 )
    629 def emulator(
    630    command_context,
    631    version,
    632    wait=False,
    633    gpu=None,
    634    verbose=False,
    635 ):
    636    """
    637    Run the Android emulator with one of the AVDs used in the Mozilla
    638    automated test environment. If necessary, the AVD is fetched from
    639    the taskcluster server and installed.
    640    """
    641    from mozrunner.devices.android_device import AndroidEmulator
    642 
    643    emulator = AndroidEmulator(
    644        version,
    645        verbose,
    646        substs=command_context.substs,
    647        device_serial="emulator-5554",
    648    )
    649    if emulator.is_running():
    650        # It is possible to run multiple emulators simultaneously, but:
    651        #  - if more than one emulator is using the same avd, errors may
    652        #    occur due to locked resources;
    653        #  - additional parameters must be specified when running tests,
    654        #    to select a specific device.
    655        # To avoid these complications, allow just one emulator at a time.
    656        command_context.log(
    657            logging.ERROR,
    658            "emulator",
    659            {},
    660            "An Android emulator is already running.\n"
    661            "Close the existing emulator and re-run this command.",
    662        )
    663        return 1
    664 
    665    if not emulator.check_avd():
    666        command_context.log(
    667            logging.WARN,
    668            "emulator",
    669            {},
    670            "AVD not found. Please run |mach bootstrap|.",
    671        )
    672        return 2
    673 
    674    if not emulator.is_available():
    675        command_context.log(
    676            logging.WARN,
    677            "emulator",
    678            {},
    679            "Emulator binary not found. Try |mach bootstrap|\n",
    680        )
    681        return 2
    682 
    683    command_context.log(
    684        logging.INFO,
    685        "emulator",
    686        {},
    687        "Starting Android emulator running %s..." % emulator.get_avd_description(),
    688    )
    689    emulator.start(gpu)
    690    if emulator.wait_for_start():
    691        command_context.log(
    692            logging.INFO, "emulator", {}, "Android emulator is running."
    693        )
    694    else:
    695        # This is unusual but the emulator may still function.
    696        command_context.log(
    697            logging.WARN,
    698            "emulator",
    699            {},
    700            "Unable to verify that emulator is running.",
    701        )
    702 
    703    if conditions.is_android(command_context):
    704        command_context.log(
    705            logging.INFO,
    706            "emulator",
    707            {},
    708            "Use 'mach install' to install or update Firefox on your emulator.",
    709        )
    710    else:
    711        command_context.log(
    712            logging.WARN,
    713            "emulator",
    714            {},
    715            "No Firefox for Android build detected.\n"
    716            "Switch to a Firefox for Android build context or use 'mach bootstrap'\n"
    717            "to setup an Android build environment.",
    718        )
    719 
    720    if wait:
    721        command_context.log(
    722            logging.INFO, "emulator", {}, "Waiting for Android emulator to close..."
    723        )
    724        rc = emulator.wait()
    725        if rc is not None:
    726            command_context.log(
    727                logging.INFO,
    728                "emulator",
    729                {},
    730                "Android emulator completed with return code %d." % rc,
    731            )
    732        else:
    733            command_context.log(
    734                logging.WARN,
    735                "emulator",
    736                {},
    737                "Unable to retrieve Android emulator return code.",
    738            )
    739    return 0
    740 
    741 
    742 @SubCommand(
    743    command="android-emulator",
    744    subcommand="reset",
    745    description="Resets the emulator and Android Virtual Device (AVD) by removing the "
    746    "'ANDROID_AVD_HOME' directory and re-bootstrapping the emulator and AVD.",
    747 )
    748 def emulator_reset(command_context):
    749    from mozboot import android
    750 
    751    os_arch = platform.machine()
    752    os_name = None
    753    if platform.system() == "Windows":
    754        os_name = "windows"
    755    elif platform.system() == "Linux":
    756        os_name = "linux"
    757    elif platform.system() == "Darwin":
    758        os_name = "macosx"
    759    else:
    760        raise Exception("Can't reset AVD on an unknown system")
    761 
    762    avd_home_path = android.AVD_HOME_PATH
    763 
    764    if avd_home_path.exists():
    765        command_context.log(
    766            logging.INFO, "emulator", {}, f"Removing AVD directory: '{avd_home_path}'"
    767        )
    768        try:
    769            shutil.rmtree(avd_home_path)
    770            command_context.log(
    771                logging.INFO,
    772                "emulator",
    773                {},
    774                f"Successfully removed AVD directory: '{avd_home_path}'",
    775            )
    776        except FileNotFoundError:
    777            pass  # Directory doesn't exist, do nothing
    778        except Exception as e:
    779            command_context.log(
    780                logging.ERROR,
    781                "emulator",
    782                {},
    783                f"Failed to remove the AVD directory: '{avd_home_path}': {e}",
    784            )
    785 
    786    sdk_manager_tool_path = android.get_sdkmanager_tool_path(
    787        android.get_sdk_path(os_name)
    788    )
    789    if not sdk_manager_tool_path.exists():
    790        command_context.log(
    791            logging.ERROR,
    792            "emulator",
    793            {},
    794            f"Unable to proceed - 'sdkmanager' not found at {sdk_manager_tool_path}. "
    795            f"Please run './mach bootstrap' to reinstall your Android SDK.",
    796        )
    797        return 1
    798 
    799    normalized_arch = os_arch.lower()
    800 
    801    if "x86" in normalized_arch or "amd64" in normalized_arch:
    802        avd_manifest_path_for_arch = android.AVD_MANIFEST_X86_64
    803    else:
    804        avd_manifest_path_for_arch = android.AVD_MANIFEST_ARM64
    805 
    806    command_context.log(
    807        logging.INFO,
    808        "emulator",
    809        {},
    810        f"Resetting emulator and AVD. AVD_MANIFEST_PATH='{avd_manifest_path_for_arch}'",
    811    )
    812 
    813    packages = android.get_android_packages(android.AndroidPackageList.EMULATOR)
    814    avd_manifest = android.get_avd_manifest(avd_manifest_path_for_arch)
    815 
    816    android.ensure_android_packages(
    817        os_name,
    818        os_arch,
    819        packages,
    820        no_interactive=True,
    821        avd_manifest=avd_manifest,
    822    )
    823 
    824    android.ensure_android_avd(
    825        os_name,
    826        os_arch,
    827        no_interactive=True,
    828        avd_manifest=avd_manifest,
    829    )
    830 
    831 
    832 @Command(
    833    "adb",
    834    category="devenv",
    835    description="Run the version of Android Debug Bridge (adb) utility that the build system would use.",
    836 )
    837 @CommandArgument("args", nargs=argparse.REMAINDER)
    838 def adb(
    839    command_context,
    840    args,
    841 ):
    842    """Run the version of Android Debug Bridge (adb) utility that the build
    843    system would use."""
    844    from mozrunner.devices.android_device import get_adb_path
    845 
    846    adb_path = get_adb_path(command_context)
    847    if not adb_path:
    848        command_context.log(
    849            logging.ERROR,
    850            "adb",
    851            {},
    852            "ADB not found. Did you run `mach bootstrap` with Android selected yet?",
    853        )
    854        return 1
    855 
    856    return command_context.run_process(
    857        [adb_path] + args, pass_thru=True, ensure_exit_code=False
    858    )