no bug - Import translations from android-l10n r=release a=l10n CLOSED TREE
[gecko.git] / mobile / android / mach_commands.py
blobb226aa3be22ed5582c6e9348d50363dce8a7e228
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 with tarfile.open(
196 os.path.join(gradle_folder, "target.maven.tar.xz"), "w|xz"
197 ) as tar:
198 for abs_path in get_maven_archive_paths(maven_folder):
199 tar.add(
200 abs_path,
201 arcname=os.path.join(
202 "geckoview", os.path.relpath(abs_path, maven_folder)
207 @SubCommand(
208 "android",
209 "archive-geckoview",
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):
215 ret = gradle(
216 command_context,
217 command_context.substs["GRADLE_ANDROID_ARCHIVE_GECKOVIEW_TASKS"] + args,
218 verbose=True,
221 if ret != 0:
222 return ret
223 if "MOZ_AUTOMATION" in os.environ:
224 create_maven_archive(command_context.topobjdir)
226 return 0
229 @SubCommand("android", "build-geckoview_example", """Build geckoview_example """)
230 @CommandArgument("args", nargs=argparse.REMAINDER)
231 def android_build_geckoview_example(command_context, args):
232 gradle(
233 command_context,
234 command_context.substs["GRADLE_ANDROID_BUILD_GECKOVIEW_EXAMPLE_TASKS"] + args,
235 verbose=True,
238 print(
239 "Execute `mach android install-geckoview_example` "
240 "to push the geckoview_example and test APKs to a device."
243 return 0
246 @SubCommand("android", "compile-all", """Build all source files""")
247 @CommandArgument("args", nargs=argparse.REMAINDER)
248 def android_compile_all(command_context, args):
249 gradle(
250 command_context,
251 command_context.substs["GRADLE_ANDROID_COMPILE_ALL_TASKS"] + args,
252 verbose=True,
255 return 0
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):
273 gradle(
274 command_context,
275 command_context.substs["GRADLE_ANDROID_INSTALL_GECKOVIEW_EXAMPLE_TASKS"] + args,
276 verbose=True,
279 print(
280 "Execute `mach android build-geckoview_example` "
281 "to just build the geckoview_example and test APKs."
284 return 0
287 @SubCommand(
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):
292 gradle(
293 command_context,
294 command_context.substs["GRADLE_ANDROID_INSTALL_GECKOVIEW_TEST_RUNNER_TASKS"]
295 + args,
296 verbose=True,
298 return 0
301 @SubCommand(
302 "android",
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):
308 install_app_bundle(
309 command_context,
310 command_context.substs["GRADLE_ANDROID_GECKOVIEW_TEST_RUNNER_BUNDLE"],
312 return 0
315 @SubCommand(
316 "android",
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):
322 install_app_bundle(
323 command_context,
324 command_context.substs["GRADLE_ANDROID_GECKOVIEW_EXAMPLE_BUNDLE"],
326 return 0
329 @SubCommand("android", "install-geckoview-test", """Install geckoview.test """)
330 @CommandArgument("args", nargs=argparse.REMAINDER)
331 def android_install_geckoview_test(command_context, args):
332 gradle(
333 command_context,
334 command_context.substs["GRADLE_ANDROID_INSTALL_GECKOVIEW_TEST_TASKS"] + args,
335 verbose=True,
337 return 0
340 @SubCommand(
341 "android",
342 "geckoview-docs",
343 """Create GeckoView javadoc and optionally upload to Github""",
345 @CommandArgument("--archive", action="store_true", help="Generate a javadoc archive.")
346 @CommandArgument(
347 "--upload",
348 metavar="USER/REPO",
349 help="Upload geckoview documentation to Github, using the specified USER/REPO.",
351 @CommandArgument(
352 "--upload-branch",
353 metavar="BRANCH[/PATH]",
354 default="gh-pages",
355 help="Use the specified branch/path for documentation commits.",
357 @CommandArgument(
358 "--javadoc-path",
359 metavar="/PATH",
360 default="javadoc",
361 help="Use the specified path for javadoc commits.",
363 @CommandArgument(
364 "--upload-message",
365 metavar="MSG",
366 default="GeckoView docs upload",
367 help="Use the specified message for commits.",
369 def android_geckoview_docs(
370 command_context,
371 archive,
372 upload,
373 upload_branch,
374 javadoc_path,
375 upload_message,
377 tasks = (
378 command_context.substs["GRADLE_ANDROID_GECKOVIEW_DOCS_ARCHIVE_TASKS"]
379 if archive or upload
380 else command_context.substs["GRADLE_ANDROID_GECKOVIEW_DOCS_TASKS"]
383 ret = gradle(command_context, tasks, verbose=True)
384 if ret or not upload:
385 return ret
387 # Upload to Github.
388 fmt = {
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"),
393 env = {}
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)
400 if secret:
401 # Set up a private key from the secrets store if applicable.
402 import requests
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
417 # Clone remote repo.
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(
423 "git",
424 "clone",
425 "--branch",
426 upload_branch,
427 "--depth",
428 "1",
429 repo_url,
430 repo_path,
432 append_env=env,
433 pass_thru=True,
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.
441 import mozfile
443 # Extract new javadoc to specified directory inside repo.
444 src_tar = mozpath.join(
445 command_context.topobjdir,
446 "gradle",
447 "build",
448 "mobile",
449 "android",
450 "geckoview",
451 "libs",
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)
458 # Commit and push.
459 command_context.run_process(["git", "add", "--all"], append_env=env, pass_thru=True)
460 if (
461 command_context.run_process(
462 ["git", "diff", "--cached", "--quiet"],
463 append_env=env,
464 pass_thru=True,
465 ensure_exit_code=False,
467 != 0
469 # We have something to commit.
470 command_context.run_process(
471 ["git", "commit", "--message", upload_message.format(**fmt)],
472 append_env=env,
473 pass_thru=True,
475 command_context.run_process(
476 ["git", "push", "origin", branch], append_env=env, pass_thru=True
479 mozfile.remove(repo_path)
480 if secret:
481 mozfile.remove(keyfile)
482 return 0
485 @Command(
486 "gradle",
487 category="devenv",
488 description="Run gradle.",
489 conditions=[conditions.is_android],
491 @CommandArgument(
492 "-v",
493 "--verbose",
494 action="store_true",
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):
499 if not verbose:
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(
509 "GRADLE_FLAGS", ""
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()
535 env.update(
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", "")
547 if 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,
552 explicit_env=env,
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):
561 pass
564 @Command(
565 "android-emulator",
566 category="devenv",
567 conditions=[],
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.",
572 @CommandArgument(
573 "--version",
574 metavar="VERSION",
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.")
585 @CommandArgument(
586 "--verbose", action="store_true", help="Log informative status messages."
588 def emulator(
589 command_context,
590 version,
591 wait=False,
592 gpu=None,
593 verbose=False,
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(
603 version,
604 verbose,
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.
615 command_context.log(
616 logging.ERROR,
617 "emulator",
619 "An Android emulator is already running.\n"
620 "Close the existing emulator and re-run this command.",
622 return 1
624 if not emulator.check_avd():
625 command_context.log(
626 logging.WARN,
627 "emulator",
629 "AVD not found. Please run |mach bootstrap|.",
631 return 2
633 if not emulator.is_available():
634 command_context.log(
635 logging.WARN,
636 "emulator",
638 "Emulator binary not found.\n"
639 "Install the Android SDK and make sure 'emulator' is in your PATH.",
641 return 2
643 command_context.log(
644 logging.INFO,
645 "emulator",
647 "Starting Android emulator running %s..." % emulator.get_avd_description(),
649 emulator.start(gpu)
650 if emulator.wait_for_start():
651 command_context.log(
652 logging.INFO, "emulator", {}, "Android emulator is running."
654 else:
655 # This is unusual but the emulator may still function.
656 command_context.log(
657 logging.WARN,
658 "emulator",
660 "Unable to verify that emulator is running.",
663 if conditions.is_android(command_context):
664 command_context.log(
665 logging.INFO,
666 "emulator",
668 "Use 'mach install' to install or update Firefox on your emulator.",
670 else:
671 command_context.log(
672 logging.WARN,
673 "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.",
680 if wait:
681 command_context.log(
682 logging.INFO, "emulator", {}, "Waiting for Android emulator to close..."
684 rc = emulator.wait()
685 if rc is not None:
686 command_context.log(
687 logging.INFO,
688 "emulator",
690 "Android emulator completed with return code %d." % rc,
692 else:
693 command_context.log(
694 logging.WARN,
695 "emulator",
697 "Unable to retrieve Android emulator return code.",
699 return 0
702 @SubCommand(
703 "android",
704 "uplift",
705 description="Uplift patch to https://github.com/mozilla-mobile/firefox-android.",
707 @CommandArgument(
708 "--revset",
709 "-r",
710 default=None,
711 help="Revision or revisions to uplift. Supported values are the same as what your "
712 "VCS provides. Defaults to the current HEAD revision.",
714 @CommandArgument(
715 "firefox_android_clone_dir",
716 help="The directory where your local clone of "
717 "https://github.com/mozilla-mobile/firefox-android repo is.",
719 def uplift(
720 command_context,
721 revset,
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)
728 new_branch_name = (
729 f"{uplift_version}-bug{bug_number}" if bug_number else f"{uplift_version}-nobug"
731 command_context.log(
732 logging.INFO,
733 "uplift",
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}...",
741 try:
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:
747 return 1
749 command_context.log(
750 logging.INFO,
751 "uplift",
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.",
756 return 0
759 def _get_default_revset_if_needed(command_context, revset):
760 if revset is not None:
761 return revset
762 if conditions.is_hg(command_context):
763 return "."
764 if conditions.is_git(command_context):
765 return "HEAD"
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):
782 args = [
783 str(which("hg")),
784 "cat",
785 "-r",
786 f"first({revset})",
787 "config/milestone.txt",
789 elif conditions.is_git(command_context):
790 revision = revset.split("..")[-1]
791 args = [
792 str(which("git")),
793 "show",
794 f"{revision}:config/milestone.txt",
796 else:
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):
812 args = [
813 str(which("hg")),
814 "log",
815 "--rev",
816 f"first({revision})",
817 "--template",
818 "{desc}",
820 elif conditions.is_git(command_context):
821 args = [
822 str(which("git")),
823 "log",
824 "--format=%s",
825 "-n",
826 "1",
827 revision,
829 else:
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
841 bug |
842 b= |
843 # a sequence of 5+ numbers preceded by whitespace
844 (?=\b\#?\d{5,}) |
845 # numbers at the very beginning
846 ^(?=\d)
848 (?:\s*\#?)(\d+)(?=\b)
849 )""",
850 re.I | re.X,
854 def _extract_bug_number(revision_message):
855 try:
856 return _BUG_TEMPLATE.match(revision_message).group(2)
857 except AttributeError:
858 return ""
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
867 args = [
868 str(which("git")),
869 "fetch",
870 _FIREFOX_ANDROID_URL,
871 f"+releases_v{uplift_version}:{new_branch_name}",
874 try:
875 subprocess.check_call(args, cwd=firefox_android_clone_dir)
876 except subprocess.CalledProcessError:
877 command_context.log(
878 logging.CRITICAL,
879 "uplift",
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",
889 raise
891 args = [
892 str(which("git")),
893 "checkout",
894 new_branch_name,
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
901 From: {user}
902 Date: {date|rfc822date}
903 Subject: {desc}
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)
914 try:
915 subprocess.check_call(
916 import_command, stdin=export_process.stdout, cwd=firefox_android_clone_dir
918 except subprocess.CalledProcessError:
919 command_context.log(
920 logging.CRITICAL,
921 "uplift",
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`.",
926 raise
927 export_process.wait()
930 def _get_export_import_commands(command_context, revset):
931 if conditions.is_hg(command_context):
932 export_command = [
933 str(which("hg")),
934 "log",
935 "--rev",
936 revset,
937 "--patch",
938 "--template",
939 _MERCURIAL_REVISION_TO_GIT_COMMIT_TEMPLATE,
940 "--include",
941 "mobile/android",
943 import_command = [str(which("git")), "am", "-p3"]
944 elif conditions.is_git(command_context):
945 export_command = [
946 str(which("git")),
947 "format-patch",
948 "--relative=mobile/android",
949 "--stdout",
950 revset,
951 "--",
953 import_command = [str(which("git")), "am"]
954 else:
955 raise NotImplementedError()
957 return export_command, import_command