Bug 1891710: part 2) Enable <Element-outerHTML.html> WPT for Trusted Types. r=smaug
[gecko.git] / mobile / android / mach_commands.py
blobfac483902cecfa37c880e8c9f486713fa044f006
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/.
5 import argparse
6 import logging
7 import os
8 import re
9 import subprocess
10 import sys
11 import tarfile
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}`.
25 Or run `mach lint`.
26 """
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.
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
41 @Command(
42 "android",
43 category="devenv",
44 description="Run Android-specific commands.",
45 conditions=[conditions.is_android],
47 def android(command_context):
48 pass
51 @SubCommand(
52 "android",
53 "assemble-app",
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):
59 ret = gradle(
60 command_context,
61 command_context.substs["GRADLE_ANDROID_APP_TASKS"] + ["-x", "lint"] + args,
62 verbose=True,
65 return ret
68 @SubCommand(
69 "android",
70 "generate-sdk-bindings",
71 """Generate SDK bindings used when building GeckoView.""",
73 @CommandArgument(
74 "inputs",
75 nargs="+",
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):
80 import itertools
82 def stem(input):
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))
89 ret = gradle(
90 command_context,
91 command_context.substs["GRADLE_ANDROID_GENERATE_SDK_BINDINGS_TASKS"]
92 + [bindings_args]
93 + args,
94 verbose=True,
97 return ret
100 @SubCommand(
101 "android",
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):
107 ret = gradle(
108 command_context,
109 command_context.substs["GRADLE_ANDROID_GENERATE_GENERATED_JNI_WRAPPERS_TASKS"]
110 + args,
111 verbose=True,
114 return ret
117 @SubCommand(
118 "android",
119 "api-lint",
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)
125 return 1
128 @SubCommand(
129 "android",
130 "test",
131 """Run Android test.
132 REMOVED/DEPRECATED: Use 'mach lint --linter android-test'.""",
134 def android_test_REMOVED(command_context):
135 print(LINT_DEPRECATION_MESSAGE)
136 return 1
139 @SubCommand(
140 "android",
141 "lint",
142 """Run Android lint.
143 REMOVED/DEPRECATED: Use 'mach lint --linter android-lint'.""",
145 def android_lint_REMOVED(command_context):
146 print(LINT_DEPRECATION_MESSAGE)
147 return 1
150 @SubCommand(
151 "android",
152 "checkstyle",
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)
158 return 1
161 @SubCommand(
162 "android",
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.
172 gradle(
173 command_context,
174 command_context.substs["GRADLE_ANDROID_DEPENDENCIES_TASKS"]
175 + ["--continue"]
176 + args,
177 verbose=True,
180 return 0
183 def get_maven_archive_paths(maven_folder):
184 for subdir, _, files in os.walk(maven_folder):
185 if "-SNAPSHOT" in subdir:
186 continue
187 for file in files:
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")
195 # Create the archive, with no compression: The archive contents are large
196 # files which cannot be significantly compressed; attempting to compress
197 # the archive is usually expensive in time and results in minimal
198 # reduction in size.
199 # Even though the archive is not compressed, use the .xz file extension
200 # so that the taskcluster worker also skips compression.
201 with tarfile.open(os.path.join(gradle_folder, "target.maven.tar.xz"), "w") as tar:
202 for abs_path in get_maven_archive_paths(maven_folder):
203 tar.add(
204 abs_path,
205 arcname=os.path.join(
206 "geckoview", os.path.relpath(abs_path, maven_folder)
211 @SubCommand(
212 "android",
213 "archive-geckoview",
214 """Create GeckoView archives.
215 See http://firefox-source-docs.mozilla.org/build/buildsystem/toolchains.html#firefox-for-android-with-gradle""", # NOQA: E501
217 @CommandArgument("args", nargs=argparse.REMAINDER)
218 def android_archive_geckoview(command_context, args):
219 ret = gradle(
220 command_context,
221 command_context.substs["GRADLE_ANDROID_ARCHIVE_GECKOVIEW_TASKS"] + args,
222 verbose=True,
225 if ret != 0:
226 return ret
227 if "MOZ_AUTOMATION" in os.environ:
228 create_maven_archive(command_context.topobjdir)
230 return 0
233 @SubCommand("android", "build-geckoview_example", """Build geckoview_example """)
234 @CommandArgument("args", nargs=argparse.REMAINDER)
235 def android_build_geckoview_example(command_context, args):
236 gradle(
237 command_context,
238 command_context.substs["GRADLE_ANDROID_BUILD_GECKOVIEW_EXAMPLE_TASKS"] + args,
239 verbose=True,
242 print(
243 "Execute `mach android install-geckoview_example` "
244 "to push the geckoview_example and test APKs to a device."
247 return 0
250 @SubCommand("android", "compile-all", """Build all source files""")
251 @CommandArgument("args", nargs=argparse.REMAINDER)
252 def android_compile_all(command_context, args):
253 gradle(
254 command_context,
255 command_context.substs["GRADLE_ANDROID_COMPILE_ALL_TASKS"] + args,
256 verbose=True,
259 return 0
262 def install_app_bundle(command_context, bundle):
263 from mozdevice import ADBDeviceFactory
265 bundletool = mozpath.join(command_context._mach_context.state_dir, "bundletool.jar")
266 device = ADBDeviceFactory(verbose=True)
267 bundle_path = mozpath.join(command_context.topobjdir, bundle)
268 java_home = java_home = os.path.dirname(
269 os.path.dirname(command_context.substs["JAVA"])
271 device.install_app_bundle(bundletool, bundle_path, java_home, timeout=120)
274 @SubCommand("android", "install-geckoview_example", """Install geckoview_example """)
275 @CommandArgument("args", nargs=argparse.REMAINDER)
276 def android_install_geckoview_example(command_context, args):
277 gradle(
278 command_context,
279 command_context.substs["GRADLE_ANDROID_INSTALL_GECKOVIEW_EXAMPLE_TASKS"] + args,
280 verbose=True,
283 print(
284 "Execute `mach android build-geckoview_example` "
285 "to just build the geckoview_example and test APKs."
288 return 0
291 @SubCommand(
292 "android", "install-geckoview-test_runner", """Install geckoview.test_runner """
294 @CommandArgument("args", nargs=argparse.REMAINDER)
295 def android_install_geckoview_test_runner(command_context, args):
296 gradle(
297 command_context,
298 command_context.substs["GRADLE_ANDROID_INSTALL_GECKOVIEW_TEST_RUNNER_TASKS"]
299 + args,
300 verbose=True,
302 return 0
305 @SubCommand(
306 "android",
307 "install-geckoview-test_runner-aab",
308 """Install geckoview.test_runner with AAB""",
310 @CommandArgument("args", nargs=argparse.REMAINDER)
311 def android_install_geckoview_test_runner_aab(command_context, args):
312 install_app_bundle(
313 command_context,
314 command_context.substs["GRADLE_ANDROID_GECKOVIEW_TEST_RUNNER_BUNDLE"],
316 return 0
319 @SubCommand(
320 "android",
321 "install-geckoview_example-aab",
322 """Install geckoview_example with AAB""",
324 @CommandArgument("args", nargs=argparse.REMAINDER)
325 def android_install_geckoview_example_aab(command_context, args):
326 install_app_bundle(
327 command_context,
328 command_context.substs["GRADLE_ANDROID_GECKOVIEW_EXAMPLE_BUNDLE"],
330 return 0
333 @SubCommand("android", "install-geckoview-test", """Install geckoview.test """)
334 @CommandArgument("args", nargs=argparse.REMAINDER)
335 def android_install_geckoview_test(command_context, args):
336 gradle(
337 command_context,
338 command_context.substs["GRADLE_ANDROID_INSTALL_GECKOVIEW_TEST_TASKS"] + args,
339 verbose=True,
341 return 0
344 @SubCommand(
345 "android",
346 "geckoview-docs",
347 """Create GeckoView javadoc and optionally upload to Github""",
349 @CommandArgument("--archive", action="store_true", help="Generate a javadoc archive.")
350 @CommandArgument(
351 "--upload",
352 metavar="USER/REPO",
353 help="Upload geckoview documentation to Github, using the specified USER/REPO.",
355 @CommandArgument(
356 "--upload-branch",
357 metavar="BRANCH[/PATH]",
358 default="gh-pages",
359 help="Use the specified branch/path for documentation commits.",
361 @CommandArgument(
362 "--javadoc-path",
363 metavar="/PATH",
364 default="javadoc",
365 help="Use the specified path for javadoc commits.",
367 @CommandArgument(
368 "--upload-message",
369 metavar="MSG",
370 default="GeckoView docs upload",
371 help="Use the specified message for commits.",
373 def android_geckoview_docs(
374 command_context,
375 archive,
376 upload,
377 upload_branch,
378 javadoc_path,
379 upload_message,
381 tasks = (
382 command_context.substs["GRADLE_ANDROID_GECKOVIEW_DOCS_ARCHIVE_TASKS"]
383 if archive or upload
384 else command_context.substs["GRADLE_ANDROID_GECKOVIEW_DOCS_TASKS"]
387 ret = gradle(command_context, tasks, verbose=True)
388 if ret or not upload:
389 return ret
391 # Upload to Github.
392 fmt = {
393 "level": os.environ.get("MOZ_SCM_LEVEL", "0"),
394 "project": os.environ.get("MH_BRANCH", "unknown"),
395 "revision": os.environ.get("GECKO_HEAD_REV", "tip"),
397 env = {}
399 # In order to push to GitHub from TaskCluster, we store a private key
400 # in the TaskCluster secrets store in the format {"content": "<KEY>"},
401 # and the corresponding public key as a writable deploy key for the
402 # destination repo on GitHub.
403 secret = os.environ.get("GECKOVIEW_DOCS_UPLOAD_SECRET", "").format(**fmt)
404 if secret:
405 # Set up a private key from the secrets store if applicable.
406 import requests
408 req = requests.get("http://taskcluster/secrets/v1/secret/" + secret)
409 req.raise_for_status()
411 keyfile = mozpath.abspath("gv-docs-upload-key")
412 with open(keyfile, "w") as f:
413 os.chmod(keyfile, 0o600)
414 f.write(req.json()["secret"]["content"])
416 # Turn off strict host key checking so ssh does not complain about
417 # unknown github.com host. We're not pushing anything sensitive, so
418 # it's okay to not check GitHub's host keys.
419 env["GIT_SSH_COMMAND"] = 'ssh -i "%s" -o StrictHostKeyChecking=no' % keyfile
421 # Clone remote repo.
422 branch = upload_branch.format(**fmt)
423 repo_url = "git@github.com:%s.git" % upload
424 repo_path = mozpath.abspath("gv-docs-repo")
425 command_context.run_process(
427 "git",
428 "clone",
429 "--branch",
430 upload_branch,
431 "--depth",
432 "1",
433 repo_url,
434 repo_path,
436 append_env=env,
437 pass_thru=True,
439 env["GIT_DIR"] = mozpath.join(repo_path, ".git")
440 env["GIT_WORK_TREE"] = repo_path
441 env["GIT_AUTHOR_NAME"] = env["GIT_COMMITTER_NAME"] = "GeckoView Docs Bot"
442 env["GIT_AUTHOR_EMAIL"] = env["GIT_COMMITTER_EMAIL"] = "nobody@mozilla.com"
444 # Copy over user documentation.
445 import mozfile
447 # Extract new javadoc to specified directory inside repo.
448 src_tar = mozpath.join(
449 command_context.topobjdir,
450 "gradle",
451 "build",
452 "mobile",
453 "android",
454 "geckoview",
455 "libs",
456 "geckoview-javadoc.jar",
458 dst_path = mozpath.join(repo_path, javadoc_path.format(**fmt))
459 mozfile.remove(dst_path)
460 mozfile.extract_zip(src_tar, dst_path)
462 # Commit and push.
463 command_context.run_process(["git", "add", "--all"], append_env=env, pass_thru=True)
464 if (
465 command_context.run_process(
466 ["git", "diff", "--cached", "--quiet"],
467 append_env=env,
468 pass_thru=True,
469 ensure_exit_code=False,
471 != 0
473 # We have something to commit.
474 command_context.run_process(
475 ["git", "commit", "--message", upload_message.format(**fmt)],
476 append_env=env,
477 pass_thru=True,
479 command_context.run_process(
480 ["git", "push", "origin", branch], append_env=env, pass_thru=True
483 mozfile.remove(repo_path)
484 if secret:
485 mozfile.remove(keyfile)
486 return 0
489 @Command(
490 "gradle",
491 category="devenv",
492 description="Run gradle.",
493 conditions=[conditions.is_android],
495 @CommandArgument(
496 "-v",
497 "--verbose",
498 action="store_true",
499 help="Verbose output for what commands the build is running.",
501 @CommandArgument("args", nargs=argparse.REMAINDER)
502 def gradle(command_context, args, verbose=False):
503 if not verbose:
504 # Avoid logging the command
505 command_context.log_manager.terminal_handler.setLevel(logging.CRITICAL)
507 # In automation, JAVA_HOME is set via mozconfig, which needs
508 # to be specially handled in each mach command. This turns
509 # $JAVA_HOME/bin/java into $JAVA_HOME.
510 java_home = os.path.dirname(os.path.dirname(command_context.substs["JAVA"]))
512 gradle_flags = command_context.substs.get("GRADLE_FLAGS", "") or os.environ.get(
513 "GRADLE_FLAGS", ""
515 gradle_flags = shell_split(gradle_flags)
517 # We force the Gradle JVM to run with the UTF-8 encoding, since we
518 # filter strings.xml, which is really UTF-8; the ellipsis character is
519 # replaced with ??? in some encodings (including ASCII). It's not yet
520 # possible to filter with encodings in Gradle
521 # (https://github.com/gradle/gradle/pull/520) and it's challenging to
522 # do our filtering with Gradle's Ant support. Moreover, all of the
523 # Android tools expect UTF-8: see
524 # http://tools.android.com/knownissues/encoding. See
525 # http://stackoverflow.com/a/21267635 for discussion of this approach.
527 # It's not even enough to set the encoding just for Gradle; it
528 # needs to be for JVMs spawned by Gradle as well. This
529 # happens during the maven deployment generating the GeckoView
530 # documents; this works around "error: unmappable character
531 # for encoding ASCII" in exoplayer2. See
532 # https://discuss.gradle.org/t/unmappable-character-for-encoding-ascii-when-building-a-utf-8-project/10692/11 # NOQA: E501
533 # and especially https://stackoverflow.com/a/21755671.
535 if command_context.substs.get("MOZ_AUTOMATION"):
536 gradle_flags += ["--console=plain"]
538 env = os.environ.copy()
539 env.update(
541 "GRADLE_OPTS": "-Dfile.encoding=utf-8",
542 "JAVA_HOME": java_home,
543 "JAVA_TOOL_OPTIONS": "-Dfile.encoding=utf-8",
544 # Let Gradle get the right Python path on Windows
545 "GRADLE_MACH_PYTHON": sys.executable,
548 # Set ANDROID_SDK_ROOT if --with-android-sdk was set.
549 # See https://bugzilla.mozilla.org/show_bug.cgi?id=1576471
550 android_sdk_root = command_context.substs.get("ANDROID_SDK_ROOT", "")
551 if android_sdk_root:
552 env["ANDROID_SDK_ROOT"] = android_sdk_root
554 return command_context.run_process(
555 [command_context.substs["GRADLE"]] + gradle_flags + args,
556 explicit_env=env,
557 pass_thru=True, # Allow user to run gradle interactively.
558 ensure_exit_code=False, # Don't throw on non-zero exit code.
559 cwd=mozpath.join(command_context.topsrcdir),
563 @Command("gradle-install", category="devenv", conditions=[REMOVED])
564 def gradle_install_REMOVED(command_context):
565 pass
568 @Command(
569 "android-emulator",
570 category="devenv",
571 conditions=[],
572 description="Run the Android emulator with an AVD from test automation. "
573 "Environment variable MOZ_EMULATOR_COMMAND_ARGS, if present, will "
574 "over-ride the command line arguments used to launch the emulator.",
576 @CommandArgument(
577 "--version",
578 metavar="VERSION",
579 choices=["arm", "arm64", "x86_64"],
580 help="Specify which AVD to run in emulator. "
581 'One of "arm" (Android supporting armv7 binaries), '
582 '"arm64" (for Apple Silicon), or '
583 '"x86_64" (Android supporting x86 or x86_64 binaries, '
584 "recommended for most applications). "
585 "By default, the value will match the current build environment.",
587 @CommandArgument("--wait", action="store_true", help="Wait for emulator to be closed.")
588 @CommandArgument("--gpu", help="Over-ride the emulator -gpu argument.")
589 @CommandArgument(
590 "--verbose", action="store_true", help="Log informative status messages."
592 def emulator(
593 command_context,
594 version,
595 wait=False,
596 gpu=None,
597 verbose=False,
600 Run the Android emulator with one of the AVDs used in the Mozilla
601 automated test environment. If necessary, the AVD is fetched from
602 the taskcluster server and installed.
604 from mozrunner.devices.android_device import AndroidEmulator
606 emulator = AndroidEmulator(
607 version,
608 verbose,
609 substs=command_context.substs,
610 device_serial="emulator-5554",
612 if emulator.is_running():
613 # It is possible to run multiple emulators simultaneously, but:
614 # - if more than one emulator is using the same avd, errors may
615 # occur due to locked resources;
616 # - additional parameters must be specified when running tests,
617 # to select a specific device.
618 # To avoid these complications, allow just one emulator at a time.
619 command_context.log(
620 logging.ERROR,
621 "emulator",
623 "An Android emulator is already running.\n"
624 "Close the existing emulator and re-run this command.",
626 return 1
628 if not emulator.check_avd():
629 command_context.log(
630 logging.WARN,
631 "emulator",
633 "AVD not found. Please run |mach bootstrap|.",
635 return 2
637 if not emulator.is_available():
638 command_context.log(
639 logging.WARN,
640 "emulator",
642 "Emulator binary not found.\n"
643 "Install the Android SDK and make sure 'emulator' is in your PATH.",
645 return 2
647 command_context.log(
648 logging.INFO,
649 "emulator",
651 "Starting Android emulator running %s..." % emulator.get_avd_description(),
653 emulator.start(gpu)
654 if emulator.wait_for_start():
655 command_context.log(
656 logging.INFO, "emulator", {}, "Android emulator is running."
658 else:
659 # This is unusual but the emulator may still function.
660 command_context.log(
661 logging.WARN,
662 "emulator",
664 "Unable to verify that emulator is running.",
667 if conditions.is_android(command_context):
668 command_context.log(
669 logging.INFO,
670 "emulator",
672 "Use 'mach install' to install or update Firefox on your emulator.",
674 else:
675 command_context.log(
676 logging.WARN,
677 "emulator",
679 "No Firefox for Android build detected.\n"
680 "Switch to a Firefox for Android build context or use 'mach bootstrap'\n"
681 "to setup an Android build environment.",
684 if wait:
685 command_context.log(
686 logging.INFO, "emulator", {}, "Waiting for Android emulator to close..."
688 rc = emulator.wait()
689 if rc is not None:
690 command_context.log(
691 logging.INFO,
692 "emulator",
694 "Android emulator completed with return code %d." % rc,
696 else:
697 command_context.log(
698 logging.WARN,
699 "emulator",
701 "Unable to retrieve Android emulator return code.",
703 return 0
706 @SubCommand(
707 "android",
708 "uplift",
709 description="Uplift patch to https://github.com/mozilla-mobile/firefox-android.",
711 @CommandArgument(
712 "--revset",
713 "-r",
714 default=None,
715 help="Revision or revisions to uplift. Supported values are the same as what your "
716 "VCS provides. Defaults to the current HEAD revision.",
718 @CommandArgument(
719 "firefox_android_clone_dir",
720 help="The directory where your local clone of "
721 "https://github.com/mozilla-mobile/firefox-android repo is.",
723 def uplift(
724 command_context,
725 revset,
726 firefox_android_clone_dir,
728 revset = _get_default_revset_if_needed(command_context, revset)
729 major_version = _get_major_version(command_context, revset)
730 uplift_version = major_version - 1
731 bug_number = _get_bug_number(command_context, revset)
732 new_branch_name = (
733 f"{uplift_version}-bug{bug_number}" if bug_number else f"{uplift_version}-nobug"
735 command_context.log(
736 logging.INFO,
737 "uplift",
739 "new_branch_name": new_branch_name,
740 "firefox_android_clone_dir": firefox_android_clone_dir,
742 "Creating branch {new_branch_name} in {firefox_android_clone_dir}...",
745 try:
746 _checkout_new_branch_updated_to_the_latest_remote(
747 command_context, firefox_android_clone_dir, uplift_version, new_branch_name
749 _export_and_apply_revset(command_context, revset, firefox_android_clone_dir)
750 except subprocess.CalledProcessError:
751 return 1
753 command_context.log(
754 logging.INFO,
755 "uplift",
756 {"revset": revset, "firefox_android_clone_dir": firefox_android_clone_dir},
757 "Revision(s) {revset} now applied to {firefox_android_clone_dir}. Please go to "
758 "this directory, inspect the commit(s), and push.",
760 return 0
763 def _get_default_revset_if_needed(command_context, revset):
764 if revset is not None:
765 return revset
766 if conditions.is_hg(command_context):
767 return "."
768 if conditions.is_git(command_context):
769 return "HEAD"
770 raise NotImplementedError()
773 def _get_major_version(command_context, revset):
774 milestone_txt = _get_milestone_txt(command_context, revset)
775 version = _extract_version_from_milestone_txt(milestone_txt)
776 return _extract_major_version(version)
779 def _get_bug_number(command_context, revision):
780 revision_message = _extract_revision_message(command_context, revision)
781 return _extract_bug_number(revision_message)
784 def _get_milestone_txt(command_context, revset):
785 if conditions.is_hg(command_context):
786 args = [
787 str(which("hg")),
788 "cat",
789 "-r",
790 f"first({revset})",
791 "config/milestone.txt",
793 elif conditions.is_git(command_context):
794 revision = revset.split("..")[-1]
795 args = [
796 str(which("git")),
797 "show",
798 f"{revision}:config/milestone.txt",
800 else:
801 raise NotImplementedError()
803 return subprocess.check_output(args, text=True)
806 def _extract_version_from_milestone_txt(milestone_txt):
807 return milestone_txt.splitlines()[-1]
810 def _extract_major_version(version):
811 return int(version.split(".")[0])
814 def _extract_revision_message(command_context, revision):
815 if conditions.is_hg(command_context):
816 args = [
817 str(which("hg")),
818 "log",
819 "--rev",
820 f"first({revision})",
821 "--template",
822 "{desc}",
824 elif conditions.is_git(command_context):
825 args = [
826 str(which("git")),
827 "log",
828 "--format=%s",
829 "-n",
830 "1",
831 revision,
833 else:
834 raise NotImplementedError()
836 return subprocess.check_output(args, text=True)
839 # Source: https://hg.mozilla.org/hgcustom/version-control-tools/file/cef43d3d676e9f9e9668a50a5d90c012e4025e5b/pylib/mozautomation/mozautomation/commitparser.py#l12
840 _BUG_TEMPLATE = re.compile(
841 r"""# bug followed by any sequence of numbers, or
842 # a standalone sequence of numbers
845 bug |
846 b= |
847 # a sequence of 5+ numbers preceded by whitespace
848 (?=\b\#?\d{5,}) |
849 # numbers at the very beginning
850 ^(?=\d)
852 (?:\s*\#?)(\d+)(?=\b)
853 )""",
854 re.I | re.X,
858 def _extract_bug_number(revision_message):
859 try:
860 return _BUG_TEMPLATE.match(revision_message).group(2)
861 except AttributeError:
862 return ""
865 _FIREFOX_ANDROID_URL = "https://github.com/mozilla-mobile/firefox-android"
868 def _checkout_new_branch_updated_to_the_latest_remote(
869 command_context, firefox_android_clone_dir, uplift_version, new_branch_name
871 args = [
872 str(which("git")),
873 "fetch",
874 _FIREFOX_ANDROID_URL,
875 f"+releases_v{uplift_version}:{new_branch_name}",
878 try:
879 subprocess.check_call(args, cwd=firefox_android_clone_dir)
880 except subprocess.CalledProcessError:
881 command_context.log(
882 logging.CRITICAL,
883 "uplift",
885 "firefox_android_clone_dir": firefox_android_clone_dir,
886 "new_branch_name": new_branch_name,
888 "Could not fetch branch {new_branch_name}. This may be a network issue. If "
889 "not, please go to {firefox_android_clone_dir}, inspect the branch and "
890 "delete it (with `git branch -D {new_branch_name}`) if you don't have any "
891 "use for it anymore",
893 raise
895 args = [
896 str(which("git")),
897 "checkout",
898 new_branch_name,
900 subprocess.check_call(args, cwd=firefox_android_clone_dir)
903 _MERCURIAL_REVISION_TO_GIT_COMMIT_TEMPLATE = """
904 From 1234567890abcdef1234567890abcdef12345678 Sat Jan 1 00:00:00 2000
905 From: {user}
906 Date: {date|rfc822date}
907 Subject: {desc}
912 def _export_and_apply_revset(command_context, revset, firefox_android_clone_dir):
913 export_command, import_command = _get_export_import_commands(
914 command_context, revset
917 export_process = subprocess.Popen(export_command, stdout=subprocess.PIPE)
918 try:
919 subprocess.check_call(
920 import_command, stdin=export_process.stdout, cwd=firefox_android_clone_dir
922 except subprocess.CalledProcessError:
923 command_context.log(
924 logging.CRITICAL,
925 "uplift",
926 {"firefox_android_clone_dir": firefox_android_clone_dir},
927 "Could not run `git am`. Please go to {firefox_android_clone_dir} and fix "
928 "the conflicts. Then run `git am --continue`.",
930 raise
931 export_process.wait()
934 def _get_export_import_commands(command_context, revset):
935 if conditions.is_hg(command_context):
936 export_command = [
937 str(which("hg")),
938 "log",
939 "--rev",
940 revset,
941 "--patch",
942 "--template",
943 _MERCURIAL_REVISION_TO_GIT_COMMIT_TEMPLATE,
944 "--include",
945 "mobile/android",
947 import_command = [str(which("git")), "am", "-p3"]
948 elif conditions.is_git(command_context):
949 export_command = [
950 str(which("git")),
951 "format-patch",
952 "--relative=mobile/android",
953 "--stdout",
955 # From the git man page:
956 # > If you want to format only <commit> itself, you can do this with
957 # > git format-patch -1 <commit>."
959 # https://git-scm.com/docs/git-format-patch#_description
960 if _is_single_revision(command_context, revset):
961 export_command.append("-1")
963 export_command.extend(
965 revset,
966 "--",
969 import_command = [str(which("git")), "am"]
970 else:
971 raise NotImplementedError()
973 return export_command, import_command
976 def _is_single_revision(command_context, revset):
977 if conditions.is_git(command_context):
978 command = [
979 str(which("git")),
980 "show",
981 "--no-patch",
982 "--format='%H'",
983 revset,
984 "--",
986 else:
987 raise NotImplementedError()
989 revisions = subprocess.check_output(command, text=True)
990 return len(revisions.splitlines()) == 1