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/.
13 import mozpack
.path
as mozpath
14 from mach
.decorators
import Command
, CommandArgument
, SubCommand
15 from mozbuild
.base
import MachCommandConditions
as conditions
16 from mozbuild
.shellutil
import split
as shell_split
17 from mozfile
import which
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}`.
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.
33 """Command no longer exists! Use the Gradle configuration rooted in the top source directory
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
44 description
="Run Android-specific commands.",
45 conditions
=[conditions
.is_android
],
47 def android(command_context
):
54 """Assemble Firefox for Android.
55 See http://firefox-source-docs.mozilla.org/build/buildsystem/toolchains.html#firefox-for-android-with-gradle""", # NOQA: E501
57 @CommandArgument("args", nargs
=argparse
.REMAINDER
)
58 def android_assemble_app(command_context
, args
):
61 command_context
.substs
["GRADLE_ANDROID_APP_TASKS"] + ["-x", "lint"] + args
,
70 "generate-sdk-bindings",
71 """Generate SDK bindings used when building GeckoView.""",
76 help="config files, like [/path/to/ClassName-classes.txt]+",
78 @CommandArgument("args", nargs
=argparse
.REMAINDER
)
79 def android_generate_sdk_bindings(command_context
, inputs
, args
):
83 # Turn "/path/to/ClassName-classes.txt" into "ClassName".
84 return os
.path
.basename(input).rsplit("-classes.txt", 1)[0]
86 bindings_inputs
= list(itertools
.chain(*((input, stem(input)) for input in inputs
)))
87 bindings_args
= "-Pgenerate_sdk_bindings_args={}".format(";".join(bindings_inputs
))
91 command_context
.substs
["GRADLE_ANDROID_GENERATE_SDK_BINDINGS_TASKS"]
102 "generate-generated-jni-wrappers",
103 """Generate GeckoView JNI wrappers used when building GeckoView.""",
105 @CommandArgument("args", nargs
=argparse
.REMAINDER
)
106 def android_generate_generated_jni_wrappers(command_context
, args
):
109 command_context
.substs
["GRADLE_ANDROID_GENERATE_GENERATED_JNI_WRAPPERS_TASKS"]
120 """Run Android api-lint.
121 REMOVED/DEPRECATED: Use 'mach lint --linter android-api-lint'.""",
123 def android_apilint_REMOVED(command_context
):
124 print(LINT_DEPRECATION_MESSAGE
)
132 REMOVED/DEPRECATED: Use 'mach lint --linter android-test'.""",
134 def android_test_REMOVED(command_context
):
135 print(LINT_DEPRECATION_MESSAGE
)
143 REMOVED/DEPRECATED: Use 'mach lint --linter android-lint'.""",
145 def android_lint_REMOVED(command_context
):
146 print(LINT_DEPRECATION_MESSAGE
)
153 """Run Android checkstyle.
154 REMOVED/DEPRECATED: Use 'mach lint --linter android-checkstyle'.""",
156 def android_checkstyle_REMOVED(command_context
):
157 print(LINT_DEPRECATION_MESSAGE
)
163 "gradle-dependencies",
164 """Collect Android Gradle dependencies.
165 See http://firefox-source-docs.mozilla.org/build/buildsystem/toolchains.html#firefox-for-android-with-gradle""", # NOQA: E501
167 @CommandArgument("args", nargs
=argparse
.REMAINDER
)
168 def android_gradle_dependencies(command_context
, args
):
169 # We don't want to gate producing dependency archives on clean
170 # lint or checkstyle, particularly because toolchain versions
171 # can change the outputs for those processes.
174 command_context
.substs
["GRADLE_ANDROID_DEPENDENCIES_TASKS"]
183 def get_maven_archive_paths(maven_folder
):
184 for subdir
, _
, files
in os
.walk(maven_folder
):
185 if "-SNAPSHOT" in subdir
:
188 yield os
.path
.join(subdir
, file)
191 def create_maven_archive(topobjdir
):
192 gradle_folder
= os
.path
.join(topobjdir
, "gradle")
193 maven_folder
= os
.path
.join(gradle_folder
, "maven")
196 os
.path
.join(gradle_folder
, "target.maven.tar.xz"), "w|xz"
198 for abs_path
in get_maven_archive_paths(maven_folder
):
201 arcname
=os
.path
.join(
202 "geckoview", os
.path
.relpath(abs_path
, maven_folder
)
210 """Create GeckoView archives.
211 See http://firefox-source-docs.mozilla.org/build/buildsystem/toolchains.html#firefox-for-android-with-gradle""", # NOQA: E501
213 @CommandArgument("args", nargs
=argparse
.REMAINDER
)
214 def android_archive_geckoview(command_context
, args
):
217 command_context
.substs
["GRADLE_ANDROID_ARCHIVE_GECKOVIEW_TASKS"] + args
,
223 if "MOZ_AUTOMATION" in os
.environ
:
224 create_maven_archive(command_context
.topobjdir
)
229 @SubCommand("android", "build-geckoview_example", """Build geckoview_example """)
230 @CommandArgument("args", nargs
=argparse
.REMAINDER
)
231 def android_build_geckoview_example(command_context
, args
):
234 command_context
.substs
["GRADLE_ANDROID_BUILD_GECKOVIEW_EXAMPLE_TASKS"] + args
,
239 "Execute `mach android install-geckoview_example` "
240 "to push the geckoview_example and test APKs to a device."
246 @SubCommand("android", "compile-all", """Build all source files""")
247 @CommandArgument("args", nargs
=argparse
.REMAINDER
)
248 def android_compile_all(command_context
, args
):
251 command_context
.substs
["GRADLE_ANDROID_COMPILE_ALL_TASKS"] + args
,
258 def install_app_bundle(command_context
, bundle
):
259 from mozdevice
import ADBDeviceFactory
261 bundletool
= mozpath
.join(command_context
._mach
_context
.state_dir
, "bundletool.jar")
262 device
= ADBDeviceFactory(verbose
=True)
263 bundle_path
= mozpath
.join(command_context
.topobjdir
, bundle
)
264 java_home
= java_home
= os
.path
.dirname(
265 os
.path
.dirname(command_context
.substs
["JAVA"])
267 device
.install_app_bundle(bundletool
, bundle_path
, java_home
, timeout
=120)
270 @SubCommand("android", "install-geckoview_example", """Install geckoview_example """)
271 @CommandArgument("args", nargs
=argparse
.REMAINDER
)
272 def android_install_geckoview_example(command_context
, args
):
275 command_context
.substs
["GRADLE_ANDROID_INSTALL_GECKOVIEW_EXAMPLE_TASKS"] + args
,
280 "Execute `mach android build-geckoview_example` "
281 "to just build the geckoview_example and test APKs."
288 "android", "install-geckoview-test_runner", """Install geckoview.test_runner """
290 @CommandArgument("args", nargs
=argparse
.REMAINDER
)
291 def android_install_geckoview_test_runner(command_context
, args
):
294 command_context
.substs
["GRADLE_ANDROID_INSTALL_GECKOVIEW_TEST_RUNNER_TASKS"]
303 "install-geckoview-test_runner-aab",
304 """Install geckoview.test_runner with AAB""",
306 @CommandArgument("args", nargs
=argparse
.REMAINDER
)
307 def android_install_geckoview_test_runner_aab(command_context
, args
):
310 command_context
.substs
["GRADLE_ANDROID_GECKOVIEW_TEST_RUNNER_BUNDLE"],
317 "install-geckoview_example-aab",
318 """Install geckoview_example with AAB""",
320 @CommandArgument("args", nargs
=argparse
.REMAINDER
)
321 def android_install_geckoview_example_aab(command_context
, args
):
324 command_context
.substs
["GRADLE_ANDROID_GECKOVIEW_EXAMPLE_BUNDLE"],
329 @SubCommand("android", "install-geckoview-test", """Install geckoview.test """)
330 @CommandArgument("args", nargs
=argparse
.REMAINDER
)
331 def android_install_geckoview_test(command_context
, args
):
334 command_context
.substs
["GRADLE_ANDROID_INSTALL_GECKOVIEW_TEST_TASKS"] + args
,
343 """Create GeckoView javadoc and optionally upload to Github""",
345 @CommandArgument("--archive", action
="store_true", help="Generate a javadoc archive.")
349 help="Upload geckoview documentation to Github, using the specified USER/REPO.",
353 metavar
="BRANCH[/PATH]",
355 help="Use the specified branch/path for documentation commits.",
361 help="Use the specified path for javadoc commits.",
366 default
="GeckoView docs upload",
367 help="Use the specified message for commits.",
369 def android_geckoview_docs(
378 command_context
.substs
["GRADLE_ANDROID_GECKOVIEW_DOCS_ARCHIVE_TASKS"]
380 else command_context
.substs
["GRADLE_ANDROID_GECKOVIEW_DOCS_TASKS"]
383 ret
= gradle(command_context
, tasks
, verbose
=True)
384 if ret
or not upload
:
389 "level": os
.environ
.get("MOZ_SCM_LEVEL", "0"),
390 "project": os
.environ
.get("MH_BRANCH", "unknown"),
391 "revision": os
.environ
.get("GECKO_HEAD_REV", "tip"),
395 # In order to push to GitHub from TaskCluster, we store a private key
396 # in the TaskCluster secrets store in the format {"content": "<KEY>"},
397 # and the corresponding public key as a writable deploy key for the
398 # destination repo on GitHub.
399 secret
= os
.environ
.get("GECKOVIEW_DOCS_UPLOAD_SECRET", "").format(**fmt
)
401 # Set up a private key from the secrets store if applicable.
404 req
= requests
.get("http://taskcluster/secrets/v1/secret/" + secret
)
405 req
.raise_for_status()
407 keyfile
= mozpath
.abspath("gv-docs-upload-key")
408 with
open(keyfile
, "w") as f
:
409 os
.chmod(keyfile
, 0o600)
410 f
.write(req
.json()["secret"]["content"])
412 # Turn off strict host key checking so ssh does not complain about
413 # unknown github.com host. We're not pushing anything sensitive, so
414 # it's okay to not check GitHub's host keys.
415 env
["GIT_SSH_COMMAND"] = 'ssh -i "%s" -o StrictHostKeyChecking=no' % keyfile
418 branch
= upload_branch
.format(**fmt
)
419 repo_url
= "git@github.com:%s.git" % upload
420 repo_path
= mozpath
.abspath("gv-docs-repo")
421 command_context
.run_process(
435 env
["GIT_DIR"] = mozpath
.join(repo_path
, ".git")
436 env
["GIT_WORK_TREE"] = repo_path
437 env
["GIT_AUTHOR_NAME"] = env
["GIT_COMMITTER_NAME"] = "GeckoView Docs Bot"
438 env
["GIT_AUTHOR_EMAIL"] = env
["GIT_COMMITTER_EMAIL"] = "nobody@mozilla.com"
440 # Copy over user documentation.
443 # Extract new javadoc to specified directory inside repo.
444 src_tar
= mozpath
.join(
445 command_context
.topobjdir
,
452 "geckoview-javadoc.jar",
454 dst_path
= mozpath
.join(repo_path
, javadoc_path
.format(**fmt
))
455 mozfile
.remove(dst_path
)
456 mozfile
.extract_zip(src_tar
, dst_path
)
459 command_context
.run_process(["git", "add", "--all"], append_env
=env
, pass_thru
=True)
461 command_context
.run_process(
462 ["git", "diff", "--cached", "--quiet"],
465 ensure_exit_code
=False,
469 # We have something to commit.
470 command_context
.run_process(
471 ["git", "commit", "--message", upload_message
.format(**fmt
)],
475 command_context
.run_process(
476 ["git", "push", "origin", branch
], append_env
=env
, pass_thru
=True
479 mozfile
.remove(repo_path
)
481 mozfile
.remove(keyfile
)
488 description
="Run gradle.",
489 conditions
=[conditions
.is_android
],
495 help="Verbose output for what commands the build is running.",
497 @CommandArgument("args", nargs
=argparse
.REMAINDER
)
498 def gradle(command_context
, args
, verbose
=False):
500 # Avoid logging the command
501 command_context
.log_manager
.terminal_handler
.setLevel(logging
.CRITICAL
)
503 # In automation, JAVA_HOME is set via mozconfig, which needs
504 # to be specially handled in each mach command. This turns
505 # $JAVA_HOME/bin/java into $JAVA_HOME.
506 java_home
= os
.path
.dirname(os
.path
.dirname(command_context
.substs
["JAVA"]))
508 gradle_flags
= command_context
.substs
.get("GRADLE_FLAGS", "") or os
.environ
.get(
511 gradle_flags
= shell_split(gradle_flags
)
513 # We force the Gradle JVM to run with the UTF-8 encoding, since we
514 # filter strings.xml, which is really UTF-8; the ellipsis character is
515 # replaced with ??? in some encodings (including ASCII). It's not yet
516 # possible to filter with encodings in Gradle
517 # (https://github.com/gradle/gradle/pull/520) and it's challenging to
518 # do our filtering with Gradle's Ant support. Moreover, all of the
519 # Android tools expect UTF-8: see
520 # http://tools.android.com/knownissues/encoding. See
521 # http://stackoverflow.com/a/21267635 for discussion of this approach.
523 # It's not even enough to set the encoding just for Gradle; it
524 # needs to be for JVMs spawned by Gradle as well. This
525 # happens during the maven deployment generating the GeckoView
526 # documents; this works around "error: unmappable character
527 # for encoding ASCII" in exoplayer2. See
528 # https://discuss.gradle.org/t/unmappable-character-for-encoding-ascii-when-building-a-utf-8-project/10692/11 # NOQA: E501
529 # and especially https://stackoverflow.com/a/21755671.
531 if command_context
.substs
.get("MOZ_AUTOMATION"):
532 gradle_flags
+= ["--console=plain"]
534 env
= os
.environ
.copy()
537 "GRADLE_OPTS": "-Dfile.encoding=utf-8",
538 "JAVA_HOME": java_home
,
539 "JAVA_TOOL_OPTIONS": "-Dfile.encoding=utf-8",
540 # Let Gradle get the right Python path on Windows
541 "GRADLE_MACH_PYTHON": sys
.executable
,
544 # Set ANDROID_SDK_ROOT if --with-android-sdk was set.
545 # See https://bugzilla.mozilla.org/show_bug.cgi?id=1576471
546 android_sdk_root
= command_context
.substs
.get("ANDROID_SDK_ROOT", "")
548 env
["ANDROID_SDK_ROOT"] = android_sdk_root
550 return command_context
.run_process(
551 [command_context
.substs
["GRADLE"]] + gradle_flags
+ args
,
553 pass_thru
=True, # Allow user to run gradle interactively.
554 ensure_exit_code
=False, # Don't throw on non-zero exit code.
555 cwd
=mozpath
.join(command_context
.topsrcdir
),
559 @Command("gradle-install", category
="devenv", conditions
=[REMOVED
])
560 def gradle_install_REMOVED(command_context
):
568 description
="Run the Android emulator with an AVD from test automation. "
569 "Environment variable MOZ_EMULATOR_COMMAND_ARGS, if present, will "
570 "over-ride the command line arguments used to launch the emulator.",
575 choices
=["arm", "arm64", "x86_64"],
576 help="Specify which AVD to run in emulator. "
577 'One of "arm" (Android supporting armv7 binaries), '
578 '"arm64" (for Apple Silicon), or '
579 '"x86_64" (Android supporting x86 or x86_64 binaries, '
580 "recommended for most applications). "
581 "By default, the value will match the current build environment.",
583 @CommandArgument("--wait", action
="store_true", help="Wait for emulator to be closed.")
584 @CommandArgument("--gpu", help="Over-ride the emulator -gpu argument.")
586 "--verbose", action
="store_true", help="Log informative status messages."
596 Run the Android emulator with one of the AVDs used in the Mozilla
597 automated test environment. If necessary, the AVD is fetched from
598 the taskcluster server and installed.
600 from mozrunner
.devices
.android_device
import AndroidEmulator
602 emulator
= AndroidEmulator(
605 substs
=command_context
.substs
,
606 device_serial
="emulator-5554",
608 if emulator
.is_running():
609 # It is possible to run multiple emulators simultaneously, but:
610 # - if more than one emulator is using the same avd, errors may
611 # occur due to locked resources;
612 # - additional parameters must be specified when running tests,
613 # to select a specific device.
614 # To avoid these complications, allow just one emulator at a time.
619 "An Android emulator is already running.\n"
620 "Close the existing emulator and re-run this command.",
624 if not emulator
.check_avd():
629 "AVD not found. Please run |mach bootstrap|.",
633 if not emulator
.is_available():
638 "Emulator binary not found.\n"
639 "Install the Android SDK and make sure 'emulator' is in your PATH.",
647 "Starting Android emulator running %s..." % emulator
.get_avd_description(),
650 if emulator
.wait_for_start():
652 logging
.INFO
, "emulator", {}, "Android emulator is running."
655 # This is unusual but the emulator may still function.
660 "Unable to verify that emulator is running.",
663 if conditions
.is_android(command_context
):
668 "Use 'mach install' to install or update Firefox on your emulator.",
675 "No Firefox for Android build detected.\n"
676 "Switch to a Firefox for Android build context or use 'mach bootstrap'\n"
677 "to setup an Android build environment.",
682 logging
.INFO
, "emulator", {}, "Waiting for Android emulator to close..."
690 "Android emulator completed with return code %d." % rc
,
697 "Unable to retrieve Android emulator return code.",
705 description
="Uplift patch to https://github.com/mozilla-mobile/firefox-android.",
711 help="Revision or revisions to uplift. Supported values are the same as what your "
712 "VCS provides. Defaults to the current HEAD revision.",
715 "firefox_android_clone_dir",
716 help="The directory where your local clone of "
717 "https://github.com/mozilla-mobile/firefox-android repo is.",
722 firefox_android_clone_dir
,
724 revset
= _get_default_revset_if_needed(command_context
, revset
)
725 major_version
= _get_major_version(command_context
, revset
)
726 uplift_version
= major_version
- 1
727 bug_number
= _get_bug_number(command_context
, revset
)
729 f
"{uplift_version}-bug{bug_number}" if bug_number
else f
"{uplift_version}-nobug"
735 "new_branch_name": new_branch_name
,
736 "firefox_android_clone_dir": firefox_android_clone_dir
,
738 "Creating branch {new_branch_name} in {firefox_android_clone_dir}...",
742 _checkout_new_branch_updated_to_the_latest_remote(
743 command_context
, firefox_android_clone_dir
, uplift_version
, new_branch_name
745 _export_and_apply_revset(command_context
, revset
, firefox_android_clone_dir
)
746 except subprocess
.CalledProcessError
:
752 {"revset": revset
, "firefox_android_clone_dir": firefox_android_clone_dir
},
753 "Revision(s) {revset} now applied to {firefox_android_clone_dir}. Please go to "
754 "this directory, inspect the commit(s), and push.",
759 def _get_default_revset_if_needed(command_context
, revset
):
760 if revset
is not None:
762 if conditions
.is_hg(command_context
):
764 if conditions
.is_git(command_context
):
766 raise NotImplementedError()
769 def _get_major_version(command_context
, revset
):
770 milestone_txt
= _get_milestone_txt(command_context
, revset
)
771 version
= _extract_version_from_milestone_txt(milestone_txt
)
772 return _extract_major_version(version
)
775 def _get_bug_number(command_context
, revision
):
776 revision_message
= _extract_revision_message(command_context
, revision
)
777 return _extract_bug_number(revision_message
)
780 def _get_milestone_txt(command_context
, revset
):
781 if conditions
.is_hg(command_context
):
787 "config/milestone.txt",
789 elif conditions
.is_git(command_context
):
790 revision
= revset
.split("..")[-1]
794 f
"{revision}:config/milestone.txt",
797 raise NotImplementedError()
799 return subprocess
.check_output(args
, text
=True)
802 def _extract_version_from_milestone_txt(milestone_txt
):
803 return milestone_txt
.splitlines()[-1]
806 def _extract_major_version(version
):
807 return int(version
.split(".")[0])
810 def _extract_revision_message(command_context
, revision
):
811 if conditions
.is_hg(command_context
):
816 f
"first({revision})",
820 elif conditions
.is_git(command_context
):
830 raise NotImplementedError()
832 return subprocess
.check_output(args
, text
=True)
835 # Source: https://hg.mozilla.org/hgcustom/version-control-tools/file/cef43d3d676e9f9e9668a50a5d90c012e4025e5b/pylib/mozautomation/mozautomation/commitparser.py#l12
836 _BUG_TEMPLATE
= re
.compile(
837 r
"""# bug followed by any sequence of numbers, or
838 # a standalone sequence of numbers
843 # a sequence of 5+ numbers preceded by whitespace
845 # numbers at the very beginning
848 (?:\s*\#?)(\d+)(?=\b)
854 def _extract_bug_number(revision_message
):
856 return _BUG_TEMPLATE
.match(revision_message
).group(2)
857 except AttributeError:
861 _FIREFOX_ANDROID_URL
= "https://github.com/mozilla-mobile/firefox-android"
864 def _checkout_new_branch_updated_to_the_latest_remote(
865 command_context
, firefox_android_clone_dir
, uplift_version
, new_branch_name
870 _FIREFOX_ANDROID_URL
,
871 f
"+releases_v{uplift_version}:{new_branch_name}",
875 subprocess
.check_call(args
, cwd
=firefox_android_clone_dir
)
876 except subprocess
.CalledProcessError
:
881 "firefox_android_clone_dir": firefox_android_clone_dir
,
882 "new_branch_name": new_branch_name
,
884 "Could not fetch branch {new_branch_name}. This may be a network issue. If "
885 "not, please go to {firefox_android_clone_dir}, inspect the branch and "
886 "delete it (with `git branch -D {new_branch_name}`) if you don't have any "
887 "use for it anymore",
896 subprocess
.check_call(args
, cwd
=firefox_android_clone_dir
)
899 _MERCURIAL_REVISION_TO_GIT_COMMIT_TEMPLATE
= """
900 From 1234567890abcdef1234567890abcdef12345678 Sat Jan 1 00:00:00 2000
902 Date: {date|rfc822date}
908 def _export_and_apply_revset(command_context
, revset
, firefox_android_clone_dir
):
909 export_command
, import_command
= _get_export_import_commands(
910 command_context
, revset
913 export_process
= subprocess
.Popen(export_command
, stdout
=subprocess
.PIPE
)
915 subprocess
.check_call(
916 import_command
, stdin
=export_process
.stdout
, cwd
=firefox_android_clone_dir
918 except subprocess
.CalledProcessError
:
922 {"firefox_android_clone_dir": firefox_android_clone_dir
},
923 "Could not run `git am`. Please go to {firefox_android_clone_dir} and fix "
924 "the conflicts. Then run `git am --continue`.",
927 export_process
.wait()
930 def _get_export_import_commands(command_context
, revset
):
931 if conditions
.is_hg(command_context
):
939 _MERCURIAL_REVISION_TO_GIT_COMMIT_TEMPLATE
,
943 import_command
= [str(which("git")), "am", "-p3"]
944 elif conditions
.is_git(command_context
):
948 "--relative=mobile/android",
953 import_command
= [str(which("git")), "am"]
955 raise NotImplementedError()
957 return export_command
, import_command