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 )