summaryrefslogtreecommitdiffstats
path: root/mobile/android/mach_commands.py
diff options
context:
space:
mode:
Diffstat (limited to 'mobile/android/mach_commands.py')
-rw-r--r--mobile/android/mach_commands.py696
1 files changed, 696 insertions, 0 deletions
diff --git a/mobile/android/mach_commands.py b/mobile/android/mach_commands.py
new file mode 100644
index 0000000000..68271bd0c0
--- /dev/null
+++ b/mobile/android/mach_commands.py
@@ -0,0 +1,696 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+import argparse
+import logging
+import os
+import sys
+import tarfile
+
+import mozpack.path as mozpath
+from mach.decorators import Command, CommandArgument, SubCommand
+from mozbuild.base import MachCommandConditions as conditions
+from mozbuild.shellutil import split as shell_split
+
+# Mach's conditions facility doesn't support subcommands. Print a
+# deprecation message ourselves instead.
+LINT_DEPRECATION_MESSAGE = """
+Android lints are now integrated with mozlint. Instead of
+`mach android {api-lint,checkstyle,lint,test}`, run
+`mach lint --linter android-{api-lint,checkstyle,lint,test}`.
+Or run `mach lint`.
+"""
+
+
+# NOTE python/mach/mach/commands/commandinfo.py references this function
+# by name. If this function is renamed or removed, that file should
+# be updated accordingly as well.
+def REMOVED(cls):
+ """Command no longer exists! Use the Gradle configuration rooted in the top source directory
+ instead.
+
+ 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
+ """
+ return False
+
+
+@Command(
+ "android",
+ category="devenv",
+ description="Run Android-specific commands.",
+ conditions=[conditions.is_android],
+)
+def android(command_context):
+ pass
+
+
+@SubCommand(
+ "android",
+ "assemble-app",
+ """Assemble Firefox for Android.
+ See http://firefox-source-docs.mozilla.org/build/buildsystem/toolchains.html#firefox-for-android-with-gradle""", # NOQA: E501
+)
+@CommandArgument("args", nargs=argparse.REMAINDER)
+def android_assemble_app(command_context, args):
+ ret = gradle(
+ command_context,
+ command_context.substs["GRADLE_ANDROID_APP_TASKS"] + ["-x", "lint"] + args,
+ verbose=True,
+ )
+
+ return ret
+
+
+@SubCommand(
+ "android",
+ "generate-sdk-bindings",
+ """Generate SDK bindings used when building GeckoView.""",
+)
+@CommandArgument(
+ "inputs",
+ nargs="+",
+ help="config files, like [/path/to/ClassName-classes.txt]+",
+)
+@CommandArgument("args", nargs=argparse.REMAINDER)
+def android_generate_sdk_bindings(command_context, inputs, args):
+ import itertools
+
+ def stem(input):
+ # Turn "/path/to/ClassName-classes.txt" into "ClassName".
+ return os.path.basename(input).rsplit("-classes.txt", 1)[0]
+
+ bindings_inputs = list(itertools.chain(*((input, stem(input)) for input in inputs)))
+ bindings_args = "-Pgenerate_sdk_bindings_args={}".format(";".join(bindings_inputs))
+
+ ret = gradle(
+ command_context,
+ command_context.substs["GRADLE_ANDROID_GENERATE_SDK_BINDINGS_TASKS"]
+ + [bindings_args]
+ + args,
+ verbose=True,
+ )
+
+ return ret
+
+
+@SubCommand(
+ "android",
+ "generate-generated-jni-wrappers",
+ """Generate GeckoView JNI wrappers used when building GeckoView.""",
+)
+@CommandArgument("args", nargs=argparse.REMAINDER)
+def android_generate_generated_jni_wrappers(command_context, args):
+ ret = gradle(
+ command_context,
+ command_context.substs["GRADLE_ANDROID_GENERATE_GENERATED_JNI_WRAPPERS_TASKS"]
+ + args,
+ verbose=True,
+ )
+
+ return ret
+
+
+@SubCommand(
+ "android",
+ "api-lint",
+ """Run Android api-lint.
+REMOVED/DEPRECATED: Use 'mach lint --linter android-api-lint'.""",
+)
+def android_apilint_REMOVED(command_context):
+ print(LINT_DEPRECATION_MESSAGE)
+ return 1
+
+
+@SubCommand(
+ "android",
+ "test",
+ """Run Android test.
+REMOVED/DEPRECATED: Use 'mach lint --linter android-test'.""",
+)
+def android_test_REMOVED(command_context):
+ print(LINT_DEPRECATION_MESSAGE)
+ return 1
+
+
+@SubCommand(
+ "android",
+ "lint",
+ """Run Android lint.
+REMOVED/DEPRECATED: Use 'mach lint --linter android-lint'.""",
+)
+def android_lint_REMOVED(command_context):
+ print(LINT_DEPRECATION_MESSAGE)
+ return 1
+
+
+@SubCommand(
+ "android",
+ "checkstyle",
+ """Run Android checkstyle.
+REMOVED/DEPRECATED: Use 'mach lint --linter android-checkstyle'.""",
+)
+def android_checkstyle_REMOVED(command_context):
+ print(LINT_DEPRECATION_MESSAGE)
+ return 1
+
+
+@SubCommand(
+ "android",
+ "gradle-dependencies",
+ """Collect Android Gradle dependencies.
+ See http://firefox-source-docs.mozilla.org/build/buildsystem/toolchains.html#firefox-for-android-with-gradle""", # NOQA: E501
+)
+@CommandArgument("args", nargs=argparse.REMAINDER)
+def android_gradle_dependencies(command_context, args):
+ # We don't want to gate producing dependency archives on clean
+ # lint or checkstyle, particularly because toolchain versions
+ # can change the outputs for those processes.
+ gradle(
+ command_context,
+ command_context.substs["GRADLE_ANDROID_DEPENDENCIES_TASKS"]
+ + ["--continue"]
+ + args,
+ verbose=True,
+ )
+
+ return 0
+
+
+def get_maven_archive_paths(maven_folder):
+ for subdir, _, files in os.walk(maven_folder):
+ if "-SNAPSHOT" in subdir:
+ continue
+ for file in files:
+ yield os.path.join(subdir, file)
+
+
+def create_maven_archive(topobjdir):
+ gradle_folder = os.path.join(topobjdir, "gradle")
+ maven_folder = os.path.join(gradle_folder, "maven")
+
+ with tarfile.open(
+ os.path.join(gradle_folder, "target.maven.tar.xz"), "w|xz"
+ ) as tar:
+ for abs_path in get_maven_archive_paths(maven_folder):
+ tar.add(
+ abs_path,
+ arcname=os.path.join(
+ "geckoview", os.path.relpath(abs_path, maven_folder)
+ ),
+ )
+
+
+@SubCommand(
+ "android",
+ "archive-geckoview",
+ """Create GeckoView archives.
+ See http://firefox-source-docs.mozilla.org/build/buildsystem/toolchains.html#firefox-for-android-with-gradle""", # NOQA: E501
+)
+@CommandArgument("args", nargs=argparse.REMAINDER)
+def android_archive_geckoview(command_context, args):
+ ret = gradle(
+ command_context,
+ command_context.substs["GRADLE_ANDROID_ARCHIVE_GECKOVIEW_TASKS"] + args,
+ verbose=True,
+ )
+
+ if ret != 0:
+ return ret
+ if "MOZ_AUTOMATION" in os.environ:
+ create_maven_archive(command_context.topobjdir)
+
+ return 0
+
+
+@SubCommand("android", "build-geckoview_example", """Build geckoview_example """)
+@CommandArgument("args", nargs=argparse.REMAINDER)
+def android_build_geckoview_example(command_context, args):
+ gradle(
+ command_context,
+ command_context.substs["GRADLE_ANDROID_BUILD_GECKOVIEW_EXAMPLE_TASKS"] + args,
+ verbose=True,
+ )
+
+ print(
+ "Execute `mach android install-geckoview_example` "
+ "to push the geckoview_example and test APKs to a device."
+ )
+
+ return 0
+
+
+@SubCommand("android", "compile-all", """Build all source files""")
+@CommandArgument("args", nargs=argparse.REMAINDER)
+def android_compile_all(command_context, args):
+ gradle(
+ command_context,
+ command_context.substs["GRADLE_ANDROID_COMPILE_ALL_TASKS"] + args,
+ verbose=True,
+ )
+
+ return 0
+
+
+def install_app_bundle(command_context, bundle):
+ from mozdevice import ADBDeviceFactory
+
+ bundletool = mozpath.join(command_context._mach_context.state_dir, "bundletool.jar")
+ device = ADBDeviceFactory(verbose=True)
+ bundle_path = mozpath.join(command_context.topobjdir, bundle)
+ java_home = java_home = os.path.dirname(
+ os.path.dirname(command_context.substs["JAVA"])
+ )
+ device.install_app_bundle(bundletool, bundle_path, java_home, timeout=120)
+
+
+@SubCommand("android", "install-geckoview_example", """Install geckoview_example """)
+@CommandArgument("args", nargs=argparse.REMAINDER)
+def android_install_geckoview_example(command_context, args):
+ gradle(
+ command_context,
+ command_context.substs["GRADLE_ANDROID_INSTALL_GECKOVIEW_EXAMPLE_TASKS"] + args,
+ verbose=True,
+ )
+
+ print(
+ "Execute `mach android build-geckoview_example` "
+ "to just build the geckoview_example and test APKs."
+ )
+
+ return 0
+
+
+@SubCommand(
+ "android", "install-geckoview-test_runner", """Install geckoview.test_runner """
+)
+@CommandArgument("args", nargs=argparse.REMAINDER)
+def android_install_geckoview_test_runner(command_context, args):
+ gradle(
+ command_context,
+ command_context.substs["GRADLE_ANDROID_INSTALL_GECKOVIEW_TEST_RUNNER_TASKS"]
+ + args,
+ verbose=True,
+ )
+ return 0
+
+
+@SubCommand(
+ "android",
+ "install-geckoview-test_runner-aab",
+ """Install geckoview.test_runner with AAB""",
+)
+@CommandArgument("args", nargs=argparse.REMAINDER)
+def android_install_geckoview_test_runner_aab(command_context, args):
+ install_app_bundle(
+ command_context,
+ command_context.substs["GRADLE_ANDROID_GECKOVIEW_TEST_RUNNER_BUNDLE"],
+ )
+ return 0
+
+
+@SubCommand(
+ "android",
+ "install-geckoview_example-aab",
+ """Install geckoview_example with AAB""",
+)
+@CommandArgument("args", nargs=argparse.REMAINDER)
+def android_install_geckoview_example_aab(command_context, args):
+ install_app_bundle(
+ command_context,
+ command_context.substs["GRADLE_ANDROID_GECKOVIEW_EXAMPLE_BUNDLE"],
+ )
+ return 0
+
+
+@SubCommand("android", "install-geckoview-test", """Install geckoview.test """)
+@CommandArgument("args", nargs=argparse.REMAINDER)
+def android_install_geckoview_test(command_context, args):
+ gradle(
+ command_context,
+ command_context.substs["GRADLE_ANDROID_INSTALL_GECKOVIEW_TEST_TASKS"] + args,
+ verbose=True,
+ )
+ return 0
+
+
+@SubCommand(
+ "android",
+ "geckoview-docs",
+ """Create GeckoView javadoc and optionally upload to Github""",
+)
+@CommandArgument("--archive", action="store_true", help="Generate a javadoc archive.")
+@CommandArgument(
+ "--upload",
+ metavar="USER/REPO",
+ help="Upload geckoview documentation to Github, using the specified USER/REPO.",
+)
+@CommandArgument(
+ "--upload-branch",
+ metavar="BRANCH[/PATH]",
+ default="gh-pages",
+ help="Use the specified branch/path for documentation commits.",
+)
+@CommandArgument(
+ "--javadoc-path",
+ metavar="/PATH",
+ default="javadoc",
+ help="Use the specified path for javadoc commits.",
+)
+@CommandArgument(
+ "--upload-message",
+ metavar="MSG",
+ default="GeckoView docs upload",
+ help="Use the specified message for commits.",
+)
+def android_geckoview_docs(
+ command_context,
+ archive,
+ upload,
+ upload_branch,
+ javadoc_path,
+ upload_message,
+):
+ tasks = (
+ command_context.substs["GRADLE_ANDROID_GECKOVIEW_DOCS_ARCHIVE_TASKS"]
+ if archive or upload
+ else command_context.substs["GRADLE_ANDROID_GECKOVIEW_DOCS_TASKS"]
+ )
+
+ ret = gradle(command_context, tasks, verbose=True)
+ if ret or not upload:
+ return ret
+
+ # Upload to Github.
+ fmt = {
+ "level": os.environ.get("MOZ_SCM_LEVEL", "0"),
+ "project": os.environ.get("MH_BRANCH", "unknown"),
+ "revision": os.environ.get("GECKO_HEAD_REV", "tip"),
+ }
+ env = {}
+
+ # In order to push to GitHub from TaskCluster, we store a private key
+ # in the TaskCluster secrets store in the format {"content": "<KEY>"},
+ # and the corresponding public key as a writable deploy key for the
+ # destination repo on GitHub.
+ secret = os.environ.get("GECKOVIEW_DOCS_UPLOAD_SECRET", "").format(**fmt)
+ if secret:
+ # Set up a private key from the secrets store if applicable.
+ import requests
+
+ req = requests.get("http://taskcluster/secrets/v1/secret/" + secret)
+ req.raise_for_status()
+
+ keyfile = mozpath.abspath("gv-docs-upload-key")
+ with open(keyfile, "w") as f:
+ os.chmod(keyfile, 0o600)
+ f.write(req.json()["secret"]["content"])
+
+ # Turn off strict host key checking so ssh does not complain about
+ # unknown github.com host. We're not pushing anything sensitive, so
+ # it's okay to not check GitHub's host keys.
+ env["GIT_SSH_COMMAND"] = 'ssh -i "%s" -o StrictHostKeyChecking=no' % keyfile
+
+ # Clone remote repo.
+ branch = upload_branch.format(**fmt)
+ repo_url = "git@github.com:%s.git" % upload
+ repo_path = mozpath.abspath("gv-docs-repo")
+ command_context.run_process(
+ [
+ "git",
+ "clone",
+ "--branch",
+ upload_branch,
+ "--depth",
+ "1",
+ repo_url,
+ repo_path,
+ ],
+ append_env=env,
+ pass_thru=True,
+ )
+ env["GIT_DIR"] = mozpath.join(repo_path, ".git")
+ env["GIT_WORK_TREE"] = repo_path
+ env["GIT_AUTHOR_NAME"] = env["GIT_COMMITTER_NAME"] = "GeckoView Docs Bot"
+ env["GIT_AUTHOR_EMAIL"] = env["GIT_COMMITTER_EMAIL"] = "nobody@mozilla.com"
+
+ # Copy over user documentation.
+ import mozfile
+
+ # Extract new javadoc to specified directory inside repo.
+ src_tar = mozpath.join(
+ command_context.topobjdir,
+ "gradle",
+ "build",
+ "mobile",
+ "android",
+ "geckoview",
+ "libs",
+ "geckoview-javadoc.jar",
+ )
+ dst_path = mozpath.join(repo_path, javadoc_path.format(**fmt))
+ mozfile.remove(dst_path)
+ mozfile.extract_zip(src_tar, dst_path)
+
+ # Commit and push.
+ command_context.run_process(["git", "add", "--all"], append_env=env, pass_thru=True)
+ if (
+ command_context.run_process(
+ ["git", "diff", "--cached", "--quiet"],
+ append_env=env,
+ pass_thru=True,
+ ensure_exit_code=False,
+ )
+ != 0
+ ):
+ # We have something to commit.
+ command_context.run_process(
+ ["git", "commit", "--message", upload_message.format(**fmt)],
+ append_env=env,
+ pass_thru=True,
+ )
+ command_context.run_process(
+ ["git", "push", "origin", branch], append_env=env, pass_thru=True
+ )
+
+ mozfile.remove(repo_path)
+ if secret:
+ mozfile.remove(keyfile)
+ return 0
+
+
+@Command(
+ "gradle",
+ category="devenv",
+ description="Run gradle.",
+ conditions=[conditions.is_android],
+)
+@CommandArgument(
+ "-v",
+ "--verbose",
+ action="store_true",
+ help="Verbose output for what commands the build is running.",
+)
+@CommandArgument("args", nargs=argparse.REMAINDER)
+def gradle(command_context, args, verbose=False):
+ if not verbose:
+ # Avoid logging the command
+ command_context.log_manager.terminal_handler.setLevel(logging.CRITICAL)
+
+ # In automation, JAVA_HOME is set via mozconfig, which needs
+ # to be specially handled in each mach command. This turns
+ # $JAVA_HOME/bin/java into $JAVA_HOME.
+ java_home = os.path.dirname(os.path.dirname(command_context.substs["JAVA"]))
+
+ gradle_flags = command_context.substs.get("GRADLE_FLAGS", "") or os.environ.get(
+ "GRADLE_FLAGS", ""
+ )
+ gradle_flags = shell_split(gradle_flags)
+
+ # We force the Gradle JVM to run with the UTF-8 encoding, since we
+ # filter strings.xml, which is really UTF-8; the ellipsis character is
+ # replaced with ??? in some encodings (including ASCII). It's not yet
+ # possible to filter with encodings in Gradle
+ # (https://github.com/gradle/gradle/pull/520) and it's challenging to
+ # do our filtering with Gradle's Ant support. Moreover, all of the
+ # Android tools expect UTF-8: see
+ # http://tools.android.com/knownissues/encoding. See
+ # http://stackoverflow.com/a/21267635 for discussion of this approach.
+ #
+ # It's not even enough to set the encoding just for Gradle; it
+ # needs to be for JVMs spawned by Gradle as well. This
+ # happens during the maven deployment generating the GeckoView
+ # documents; this works around "error: unmappable character
+ # for encoding ASCII" in exoplayer2. See
+ # https://discuss.gradle.org/t/unmappable-character-for-encoding-ascii-when-building-a-utf-8-project/10692/11 # NOQA: E501
+ # and especially https://stackoverflow.com/a/21755671.
+
+ if command_context.substs.get("MOZ_AUTOMATION"):
+ gradle_flags += ["--console=plain"]
+
+ env = os.environ.copy()
+ env.update(
+ {
+ "GRADLE_OPTS": "-Dfile.encoding=utf-8",
+ "JAVA_HOME": java_home,
+ "JAVA_TOOL_OPTIONS": "-Dfile.encoding=utf-8",
+ # Let Gradle get the right Python path on Windows
+ "GRADLE_MACH_PYTHON": sys.executable,
+ }
+ )
+ # Set ANDROID_SDK_ROOT if --with-android-sdk was set.
+ # See https://bugzilla.mozilla.org/show_bug.cgi?id=1576471
+ android_sdk_root = command_context.substs.get("ANDROID_SDK_ROOT", "")
+ if android_sdk_root:
+ env["ANDROID_SDK_ROOT"] = android_sdk_root
+
+ return command_context.run_process(
+ [command_context.substs["GRADLE"]] + gradle_flags + args,
+ explicit_env=env,
+ pass_thru=True, # Allow user to run gradle interactively.
+ ensure_exit_code=False, # Don't throw on non-zero exit code.
+ cwd=mozpath.join(command_context.topsrcdir),
+ )
+
+
+@Command("gradle-install", category="devenv", conditions=[REMOVED])
+def gradle_install_REMOVED(command_context):
+ pass
+
+
+@Command(
+ "android-emulator",
+ category="devenv",
+ conditions=[],
+ description="Run the Android emulator with an AVD from test automation. "
+ "Environment variable MOZ_EMULATOR_COMMAND_ARGS, if present, will "
+ "over-ride the command line arguments used to launch the emulator.",
+)
+@CommandArgument(
+ "--version",
+ metavar="VERSION",
+ choices=["arm", "arm64", "x86_64"],
+ help="Specify which AVD to run in emulator. "
+ 'One of "arm" (Android supporting armv7 binaries), '
+ '"arm64" (for Apple Silicon), or '
+ '"x86_64" (Android supporting x86 or x86_64 binaries, '
+ "recommended for most applications). "
+ "By default, the value will match the current build environment.",
+)
+@CommandArgument("--wait", action="store_true", help="Wait for emulator to be closed.")
+@CommandArgument("--gpu", help="Over-ride the emulator -gpu argument.")
+@CommandArgument(
+ "--verbose", action="store_true", help="Log informative status messages."
+)
+def emulator(
+ command_context,
+ version,
+ wait=False,
+ gpu=None,
+ verbose=False,
+):
+ """
+ Run the Android emulator with one of the AVDs used in the Mozilla
+ automated test environment. If necessary, the AVD is fetched from
+ the taskcluster server and installed.
+ """
+ from mozrunner.devices.android_device import AndroidEmulator
+
+ emulator = AndroidEmulator(
+ version,
+ verbose,
+ substs=command_context.substs,
+ device_serial="emulator-5554",
+ )
+ if emulator.is_running():
+ # It is possible to run multiple emulators simultaneously, but:
+ # - if more than one emulator is using the same avd, errors may
+ # occur due to locked resources;
+ # - additional parameters must be specified when running tests,
+ # to select a specific device.
+ # To avoid these complications, allow just one emulator at a time.
+ command_context.log(
+ logging.ERROR,
+ "emulator",
+ {},
+ "An Android emulator is already running.\n"
+ "Close the existing emulator and re-run this command.",
+ )
+ return 1
+
+ if not emulator.check_avd():
+ command_context.log(
+ logging.WARN,
+ "emulator",
+ {},
+ "AVD not found. Please run |mach bootstrap|.",
+ )
+ return 2
+
+ if not emulator.is_available():
+ command_context.log(
+ logging.WARN,
+ "emulator",
+ {},
+ "Emulator binary not found.\n"
+ "Install the Android SDK and make sure 'emulator' is in your PATH.",
+ )
+ return 2
+
+ command_context.log(
+ logging.INFO,
+ "emulator",
+ {},
+ "Starting Android emulator running %s..." % emulator.get_avd_description(),
+ )
+ emulator.start(gpu)
+ if emulator.wait_for_start():
+ command_context.log(
+ logging.INFO, "emulator", {}, "Android emulator is running."
+ )
+ else:
+ # This is unusual but the emulator may still function.
+ command_context.log(
+ logging.WARN,
+ "emulator",
+ {},
+ "Unable to verify that emulator is running.",
+ )
+
+ if conditions.is_android(command_context):
+ command_context.log(
+ logging.INFO,
+ "emulator",
+ {},
+ "Use 'mach install' to install or update Firefox on your emulator.",
+ )
+ else:
+ command_context.log(
+ logging.WARN,
+ "emulator",
+ {},
+ "No Firefox for Android build detected.\n"
+ "Switch to a Firefox for Android build context or use 'mach bootstrap'\n"
+ "to setup an Android build environment.",
+ )
+
+ if wait:
+ command_context.log(
+ logging.INFO, "emulator", {}, "Waiting for Android emulator to close..."
+ )
+ rc = emulator.wait()
+ if rc is not None:
+ command_context.log(
+ logging.INFO,
+ "emulator",
+ {},
+ "Android emulator completed with return code %d." % rc,
+ )
+ else:
+ command_context.log(
+ logging.WARN,
+ "emulator",
+ {},
+ "Unable to retrieve Android emulator return code.",
+ )
+ return 0