summaryrefslogtreecommitdiffstats
path: root/python/mozbuild/mozbuild/mach_commands.py
diff options
context:
space:
mode:
Diffstat (limited to 'python/mozbuild/mozbuild/mach_commands.py')
-rw-r--r--python/mozbuild/mozbuild/mach_commands.py2168
1 files changed, 2168 insertions, 0 deletions
diff --git a/python/mozbuild/mozbuild/mach_commands.py b/python/mozbuild/mozbuild/mach_commands.py
new file mode 100644
index 0000000000..d737712de8
--- /dev/null
+++ b/python/mozbuild/mozbuild/mach_commands.py
@@ -0,0 +1,2168 @@
+# 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/.
+
+from __future__ import absolute_import, print_function, unicode_literals
+
+import argparse
+import itertools
+import json
+import logging
+import operator
+import os
+import re
+import shutil
+import subprocess
+import sys
+import tempfile
+
+import mozpack.path as mozpath
+
+from mach.decorators import (
+ CommandArgument,
+ CommandArgumentGroup,
+ CommandProvider,
+ Command,
+ SettingsProvider,
+ SubCommand,
+)
+
+from mozbuild.base import (
+ BinaryNotFoundException,
+ BuildEnvironmentNotFoundException,
+ MachCommandBase,
+ MachCommandConditions as conditions,
+ MozbuildObject,
+)
+from mozbuild.util import MOZBUILD_METRICS_PATH
+
+here = os.path.abspath(os.path.dirname(__file__))
+
+EXCESSIVE_SWAP_MESSAGE = """
+===================
+PERFORMANCE WARNING
+
+Your machine experienced a lot of swap activity during the build. This is
+possibly a sign that your machine doesn't have enough physical memory or
+not enough available memory to perform the build. It's also possible some
+other system activity during the build is to blame.
+
+If you feel this message is not appropriate for your machine configuration,
+please file a Firefox Build System :: General bug at
+https://bugzilla.mozilla.org/enter_bug.cgi?product=Firefox%20Build%20System&component=General
+and tell us about your machine and build configuration so we can adjust the
+warning heuristic.
+===================
+"""
+
+
+class StoreDebugParamsAndWarnAction(argparse.Action):
+ def __call__(self, parser, namespace, values, option_string=None):
+ sys.stderr.write(
+ "The --debugparams argument is deprecated. Please "
+ + "use --debugger-args instead.\n\n"
+ )
+ setattr(namespace, self.dest, values)
+
+
+@CommandProvider
+class Watch(MachCommandBase):
+ """Interface to watch and re-build the tree."""
+
+ @Command(
+ "watch",
+ category="post-build",
+ description="Watch and re-build the tree.",
+ conditions=[conditions.is_firefox],
+ )
+ @CommandArgument(
+ "-v",
+ "--verbose",
+ action="store_true",
+ help="Verbose output for what commands the watcher is running.",
+ )
+ def watch(self, verbose=False):
+ """Watch and re-build the source tree."""
+
+ if not conditions.is_artifact_build(self):
+ print(
+ "mach watch requires an artifact build. See "
+ "https://developer.mozilla.org/docs/Mozilla/Developer_guide/Build_Instructions/Simple_Firefox_build" # noqa
+ )
+ return 1
+
+ if not self.substs.get("WATCHMAN", None):
+ print(
+ "mach watch requires watchman to be installed. See "
+ "https://developer.mozilla.org/docs/Mozilla/Developer_guide/Build_Instructions/Incremental_builds_with_filesystem_watching" # noqa
+ )
+ return 1
+
+ self.activate_virtualenv()
+ try:
+ self.virtualenv_manager.install_pip_package("pywatchman==1.4.1")
+ except Exception:
+ print(
+ "Could not install pywatchman from pip. See "
+ "https://developer.mozilla.org/docs/Mozilla/Developer_guide/Build_Instructions/Incremental_builds_with_filesystem_watching" # noqa
+ )
+ return 1
+
+ from mozbuild.faster_daemon import Daemon
+
+ daemon = Daemon(self.config_environment)
+
+ try:
+ return daemon.watch()
+ except KeyboardInterrupt:
+ # Suppress ugly stack trace when user hits Ctrl-C.
+ sys.exit(3)
+
+
+@CommandProvider
+class CargoProvider(MachCommandBase):
+ """Invoke cargo in useful ways."""
+
+ @Command("cargo", category="build", description="Invoke cargo in useful ways.")
+ def cargo(self):
+ self._sub_mach(["help", "cargo"])
+ return 1
+
+ @SubCommand(
+ "cargo",
+ "check",
+ description="Run `cargo check` on a given crate. Defaults to gkrust.",
+ )
+ @CommandArgument(
+ "--all-crates",
+ default=None,
+ action="store_true",
+ help="Check all of the crates in the tree.",
+ )
+ @CommandArgument(
+ "crates", default=None, nargs="*", help="The crate name(s) to check."
+ )
+ @CommandArgument(
+ "--jobs",
+ "-j",
+ default="1",
+ nargs="?",
+ metavar="jobs",
+ type=int,
+ help="Run the tests in parallel using multiple processes.",
+ )
+ @CommandArgument("-v", "--verbose", action="store_true", help="Verbose output.")
+ def check(self, all_crates=None, crates=None, jobs=0, verbose=False):
+ # XXX duplication with `mach vendor rust`
+ crates_and_roots = {
+ "gkrust": "toolkit/library/rust",
+ "gkrust-gtest": "toolkit/library/gtest/rust",
+ "js": "js/rust",
+ "mozjs_sys": "js/src",
+ "baldrdash": "js/src/wasm/cranelift",
+ "geckodriver": "testing/geckodriver",
+ }
+
+ if all_crates:
+ crates = crates_and_roots.keys()
+ elif crates is None or crates == []:
+ crates = ["gkrust"]
+
+ for crate in crates:
+ root = crates_and_roots.get(crate, None)
+ if not root:
+ print(
+ "Cannot locate crate %s. Please check your spelling or "
+ "add the crate information to the list." % crate
+ )
+ return 1
+
+ check_targets = [
+ "force-cargo-library-check",
+ "force-cargo-host-library-check",
+ "force-cargo-program-check",
+ "force-cargo-host-program-check",
+ ]
+
+ ret = self._run_make(
+ srcdir=False,
+ directory=root,
+ ensure_exit_code=0,
+ silent=not verbose,
+ print_directory=False,
+ target=check_targets,
+ num_jobs=jobs,
+ )
+ if ret != 0:
+ return ret
+
+ return 0
+
+
+@CommandProvider
+class Doctor(MachCommandBase):
+ """Provide commands for diagnosing common build environment problems"""
+
+ @Command("doctor", category="devenv", description="")
+ @CommandArgument(
+ "--fix",
+ default=None,
+ action="store_true",
+ help="Attempt to fix found problems.",
+ )
+ def doctor(self, fix=None):
+ self.activate_virtualenv()
+ from mozbuild.doctor import Doctor
+
+ doctor = Doctor(self.topsrcdir, self.topobjdir, fix)
+ return doctor.check_all()
+
+
+@CommandProvider(metrics_path=MOZBUILD_METRICS_PATH)
+class Clobber(MachCommandBase):
+ NO_AUTO_LOG = True
+ CLOBBER_CHOICES = set(["objdir", "python", "gradle"])
+
+ @Command(
+ "clobber",
+ category="build",
+ description="Clobber the tree (delete the object directory).",
+ )
+ @CommandArgument(
+ "what",
+ default=["objdir", "python"],
+ nargs="*",
+ help="Target to clobber, must be one of {{{}}} (default "
+ "objdir and python).".format(", ".join(CLOBBER_CHOICES)),
+ )
+ @CommandArgument("--full", action="store_true", help="Perform a full clobber")
+ def clobber(self, what, full=False):
+ """Clean up the source and object directories.
+
+ Performing builds and running various commands generate various files.
+
+ Sometimes it is necessary to clean up these files in order to make
+ things work again. This command can be used to perform that cleanup.
+
+ The `objdir` target removes most files in the current object directory
+ (where build output is stored). Some files (like Visual Studio project
+ files) are not removed by default. If you would like to remove the
+ object directory in its entirety, run with `--full`.
+
+ The `python` target will clean up various generated Python files from
+ the source directory and will remove untracked files from well-known
+ directories containing Python packages. Run this to remove .pyc files,
+ compiled C extensions, etc. Note: all files not tracked or ignored by
+ version control in third_party/python will be deleted. Run the `status`
+ command of your VCS to see if any untracked files you haven't committed
+ yet will be deleted.
+
+ The `gradle` target will remove the "gradle" subdirectory of the object
+ directory.
+
+ By default, the command clobbers the `objdir` and `python` targets.
+ """
+ what = set(what)
+ invalid = what - self.CLOBBER_CHOICES
+ if invalid:
+ print("Unknown clobber target(s): {}".format(", ".join(invalid)))
+ return 1
+
+ ret = 0
+ if "objdir" in what:
+ from mozbuild.controller.clobber import Clobberer
+
+ try:
+ Clobberer(self.topsrcdir, self.topobjdir, self.substs).remove_objdir(
+ full
+ )
+ except OSError as e:
+ if sys.platform.startswith("win"):
+ if isinstance(e, WindowsError) and e.winerror in (5, 32):
+ self.log(
+ logging.ERROR,
+ "file_access_error",
+ {"error": e},
+ "Could not clobber because a file was in use. If the "
+ "application is running, try closing it. {error}",
+ )
+ return 1
+ raise
+
+ if "python" in what:
+ if conditions.is_hg(self):
+ cmd = [
+ "hg",
+ "--config",
+ "extensions.purge=",
+ "purge",
+ "--all",
+ "-I",
+ "glob:**.py[cdo]",
+ "-I",
+ "glob:**/__pycache__",
+ "-I",
+ "path:third_party/python/",
+ ]
+ elif conditions.is_git(self):
+ cmd = [
+ "git",
+ "clean",
+ "-d",
+ "-f",
+ "-x",
+ "*.py[cdo]",
+ "*/__pycache__/*",
+ "third_party/python/",
+ ]
+ else:
+ # We don't know what is tracked/untracked if we don't have VCS.
+ # So we can't clean python/ and third_party/python/.
+ cmd = ["find", ".", "-type", "f", "-name", "*.py[cdo]", "-delete"]
+ subprocess.call(cmd, cwd=self.topsrcdir)
+ cmd = [
+ "find",
+ ".",
+ "-type",
+ "d",
+ "-name",
+ "__pycache__",
+ "-empty",
+ "-delete",
+ ]
+ ret = subprocess.call(cmd, cwd=self.topsrcdir)
+
+ if "gradle" in what:
+ shutil.rmtree(mozpath.join(self.topobjdir, "gradle"))
+
+ return ret
+
+ @property
+ def substs(self):
+ try:
+ return super(Clobber, self).substs
+ except BuildEnvironmentNotFoundException:
+ return {}
+
+
+@CommandProvider
+class Logs(MachCommandBase):
+ """Provide commands to read mach logs."""
+
+ NO_AUTO_LOG = True
+
+ @Command("show-log", category="post-build", description="Display mach logs")
+ @CommandArgument(
+ "log_file",
+ nargs="?",
+ type=argparse.FileType("rb"),
+ help="Filename to read log data from. Defaults to the log of the last "
+ "mach command.",
+ )
+ def show_log(self, log_file=None):
+ if not log_file:
+ path = self._get_state_filename("last_log.json")
+ log_file = open(path, "rb")
+
+ if os.isatty(sys.stdout.fileno()):
+ env = dict(os.environ)
+ if "LESS" not in env:
+ # Sensible default flags if none have been set in the user
+ # environment.
+ env[b"LESS"] = b"FRX"
+ less = subprocess.Popen(["less"], stdin=subprocess.PIPE, env=env)
+ # Various objects already have a reference to sys.stdout, so we
+ # can't just change it, we need to change the file descriptor under
+ # it to redirect to less's input.
+ # First keep a copy of the sys.stdout file descriptor.
+ output_fd = os.dup(sys.stdout.fileno())
+ os.dup2(less.stdin.fileno(), sys.stdout.fileno())
+
+ startTime = 0
+ for line in log_file:
+ created, action, params = json.loads(line)
+ if not startTime:
+ startTime = created
+ self.log_manager.terminal_handler.formatter.start_time = created
+ if "line" in params:
+ record = logging.makeLogRecord(
+ {
+ "created": created,
+ "name": self._logger.name,
+ "levelno": logging.INFO,
+ "msg": "{line}",
+ "params": params,
+ "action": action,
+ }
+ )
+ self._logger.handle(record)
+
+ if self.log_manager.terminal:
+ # Close less's input so that it knows that we're done sending data.
+ less.stdin.close()
+ # Since the less's input file descriptor is now also the stdout
+ # file descriptor, we still actually have a non-closed system file
+ # descriptor for less's input. Replacing sys.stdout's file
+ # descriptor with what it was before we replaced it will properly
+ # close less's input.
+ os.dup2(output_fd, sys.stdout.fileno())
+ less.wait()
+
+
+@CommandProvider
+class Warnings(MachCommandBase):
+ """Provide commands for inspecting warnings."""
+
+ @property
+ def database_path(self):
+ return self._get_state_filename("warnings.json")
+
+ @property
+ def database(self):
+ from mozbuild.compilation.warnings import WarningsDatabase
+
+ path = self.database_path
+
+ database = WarningsDatabase()
+
+ if os.path.exists(path):
+ database.load_from_file(path)
+
+ return database
+
+ @Command(
+ "warnings-summary",
+ category="post-build",
+ description="Show a summary of compiler warnings.",
+ )
+ @CommandArgument(
+ "-C",
+ "--directory",
+ default=None,
+ help="Change to a subdirectory of the build directory first.",
+ )
+ @CommandArgument(
+ "report",
+ default=None,
+ nargs="?",
+ help="Warnings report to display. If not defined, show the most "
+ "recent report.",
+ )
+ def summary(self, directory=None, report=None):
+ database = self.database
+
+ if directory:
+ dirpath = self.join_ensure_dir(self.topsrcdir, directory)
+ if not dirpath:
+ return 1
+ else:
+ dirpath = None
+
+ type_counts = database.type_counts(dirpath)
+ sorted_counts = sorted(type_counts.items(), key=operator.itemgetter(1))
+
+ total = 0
+ for k, v in sorted_counts:
+ print("%d\t%s" % (v, k))
+ total += v
+
+ print("%d\tTotal" % total)
+
+ @Command(
+ "warnings-list",
+ category="post-build",
+ description="Show a list of compiler warnings.",
+ )
+ @CommandArgument(
+ "-C",
+ "--directory",
+ default=None,
+ help="Change to a subdirectory of the build directory first.",
+ )
+ @CommandArgument(
+ "--flags", default=None, nargs="+", help="Which warnings flags to match."
+ )
+ @CommandArgument(
+ "report",
+ default=None,
+ nargs="?",
+ help="Warnings report to display. If not defined, show the most "
+ "recent report.",
+ )
+ def list(self, directory=None, flags=None, report=None):
+ database = self.database
+
+ by_name = sorted(database.warnings)
+
+ topsrcdir = mozpath.normpath(self.topsrcdir)
+
+ if directory:
+ directory = mozpath.normsep(directory)
+ dirpath = self.join_ensure_dir(topsrcdir, directory)
+ if not dirpath:
+ return 1
+
+ if flags:
+ # Flatten lists of flags.
+ flags = set(itertools.chain(*[flaglist.split(",") for flaglist in flags]))
+
+ for warning in by_name:
+ filename = mozpath.normsep(warning["filename"])
+
+ if filename.startswith(topsrcdir):
+ filename = filename[len(topsrcdir) + 1 :]
+
+ if directory and not filename.startswith(directory):
+ continue
+
+ if flags and warning["flag"] not in flags:
+ continue
+
+ if warning["column"] is not None:
+ print(
+ "%s:%d:%d [%s] %s"
+ % (
+ filename,
+ warning["line"],
+ warning["column"],
+ warning["flag"],
+ warning["message"],
+ )
+ )
+ else:
+ print(
+ "%s:%d [%s] %s"
+ % (filename, warning["line"], warning["flag"], warning["message"])
+ )
+
+ def join_ensure_dir(self, dir1, dir2):
+ dir1 = mozpath.normpath(dir1)
+ dir2 = mozpath.normsep(dir2)
+ joined_path = mozpath.join(dir1, dir2)
+ if os.path.isdir(joined_path):
+ return joined_path
+ print("Specified directory not found.")
+ return None
+
+
+@CommandProvider
+class GTestCommands(MachCommandBase):
+ @Command(
+ "gtest", category="testing", description="Run GTest unit tests (C++ tests)."
+ )
+ @CommandArgument(
+ "gtest_filter",
+ default=b"*",
+ nargs="?",
+ metavar="gtest_filter",
+ help="test_filter is a ':'-separated list of wildcard patterns "
+ "(called the positive patterns), optionally followed by a '-' "
+ "and another ':'-separated pattern list (called the negative patterns).",
+ )
+ @CommandArgument(
+ "--jobs",
+ "-j",
+ default="1",
+ nargs="?",
+ metavar="jobs",
+ type=int,
+ help="Run the tests in parallel using multiple processes.",
+ )
+ @CommandArgument(
+ "--tbpl-parser",
+ "-t",
+ action="store_true",
+ help="Output test results in a format that can be parsed by TBPL.",
+ )
+ @CommandArgument(
+ "--shuffle",
+ "-s",
+ action="store_true",
+ help="Randomize the execution order of tests.",
+ )
+ @CommandArgument(
+ "--enable-webrender",
+ action="store_true",
+ default=False,
+ dest="enable_webrender",
+ help="Enable the WebRender compositor in Gecko.",
+ )
+ @CommandArgumentGroup("Android")
+ @CommandArgument(
+ "--package",
+ default="org.mozilla.geckoview.test",
+ group="Android",
+ help="Package name of test app.",
+ )
+ @CommandArgument(
+ "--adbpath", dest="adb_path", group="Android", help="Path to adb binary."
+ )
+ @CommandArgument(
+ "--deviceSerial",
+ dest="device_serial",
+ group="Android",
+ help="adb serial number of remote device. "
+ "Required when more than one device is connected to the host. "
+ "Use 'adb devices' to see connected devices.",
+ )
+ @CommandArgument(
+ "--remoteTestRoot",
+ dest="remote_test_root",
+ group="Android",
+ help="Remote directory to use as test root " "(eg. /data/local/tmp/test_root).",
+ )
+ @CommandArgument(
+ "--libxul", dest="libxul_path", group="Android", help="Path to gtest libxul.so."
+ )
+ @CommandArgument(
+ "--no-install",
+ action="store_true",
+ default=False,
+ group="Android",
+ help="Skip the installation of the APK.",
+ )
+ @CommandArgumentGroup("debugging")
+ @CommandArgument(
+ "--debug",
+ action="store_true",
+ group="debugging",
+ help="Enable the debugger. Not specifying a --debugger option will result in "
+ "the default debugger being used.",
+ )
+ @CommandArgument(
+ "--debugger",
+ default=None,
+ type=str,
+ group="debugging",
+ help="Name of debugger to use.",
+ )
+ @CommandArgument(
+ "--debugger-args",
+ default=None,
+ metavar="params",
+ type=str,
+ group="debugging",
+ help="Command-line arguments to pass to the debugger itself; "
+ "split as the Bourne shell would.",
+ )
+ def gtest(
+ self,
+ shuffle,
+ jobs,
+ gtest_filter,
+ tbpl_parser,
+ enable_webrender,
+ package,
+ adb_path,
+ device_serial,
+ remote_test_root,
+ libxul_path,
+ no_install,
+ debug,
+ debugger,
+ debugger_args,
+ ):
+
+ # We lazy build gtest because it's slow to link
+ try:
+ self.config_environment
+ except Exception:
+ print("Please run |./mach build| before |./mach gtest|.")
+ return 1
+
+ res = self._mach_context.commands.dispatch(
+ "build", self._mach_context, what=["recurse_gtest"]
+ )
+ if res:
+ print("Could not build xul-gtest")
+ return res
+
+ if self.substs.get("MOZ_WIDGET_TOOLKIT") == "cocoa":
+ self._run_make(
+ directory="browser/app", target="repackage", ensure_exit_code=True
+ )
+
+ cwd = os.path.join(self.topobjdir, "_tests", "gtest")
+
+ if not os.path.isdir(cwd):
+ os.makedirs(cwd)
+
+ if conditions.is_android(self):
+ if jobs != 1:
+ print("--jobs is not supported on Android and will be ignored")
+ if debug or debugger or debugger_args:
+ print(
+ "--debug options are not supported on Android and will be ignored"
+ )
+ from mozrunner.devices.android_device import InstallIntent
+
+ return self.android_gtest(
+ cwd,
+ shuffle,
+ gtest_filter,
+ package,
+ adb_path,
+ device_serial,
+ remote_test_root,
+ libxul_path,
+ enable_webrender,
+ InstallIntent.NO if no_install else InstallIntent.YES,
+ )
+
+ if (
+ package
+ or adb_path
+ or device_serial
+ or remote_test_root
+ or libxul_path
+ or no_install
+ ):
+ print("One or more Android-only options will be ignored")
+
+ app_path = self.get_binary_path("app")
+ args = [app_path, "-unittest", "--gtest_death_test_style=threadsafe"]
+
+ if sys.platform.startswith("win") and "MOZ_LAUNCHER_PROCESS" in self.defines:
+ args.append("--wait-for-browser")
+
+ if debug or debugger or debugger_args:
+ args = self.prepend_debugger_args(args, debugger, debugger_args)
+ if not args:
+ return 1
+
+ # Use GTest environment variable to control test execution
+ # For details see:
+ # https://code.google.com/p/googletest/wiki/AdvancedGuide#Running_Test_Programs:_Advanced_Options
+ gtest_env = {b"GTEST_FILTER": gtest_filter}
+
+ # Note: we must normalize the path here so that gtest on Windows sees
+ # a MOZ_GMP_PATH which has only Windows dir seperators, because
+ # nsIFile cannot open the paths with non-Windows dir seperators.
+ xre_path = os.path.join(os.path.normpath(self.topobjdir), "dist", "bin")
+ gtest_env["MOZ_XRE_DIR"] = xre_path
+ gtest_env["MOZ_GMP_PATH"] = os.pathsep.join(
+ os.path.join(xre_path, p, "1.0") for p in ("gmp-fake", "gmp-fakeopenh264")
+ )
+
+ gtest_env[b"MOZ_RUN_GTEST"] = b"True"
+
+ if shuffle:
+ gtest_env[b"GTEST_SHUFFLE"] = b"True"
+
+ if tbpl_parser:
+ gtest_env[b"MOZ_TBPL_PARSER"] = b"True"
+
+ if enable_webrender:
+ gtest_env[b"MOZ_WEBRENDER"] = b"1"
+ gtest_env[b"MOZ_ACCELERATED"] = b"1"
+ else:
+ gtest_env[b"MOZ_WEBRENDER"] = b"0"
+
+ if jobs == 1:
+ return self.run_process(
+ args=args,
+ append_env=gtest_env,
+ cwd=cwd,
+ ensure_exit_code=False,
+ pass_thru=True,
+ )
+
+ from mozprocess import ProcessHandlerMixin
+ import functools
+
+ def handle_line(job_id, line):
+ # Prepend the jobId
+ line = "[%d] %s" % (job_id + 1, line.strip())
+ self.log(logging.INFO, "GTest", {"line": line}, "{line}")
+
+ gtest_env["GTEST_TOTAL_SHARDS"] = str(jobs)
+ processes = {}
+ for i in range(0, jobs):
+ gtest_env["GTEST_SHARD_INDEX"] = str(i)
+ processes[i] = ProcessHandlerMixin(
+ [app_path, "-unittest"],
+ cwd=cwd,
+ env=gtest_env,
+ processOutputLine=[functools.partial(handle_line, i)],
+ universal_newlines=True,
+ )
+ processes[i].run()
+
+ exit_code = 0
+ for process in processes.values():
+ status = process.wait()
+ if status:
+ exit_code = status
+
+ # Clamp error code to 255 to prevent overflowing multiple of
+ # 256 into 0
+ if exit_code > 255:
+ exit_code = 255
+
+ return exit_code
+
+ def android_gtest(
+ self,
+ test_dir,
+ shuffle,
+ gtest_filter,
+ package,
+ adb_path,
+ device_serial,
+ remote_test_root,
+ libxul_path,
+ enable_webrender,
+ install,
+ ):
+ # setup logging for mozrunner
+ from mozlog.commandline import setup_logging
+
+ format_args = {"level": self._mach_context.settings["test"]["level"]}
+ default_format = self._mach_context.settings["test"]["format"]
+ setup_logging("mach-gtest", {}, {default_format: sys.stdout}, format_args)
+
+ # ensure that a device is available and test app is installed
+ from mozrunner.devices.android_device import verify_android_device, get_adb_path
+
+ verify_android_device(
+ self, install=install, app=package, device_serial=device_serial
+ )
+
+ if not adb_path:
+ adb_path = get_adb_path(self)
+ if not libxul_path:
+ libxul_path = os.path.join(
+ self.topobjdir, "dist", "bin", "gtest", "libxul.so"
+ )
+
+ # run gtest via remotegtests.py
+ exit_code = 0
+ import imp
+
+ path = os.path.join("testing", "gtest", "remotegtests.py")
+ with open(path, "r") as fh:
+ imp.load_module("remotegtests", fh, path, (".py", "r", imp.PY_SOURCE))
+ import remotegtests
+
+ tester = remotegtests.RemoteGTests()
+ if not tester.run_gtest(
+ test_dir,
+ shuffle,
+ gtest_filter,
+ package,
+ adb_path,
+ device_serial,
+ remote_test_root,
+ libxul_path,
+ None,
+ enable_webrender,
+ ):
+ exit_code = 1
+ tester.cleanup()
+
+ return exit_code
+
+ def prepend_debugger_args(self, args, debugger, debugger_args):
+ """
+ Given an array with program arguments, prepend arguments to run it under a
+ debugger.
+
+ :param args: The executable and arguments used to run the process normally.
+ :param debugger: The debugger to use, or empty to use the default debugger.
+ :param debugger_args: Any additional parameters to pass to the debugger.
+ """
+
+ import mozdebug
+
+ if not debugger:
+ # No debugger name was provided. Look for the default ones on
+ # current OS.
+ debugger = mozdebug.get_default_debugger_name(
+ mozdebug.DebuggerSearch.KeepLooking
+ )
+
+ if debugger:
+ debuggerInfo = mozdebug.get_debugger_info(debugger, debugger_args)
+
+ if not debugger or not debuggerInfo:
+ print("Could not find a suitable debugger in your PATH.")
+ return None
+
+ # Parameters come from the CLI. We need to convert them before
+ # their use.
+ if debugger_args:
+ from mozbuild import shellutil
+
+ try:
+ debugger_args = shellutil.split(debugger_args)
+ except shellutil.MetaCharacterException as e:
+ print(
+ "The --debugger_args you passed require a real shell to parse them."
+ )
+ print("(We can't handle the %r character.)" % e.char)
+ return None
+
+ # Prepend the debugger args.
+ args = [debuggerInfo.path] + debuggerInfo.args + args
+ return args
+
+
+@CommandProvider
+class Package(MachCommandBase):
+ """Package the built product for distribution."""
+
+ @Command(
+ "package",
+ category="post-build",
+ description="Package the built product for distribution as an APK, DMG, etc.",
+ )
+ @CommandArgument(
+ "-v",
+ "--verbose",
+ action="store_true",
+ help="Verbose output for what commands the packaging process is running.",
+ )
+ def package(self, verbose=False):
+ ret = self._run_make(
+ directory=".", target="package", silent=not verbose, ensure_exit_code=False
+ )
+ if ret == 0:
+ self.notify("Packaging complete")
+ return ret
+
+
+def _get_android_install_parser():
+ parser = argparse.ArgumentParser()
+ parser.add_argument(
+ "--app",
+ default="org.mozilla.geckoview_example",
+ help="Android package to install " "(default: org.mozilla.geckoview_example)",
+ )
+ parser.add_argument(
+ "--verbose",
+ "-v",
+ action="store_true",
+ help="Print verbose output when installing.",
+ )
+ return parser
+
+
+def setup_install_parser():
+ build = MozbuildObject.from_environment(cwd=here)
+ if conditions.is_android(build):
+ return _get_android_install_parser()
+ return argparse.ArgumentParser()
+
+
+@CommandProvider
+class Install(MachCommandBase):
+ """Install a package."""
+
+ @Command(
+ "install",
+ category="post-build",
+ conditions=[conditions.has_build],
+ parser=setup_install_parser,
+ description="Install the package on the machine (or device in the case of Android).",
+ )
+ def install(self, **kwargs):
+ if conditions.is_android(self):
+ from mozrunner.devices.android_device import (
+ verify_android_device,
+ InstallIntent,
+ )
+
+ ret = verify_android_device(self, install=InstallIntent.YES, **kwargs) == 0
+ else:
+ ret = self._run_make(
+ directory=".", target="install", ensure_exit_code=False
+ )
+
+ if ret == 0:
+ self.notify("Install complete")
+ return ret
+
+
+@SettingsProvider
+class RunSettings:
+ config_settings = [
+ (
+ "runprefs.*",
+ "string",
+ """
+Pass a pref into Firefox when using `mach run`, of the form `foo.bar=value`.
+Prefs will automatically be cast into the appropriate type. Integers can be
+single quoted to force them to be strings.
+""".strip(),
+ ),
+ ]
+
+
+def _get_android_run_parser():
+ parser = argparse.ArgumentParser()
+ parser.add_argument(
+ "--app",
+ default="org.mozilla.geckoview_example",
+ help="Android package to run " "(default: org.mozilla.geckoview_example)",
+ )
+ parser.add_argument(
+ "--intent",
+ default="android.intent.action.VIEW",
+ help="Android intent action to launch with "
+ "(default: android.intent.action.VIEW)",
+ )
+ parser.add_argument(
+ "--setenv",
+ dest="env",
+ action="append",
+ default=[],
+ help="Set target environment variable, like FOO=BAR",
+ )
+ parser.add_argument(
+ "--profile",
+ "-P",
+ default=None,
+ help="Path to Gecko profile, like /path/to/host/profile "
+ "or /path/to/target/profile",
+ )
+ parser.add_argument("--url", default=None, help="URL to open")
+ parser.add_argument(
+ "--no-install",
+ action="store_true",
+ default=False,
+ help="Do not try to install application on device before running "
+ "(default: False)",
+ )
+ parser.add_argument(
+ "--no-wait",
+ action="store_true",
+ default=False,
+ help="Do not wait for application to start before returning "
+ "(default: False)",
+ )
+ parser.add_argument(
+ "--enable-fission",
+ action="store_true",
+ help="Run the program with Fission (site isolation) enabled.",
+ )
+ parser.add_argument(
+ "--fail-if-running",
+ action="store_true",
+ default=False,
+ help="Fail if application is already running (default: False)",
+ )
+ parser.add_argument(
+ "--restart",
+ action="store_true",
+ default=False,
+ help="Stop the application if it is already running (default: False)",
+ )
+ return parser
+
+
+def _get_jsshell_run_parser():
+ parser = argparse.ArgumentParser()
+ group = parser.add_argument_group("the compiled program")
+ group.add_argument(
+ "params",
+ nargs="...",
+ default=[],
+ help="Command-line arguments to be passed through to the program. Not "
+ "specifying a --profile or -P option will result in a temporary profile "
+ "being used.",
+ )
+
+ group = parser.add_argument_group("debugging")
+ group.add_argument(
+ "--debug",
+ action="store_true",
+ help="Enable the debugger. Not specifying a --debugger option will result "
+ "in the default debugger being used.",
+ )
+ group.add_argument(
+ "--debugger", default=None, type=str, help="Name of debugger to use."
+ )
+ group.add_argument(
+ "--debugger-args",
+ default=None,
+ metavar="params",
+ type=str,
+ help="Command-line arguments to pass to the debugger itself; "
+ "split as the Bourne shell would.",
+ )
+ group.add_argument(
+ "--debugparams",
+ action=StoreDebugParamsAndWarnAction,
+ default=None,
+ type=str,
+ dest="debugger_args",
+ help=argparse.SUPPRESS,
+ )
+
+ return parser
+
+
+def _get_desktop_run_parser():
+ parser = argparse.ArgumentParser()
+ group = parser.add_argument_group("the compiled program")
+ group.add_argument(
+ "params",
+ nargs="...",
+ default=[],
+ help="Command-line arguments to be passed through to the program. Not "
+ "specifying a --profile or -P option will result in a temporary profile "
+ "being used.",
+ )
+ group.add_argument("--packaged", action="store_true", help="Run a packaged build.")
+ group.add_argument(
+ "--app", help="Path to executable to run (default: output of ./mach build)"
+ )
+ group.add_argument(
+ "--remote",
+ "-r",
+ action="store_true",
+ help="Do not pass the --no-remote argument by default.",
+ )
+ group.add_argument(
+ "--background",
+ "-b",
+ action="store_true",
+ help="Do not pass the --foreground argument by default on Mac.",
+ )
+ group.add_argument(
+ "--noprofile",
+ "-n",
+ action="store_true",
+ help="Do not pass the --profile argument by default.",
+ )
+ group.add_argument(
+ "--disable-e10s",
+ action="store_true",
+ help="Run the program with electrolysis disabled.",
+ )
+ group.add_argument(
+ "--enable-crash-reporter",
+ action="store_true",
+ help="Run the program with the crash reporter enabled.",
+ )
+ group.add_argument(
+ "--enable-fission",
+ action="store_true",
+ help="Run the program with Fission (site isolation) enabled.",
+ )
+ group.add_argument(
+ "--setpref",
+ action="append",
+ default=[],
+ help="Set the specified pref before starting the program. Can be set "
+ "multiple times. Prefs can also be set in ~/.mozbuild/machrc in the "
+ "[runprefs] section - see `./mach settings` for more information.",
+ )
+ group.add_argument(
+ "--temp-profile",
+ action="store_true",
+ help="Run the program using a new temporary profile created inside "
+ "the objdir.",
+ )
+ group.add_argument(
+ "--macos-open",
+ action="store_true",
+ help="On macOS, run the program using the open(1) command. Per open(1), "
+ "the browser is launched \"just as if you had double-clicked the file's "
+ 'icon". The browser can not be launched under a debugger with this '
+ "option.",
+ )
+
+ group = parser.add_argument_group("debugging")
+ group.add_argument(
+ "--debug",
+ action="store_true",
+ help="Enable the debugger. Not specifying a --debugger option will result "
+ "in the default debugger being used.",
+ )
+ group.add_argument(
+ "--debugger", default=None, type=str, help="Name of debugger to use."
+ )
+ group.add_argument(
+ "--debugger-args",
+ default=None,
+ metavar="params",
+ type=str,
+ help="Command-line arguments to pass to the debugger itself; "
+ "split as the Bourne shell would.",
+ )
+ group.add_argument(
+ "--debugparams",
+ action=StoreDebugParamsAndWarnAction,
+ default=None,
+ type=str,
+ dest="debugger_args",
+ help=argparse.SUPPRESS,
+ )
+
+ group = parser.add_argument_group("DMD")
+ group.add_argument(
+ "--dmd",
+ action="store_true",
+ help="Enable DMD. The following arguments have no effect without this.",
+ )
+ group.add_argument(
+ "--mode",
+ choices=["live", "dark-matter", "cumulative", "scan"],
+ help="Profiling mode. The default is 'dark-matter'.",
+ )
+ group.add_argument(
+ "--stacks",
+ choices=["partial", "full"],
+ help="Allocation stack trace coverage. The default is 'partial'.",
+ )
+ group.add_argument(
+ "--show-dump-stats", action="store_true", help="Show stats when doing dumps."
+ )
+
+ return parser
+
+
+def setup_run_parser():
+ build = MozbuildObject.from_environment(cwd=here)
+ if conditions.is_android(build):
+ return _get_android_run_parser()
+ if conditions.is_jsshell(build):
+ return _get_jsshell_run_parser()
+ return _get_desktop_run_parser()
+
+
+@CommandProvider
+class RunProgram(MachCommandBase):
+ """Run the compiled program."""
+
+ @Command(
+ "run",
+ category="post-build",
+ conditions=[conditions.has_build_or_shell],
+ parser=setup_run_parser,
+ description="Run the compiled program, possibly under a debugger or DMD.",
+ )
+ def run(self, **kwargs):
+ if conditions.is_android(self):
+ return self._run_android(**kwargs)
+ if conditions.is_jsshell(self):
+ return self._run_jsshell(**kwargs)
+ return self._run_desktop(**kwargs)
+
+ def _run_android(
+ self,
+ app="org.mozilla.geckoview_example",
+ intent=None,
+ env=[],
+ profile=None,
+ url=None,
+ no_install=None,
+ no_wait=None,
+ fail_if_running=None,
+ restart=None,
+ enable_fission=False,
+ ):
+ from mozrunner.devices.android_device import (
+ verify_android_device,
+ _get_device,
+ InstallIntent,
+ )
+ from six.moves import shlex_quote
+
+ if app == "org.mozilla.geckoview_example":
+ activity_name = "org.mozilla.geckoview_example.GeckoViewActivity"
+ elif app == "org.mozilla.geckoview.test":
+ activity_name = "org.mozilla.geckoview.test.TestRunnerActivity"
+ elif "fennec" in app or "firefox" in app:
+ activity_name = "org.mozilla.gecko.BrowserApp"
+ else:
+ raise RuntimeError("Application not recognized: {}".format(app))
+
+ # `verify_android_device` respects `DEVICE_SERIAL` if it is set and sets it otherwise.
+ verify_android_device(
+ self, app=app, install=InstallIntent.NO if no_install else InstallIntent.YES
+ )
+ device_serial = os.environ.get("DEVICE_SERIAL")
+ if not device_serial:
+ print("No ADB devices connected.")
+ return 1
+
+ device = _get_device(self.substs, device_serial=device_serial)
+
+ args = []
+ if profile:
+ if os.path.isdir(profile):
+ host_profile = profile
+ # Always /data/local/tmp, rather than `device.test_root`, because GeckoView only
+ # takes its configuration file from /data/local/tmp, and we want to follow suit.
+ target_profile = "/data/local/tmp/{}-profile".format(app)
+ device.rm(target_profile, recursive=True, force=True)
+ device.push(host_profile, target_profile)
+ self.log(
+ logging.INFO,
+ "run",
+ {"host_profile": host_profile, "target_profile": target_profile},
+ 'Pushed profile from host "{host_profile}" to target "{target_profile}"',
+ )
+ else:
+ target_profile = profile
+ self.log(
+ logging.INFO,
+ "run",
+ {"target_profile": target_profile},
+ 'Using profile from target "{target_profile}"',
+ )
+
+ args = ["--profile", shlex_quote(target_profile)]
+
+ if enable_fission:
+ env.append("MOZ_FORCE_ENABLE_FISSION=1")
+
+ extras = {}
+ for i, e in enumerate(env):
+ extras["env{}".format(i)] = e
+ if args:
+ extras["args"] = " ".join(args)
+
+ if env or args:
+ restart = True
+
+ if restart:
+ fail_if_running = False
+ self.log(
+ logging.INFO,
+ "run",
+ {"app": app},
+ "Stopping {app} to ensure clean restart.",
+ )
+ device.stop_application(app)
+
+ # We'd prefer to log the actual `am start ...` command, but it's not trivial to wire the
+ # device's logger to mach's logger.
+ self.log(
+ logging.INFO,
+ "run",
+ {"app": app, "activity_name": activity_name},
+ "Starting {app}/{activity_name}.",
+ )
+
+ device.launch_application(
+ app_name=app,
+ activity_name=activity_name,
+ intent=intent,
+ extras=extras,
+ url=url,
+ wait=not no_wait,
+ fail_if_running=fail_if_running,
+ )
+
+ return 0
+
+ def _run_jsshell(self, params, debug, debugger, debugger_args):
+ try:
+ binpath = self.get_binary_path("app")
+ except BinaryNotFoundException as e:
+ self.log(logging.ERROR, "run", {"error": str(e)}, "ERROR: {error}")
+ self.log(logging.INFO, "run", {"help": e.help()}, "{help}")
+ return 1
+
+ args = [binpath]
+
+ if params:
+ args.extend(params)
+
+ extra_env = {
+ "RUST_BACKTRACE": "full",
+ }
+
+ if debug or debugger or debugger_args:
+ if "INSIDE_EMACS" in os.environ:
+ self.log_manager.terminal_handler.setLevel(logging.WARNING)
+
+ import mozdebug
+
+ if not debugger:
+ # No debugger name was provided. Look for the default ones on
+ # current OS.
+ debugger = mozdebug.get_default_debugger_name(
+ mozdebug.DebuggerSearch.KeepLooking
+ )
+
+ if debugger:
+ self.debuggerInfo = mozdebug.get_debugger_info(debugger, debugger_args)
+
+ if not debugger or not self.debuggerInfo:
+ print("Could not find a suitable debugger in your PATH.")
+ return 1
+
+ # Parameters come from the CLI. We need to convert them before
+ # their use.
+ if debugger_args:
+ from mozbuild import shellutil
+
+ try:
+ debugger_args = shellutil.split(debugger_args)
+ except shellutil.MetaCharacterException as e:
+ print(
+ "The --debugger-args you passed require a real shell to parse them."
+ )
+ print("(We can't handle the %r character.)" % e.char)
+ return 1
+
+ # Prepend the debugger args.
+ args = [self.debuggerInfo.path] + self.debuggerInfo.args + args
+
+ return self.run_process(
+ args=args, ensure_exit_code=False, pass_thru=True, append_env=extra_env
+ )
+
+ def _run_desktop(
+ self,
+ params,
+ packaged,
+ app,
+ remote,
+ background,
+ noprofile,
+ disable_e10s,
+ enable_crash_reporter,
+ enable_fission,
+ setpref,
+ temp_profile,
+ macos_open,
+ debug,
+ debugger,
+ debugger_args,
+ dmd,
+ mode,
+ stacks,
+ show_dump_stats,
+ ):
+ from mozprofile import Profile, Preferences
+
+ try:
+ if packaged:
+ binpath = self.get_binary_path(where="staged-package")
+ else:
+ binpath = app or self.get_binary_path("app")
+ except BinaryNotFoundException as e:
+ self.log(logging.ERROR, "run", {"error": str(e)}, "ERROR: {error}")
+ if packaged:
+ self.log(
+ logging.INFO,
+ "run",
+ {
+ "help": "It looks like your build isn't packaged. "
+ "You can run |./mach package| to package it."
+ },
+ "{help}",
+ )
+ else:
+ self.log(logging.INFO, "run", {"help": e.help()}, "{help}")
+ return 1
+
+ args = []
+ if macos_open:
+ if debug:
+ print(
+ "The browser can not be launched in the debugger "
+ "when using the macOS open command."
+ )
+ return 1
+ try:
+ m = re.search(r"^.+\.app", binpath)
+ apppath = m.group(0)
+ args = ["open", apppath, "--args"]
+ except Exception as e:
+ print(
+ "Couldn't get the .app path from the binary path. "
+ "The macOS open option can only be used on macOS"
+ )
+ print(e)
+ return 1
+ else:
+ args = [binpath]
+
+ if params:
+ args.extend(params)
+
+ if not remote:
+ args.append("-no-remote")
+
+ if not background and sys.platform == "darwin":
+ args.append("-foreground")
+
+ if sys.platform.startswith("win") and "MOZ_LAUNCHER_PROCESS" in self.defines:
+ args.append("-wait-for-browser")
+
+ no_profile_option_given = all(
+ p not in params for p in ["-profile", "--profile", "-P"]
+ )
+ if no_profile_option_given and not noprofile:
+ prefs = {
+ "browser.aboutConfig.showWarning": False,
+ "browser.shell.checkDefaultBrowser": False,
+ "general.warnOnAboutConfig": False,
+ }
+ prefs.update(self._mach_context.settings.runprefs)
+ prefs.update([p.split("=", 1) for p in setpref])
+ for pref in prefs:
+ prefs[pref] = Preferences.cast(prefs[pref])
+
+ tmpdir = os.path.join(self.topobjdir, "tmp")
+ if not os.path.exists(tmpdir):
+ os.makedirs(tmpdir)
+
+ if temp_profile:
+ path = tempfile.mkdtemp(dir=tmpdir, prefix="profile-")
+ else:
+ path = os.path.join(tmpdir, "profile-default")
+
+ profile = Profile(path, preferences=prefs)
+ args.append("-profile")
+ args.append(profile.profile)
+
+ if not no_profile_option_given and setpref:
+ print("setpref is only supported if a profile is not specified")
+ return 1
+
+ if not no_profile_option_given:
+ # The profile name may be non-ascii, but come from the
+ # commandline as str, so convert here with a better guess at
+ # an encoding than the default.
+ encoding = sys.getfilesystemencoding() or sys.getdefaultencoding()
+ args = [
+ unicode(a, encoding) if not isinstance(a, unicode) else a
+ for a in args
+ ]
+
+ some_debugging_option = debug or debugger or debugger_args
+
+ # By default, because Firefox is a GUI app, on Windows it will not
+ # 'create' a console to which stdout/stderr is printed. This means
+ # printf/dump debugging is invisible. We default to adding the
+ # -attach-console argument to fix this. We avoid this if we're launched
+ # under a debugger (which can do its own picking up of stdout/stderr).
+ # We also check for both the -console and -attach-console flags:
+ # -console causes Firefox to create a separate window;
+ # -attach-console just ends us up with output that gets relayed via mach.
+ # We shouldn't override the user using -console. For more info, see
+ # https://bugzilla.mozilla.org/show_bug.cgi?id=1257155
+ if (
+ sys.platform.startswith("win")
+ and not some_debugging_option
+ and "-console" not in args
+ and "--console" not in args
+ and "-attach-console" not in args
+ and "--attach-console" not in args
+ ):
+ args.append("-attach-console")
+
+ extra_env = {
+ "MOZ_DEVELOPER_REPO_DIR": self.topsrcdir,
+ "MOZ_DEVELOPER_OBJ_DIR": self.topobjdir,
+ "RUST_BACKTRACE": "full",
+ }
+
+ if not enable_crash_reporter:
+ extra_env["MOZ_CRASHREPORTER_DISABLE"] = "1"
+ else:
+ extra_env["MOZ_CRASHREPORTER"] = "1"
+
+ if disable_e10s:
+ version_file = os.path.join(
+ self.topsrcdir, "browser", "config", "version.txt"
+ )
+ f = open(version_file, "r")
+ extra_env["MOZ_FORCE_DISABLE_E10S"] = f.read().strip()
+
+ if enable_fission:
+ extra_env["MOZ_FORCE_ENABLE_FISSION"] = "1"
+
+ if some_debugging_option:
+ if "INSIDE_EMACS" in os.environ:
+ self.log_manager.terminal_handler.setLevel(logging.WARNING)
+
+ import mozdebug
+
+ if not debugger:
+ # No debugger name was provided. Look for the default ones on
+ # current OS.
+ debugger = mozdebug.get_default_debugger_name(
+ mozdebug.DebuggerSearch.KeepLooking
+ )
+
+ if debugger:
+ self.debuggerInfo = mozdebug.get_debugger_info(debugger, debugger_args)
+
+ if not debugger or not self.debuggerInfo:
+ print("Could not find a suitable debugger in your PATH.")
+ return 1
+
+ # Parameters come from the CLI. We need to convert them before
+ # their use.
+ if debugger_args:
+ from mozbuild import shellutil
+
+ try:
+ debugger_args = shellutil.split(debugger_args)
+ except shellutil.MetaCharacterException as e:
+ print(
+ "The --debugger-args you passed require a real shell to parse them."
+ )
+ print("(We can't handle the %r character.)" % e.char)
+ return 1
+
+ # Prepend the debugger args.
+ args = [self.debuggerInfo.path] + self.debuggerInfo.args + args
+
+ if dmd:
+ dmd_params = []
+
+ if mode:
+ dmd_params.append("--mode=" + mode)
+ if stacks:
+ dmd_params.append("--stacks=" + stacks)
+ if show_dump_stats:
+ dmd_params.append("--show-dump-stats=yes")
+
+ if dmd_params:
+ extra_env["DMD"] = " ".join(dmd_params)
+ else:
+ extra_env["DMD"] = "1"
+
+ return self.run_process(
+ args=args, ensure_exit_code=False, pass_thru=True, append_env=extra_env
+ )
+
+
+@CommandProvider
+class Buildsymbols(MachCommandBase):
+ """Produce a package of debug symbols suitable for use with Breakpad."""
+
+ @Command(
+ "buildsymbols",
+ category="post-build",
+ description="Produce a package of Breakpad-format symbols.",
+ )
+ def buildsymbols(self):
+ return self._run_make(
+ directory=".", target="buildsymbols", ensure_exit_code=False
+ )
+
+
+@CommandProvider
+class MachDebug(MachCommandBase):
+ @Command(
+ "environment",
+ category="build-dev",
+ description="Show info about the mach and build environment.",
+ )
+ @CommandArgument(
+ "--format",
+ default="pretty",
+ choices=["pretty", "json"],
+ help="Print data in the given format.",
+ )
+ @CommandArgument("--output", "-o", type=str, help="Output to the given file.")
+ @CommandArgument(
+ "--verbose", "-v", action="store_true", help="Print verbose output."
+ )
+ def environment(self, format, output=None, verbose=False):
+ func = getattr(self, "_environment_%s" % format.replace(".", "_"))
+
+ if output:
+ # We want to preserve mtimes if the output file already exists
+ # and the content hasn't changed.
+ from mozbuild.util import FileAvoidWrite
+
+ with FileAvoidWrite(output) as out:
+ return func(out, verbose)
+ return func(sys.stdout, verbose)
+
+ def _environment_pretty(self, out, verbose):
+ state_dir = self._mach_context.state_dir
+ import platform
+
+ print("platform:\n\t%s" % platform.platform(), file=out)
+ print("python version:\n\t%s" % sys.version, file=out)
+ print("python prefix:\n\t%s" % sys.prefix, file=out)
+ print("mach cwd:\n\t%s" % self._mach_context.cwd, file=out)
+ print("os cwd:\n\t%s" % os.getcwd(), file=out)
+ print("mach directory:\n\t%s" % self._mach_context.topdir, file=out)
+ print("state directory:\n\t%s" % state_dir, file=out)
+
+ print("object directory:\n\t%s" % self.topobjdir, file=out)
+
+ if self.mozconfig["path"]:
+ print("mozconfig path:\n\t%s" % self.mozconfig["path"], file=out)
+ if self.mozconfig["configure_args"]:
+ print("mozconfig configure args:", file=out)
+ for arg in self.mozconfig["configure_args"]:
+ print("\t%s" % arg, file=out)
+
+ if self.mozconfig["make_extra"]:
+ print("mozconfig extra make args:", file=out)
+ for arg in self.mozconfig["make_extra"]:
+ print("\t%s" % arg, file=out)
+
+ if self.mozconfig["make_flags"]:
+ print("mozconfig make flags:", file=out)
+ for arg in self.mozconfig["make_flags"]:
+ print("\t%s" % arg, file=out)
+
+ config = None
+
+ try:
+ config = self.config_environment
+
+ except Exception:
+ pass
+
+ if config:
+ print("config topsrcdir:\n\t%s" % config.topsrcdir, file=out)
+ print("config topobjdir:\n\t%s" % config.topobjdir, file=out)
+
+ if verbose:
+ print("config substitutions:", file=out)
+ for k in sorted(config.substs):
+ print("\t%s: %s" % (k, config.substs[k]), file=out)
+
+ print("config defines:", file=out)
+ for k in sorted(config.defines):
+ print("\t%s" % k, file=out)
+
+ def _environment_json(self, out, verbose):
+ import json
+
+ class EnvironmentEncoder(json.JSONEncoder):
+ def default(self, obj):
+ if isinstance(obj, MozbuildObject):
+ result = {
+ "topsrcdir": obj.topsrcdir,
+ "topobjdir": obj.topobjdir,
+ "mozconfig": obj.mozconfig,
+ }
+ if verbose:
+ result["substs"] = obj.substs
+ result["defines"] = obj.defines
+ return result
+ elif isinstance(obj, set):
+ return list(obj)
+ return json.JSONEncoder.default(self, obj)
+
+ json.dump(self, cls=EnvironmentEncoder, sort_keys=True, fp=out)
+
+
+@CommandProvider
+class Repackage(MachCommandBase):
+ """Repackages artifacts into different formats.
+
+ This is generally used after packages are signed by the signing
+ scriptworkers in order to bundle things up into shippable formats, such as a
+ .dmg on OSX or an installer exe on Windows.
+ """
+
+ @Command(
+ "repackage",
+ category="misc",
+ description="Repackage artifacts into different formats.",
+ )
+ def repackage(self):
+ print("Usage: ./mach repackage [dmg|installer|mar] [args...]")
+
+ @SubCommand(
+ "repackage", "dmg", description="Repackage a tar file into a .dmg for OSX"
+ )
+ @CommandArgument("--input", "-i", type=str, required=True, help="Input filename")
+ @CommandArgument("--output", "-o", type=str, required=True, help="Output filename")
+ def repackage_dmg(self, input, output):
+ if not os.path.exists(input):
+ print("Input file does not exist: %s" % input)
+ return 1
+
+ if not os.path.exists(os.path.join(self.topobjdir, "config.status")):
+ print(
+ "config.status not found. Please run |mach configure| "
+ "prior to |mach repackage|."
+ )
+ return 1
+
+ from mozbuild.repackaging.dmg import repackage_dmg
+
+ repackage_dmg(input, output)
+
+ @SubCommand(
+ "repackage", "installer", description="Repackage into a Windows installer exe"
+ )
+ @CommandArgument(
+ "--tag",
+ type=str,
+ required=True,
+ help="The .tag file used to build the installer",
+ )
+ @CommandArgument(
+ "--setupexe",
+ type=str,
+ required=True,
+ help="setup.exe file inside the installer",
+ )
+ @CommandArgument(
+ "--package",
+ type=str,
+ required=False,
+ help="Optional package .zip for building a full installer",
+ )
+ @CommandArgument("--output", "-o", type=str, required=True, help="Output filename")
+ @CommandArgument(
+ "--package-name",
+ type=str,
+ required=False,
+ help="Name of the package being rebuilt",
+ )
+ @CommandArgument(
+ "--sfx-stub", type=str, required=True, help="Path to the self-extraction stub."
+ )
+ @CommandArgument(
+ "--use-upx",
+ required=False,
+ action="store_true",
+ help="Run UPX on the self-extraction stub.",
+ )
+ def repackage_installer(
+ self, tag, setupexe, package, output, package_name, sfx_stub, use_upx
+ ):
+ from mozbuild.repackaging.installer import repackage_installer
+
+ repackage_installer(
+ topsrcdir=self.topsrcdir,
+ tag=tag,
+ setupexe=setupexe,
+ package=package,
+ output=output,
+ package_name=package_name,
+ sfx_stub=sfx_stub,
+ use_upx=use_upx,
+ )
+
+ @SubCommand("repackage", "msi", description="Repackage into a MSI")
+ @CommandArgument(
+ "--wsx",
+ type=str,
+ required=True,
+ help="The wsx file used to build the installer",
+ )
+ @CommandArgument(
+ "--version",
+ type=str,
+ required=True,
+ help="The Firefox version used to create the installer",
+ )
+ @CommandArgument(
+ "--locale", type=str, required=True, help="The locale of the installer"
+ )
+ @CommandArgument(
+ "--arch", type=str, required=True, help="The archtecture you are building."
+ )
+ @CommandArgument("--setupexe", type=str, required=True, help="setup.exe installer")
+ @CommandArgument(
+ "--candle", type=str, required=False, help="location of candle binary"
+ )
+ @CommandArgument(
+ "--light", type=str, required=False, help="location of light binary"
+ )
+ @CommandArgument("--output", "-o", type=str, required=True, help="Output filename")
+ def repackage_msi(
+ self, wsx, version, locale, arch, setupexe, candle, light, output
+ ):
+ from mozbuild.repackaging.msi import repackage_msi
+
+ repackage_msi(
+ topsrcdir=self.topsrcdir,
+ wsx=wsx,
+ version=version,
+ locale=locale,
+ arch=arch,
+ setupexe=setupexe,
+ candle=candle,
+ light=light,
+ output=output,
+ )
+
+ @SubCommand("repackage", "mar", description="Repackage into complete MAR file")
+ @CommandArgument("--input", "-i", type=str, required=True, help="Input filename")
+ @CommandArgument("--mar", type=str, required=True, help="Mar binary path")
+ @CommandArgument("--output", "-o", type=str, required=True, help="Output filename")
+ @CommandArgument(
+ "--arch", type=str, required=True, help="The archtecture you are building."
+ )
+ @CommandArgument("--mar-channel-id", type=str, help="Mar channel id")
+ def repackage_mar(self, input, mar, output, arch, mar_channel_id):
+ from mozbuild.repackaging.mar import repackage_mar
+
+ repackage_mar(
+ self.topsrcdir,
+ input,
+ mar,
+ output,
+ arch=arch,
+ mar_channel_id=mar_channel_id,
+ )
+
+
+@SettingsProvider
+class TelemetrySettings:
+ config_settings = [
+ (
+ "build.telemetry",
+ "boolean",
+ """
+Enable submission of build system telemetry.
+ """.strip(),
+ False,
+ ),
+ ]
+
+
+@CommandProvider
+class L10NCommands(MachCommandBase):
+ @Command(
+ "package-multi-locale",
+ category="post-build",
+ description="Package a multi-locale version of the built product "
+ "for distribution as an APK, DMG, etc.",
+ )
+ @CommandArgument(
+ "--locales",
+ metavar="LOCALES",
+ nargs="+",
+ required=True,
+ help='List of locales to package, including "en-US"',
+ )
+ @CommandArgument(
+ "--verbose", action="store_true", help="Log informative status messages."
+ )
+ def package_l10n(self, verbose=False, locales=[]):
+ if "RecursiveMake" not in self.substs["BUILD_BACKENDS"]:
+ print(
+ "Artifact builds do not support localization. "
+ "If you know what you are doing, you can use:\n"
+ "ac_add_options --disable-compile-environment\n"
+ "export BUILD_BACKENDS=FasterMake,RecursiveMake\n"
+ "in your mozconfig."
+ )
+ return 1
+
+ if "en-US" not in locales:
+ self.log(
+ logging.WARN,
+ "package-multi-locale",
+ {"locales": locales},
+ 'List of locales does not include default locale "en-US": '
+ '{locales}; adding "en-US"',
+ )
+ locales.append("en-US")
+ locales = list(sorted(locales))
+
+ append_env = {
+ # We are only (re-)packaging, we don't want to (re-)build
+ # anything inside Gradle.
+ "GRADLE_INVOKED_WITHIN_MACH_BUILD": "1",
+ "MOZ_CHROME_MULTILOCALE": " ".join(locales),
+ }
+
+ for locale in locales:
+ if locale == "en-US":
+ self.log(
+ logging.INFO,
+ "package-multi-locale",
+ {"locale": locale},
+ "Skipping default locale {locale}",
+ )
+ continue
+
+ self.log(
+ logging.INFO,
+ "package-multi-locale",
+ {"locale": locale},
+ "Processing chrome Gecko resources for locale {locale}",
+ )
+ self.run_process(
+ [
+ mozpath.join(self.topsrcdir, "mach"),
+ "build",
+ "chrome-{}".format(locale),
+ ],
+ append_env=append_env,
+ pass_thru=True,
+ ensure_exit_code=True,
+ cwd=mozpath.join(self.topsrcdir),
+ )
+
+ if self.substs["MOZ_BUILD_APP"] == "mobile/android":
+ self.log(
+ logging.INFO,
+ "package-multi-locale",
+ {},
+ "Invoking `mach android assemble-app`",
+ )
+ self.run_process(
+ [mozpath.join(self.topsrcdir, "mach"), "android", "assemble-app"],
+ append_env=append_env,
+ pass_thru=True,
+ ensure_exit_code=True,
+ cwd=mozpath.join(self.topsrcdir),
+ )
+
+ if self.substs["MOZ_BUILD_APP"] == "browser":
+ self.log(
+ logging.INFO,
+ "package-multi-locale",
+ {},
+ "Repackaging browser",
+ )
+ self._run_make(
+ directory=mozpath.join(self.topobjdir, "browser", "app"),
+ target=["tools"],
+ append_env=append_env,
+ pass_thru=True,
+ ensure_exit_code=True,
+ )
+
+ self.log(
+ logging.INFO,
+ "package-multi-locale",
+ {},
+ "Invoking multi-locale `mach package`",
+ )
+ target = ["package"]
+ if self.substs["MOZ_BUILD_APP"] == "mobile/android":
+ target.append("AB_CD=multi")
+
+ self._run_make(
+ directory=self.topobjdir,
+ target=target,
+ append_env=append_env,
+ pass_thru=True,
+ ensure_exit_code=True,
+ )
+
+ if self.substs["MOZ_BUILD_APP"] == "mobile/android":
+ self.log(
+ logging.INFO,
+ "package-multi-locale",
+ {},
+ "Invoking `mach android archive-geckoview`",
+ )
+ self.run_process(
+ [mozpath.join(self.topsrcdir, "mach"), "android", "archive-geckoview"],
+ append_env=append_env,
+ pass_thru=True,
+ ensure_exit_code=True,
+ cwd=mozpath.join(self.topsrcdir),
+ )
+
+ return 0
+
+
+@CommandProvider
+class CreateMachEnvironment(MachCommandBase):
+ """Create the mach virtualenvs."""
+
+ @Command(
+ "create-mach-environment",
+ category="devenv",
+ description=(
+ "Create the `mach` virtualenvs. If executed with python3 (the "
+ "default when entering from `mach`), create both a python3 "
+ "and python2.7 virtualenv. If executed with python2, only "
+ "create the python2.7 virtualenv."
+ ),
+ )
+ @CommandArgument(
+ "-f",
+ "--force",
+ action="store_true",
+ help=("Force re-creating the virtualenv even if it is already " "up-to-date."),
+ )
+ def create_mach_environment(self, force=False):
+ from mozboot.util import get_mach_virtualenv_root
+ from mozbuild.pythonutil import find_python2_executable
+ from mozbuild.virtualenv import VirtualenvManager
+ from six import PY2
+
+ virtualenv_path = get_mach_virtualenv_root(py2=PY2)
+ if sys.executable.startswith(virtualenv_path):
+ print(
+ "You can only create a mach environment with the system "
+ "Python. Re-run this `mach` command with the system Python.",
+ file=sys.stderr,
+ )
+ return 1
+
+ manager = VirtualenvManager(
+ self.topsrcdir,
+ virtualenv_path,
+ sys.stdout,
+ os.path.join(self.topsrcdir, "build", "mach_virtualenv_packages.txt"),
+ populate_local_paths=False,
+ )
+
+ if manager.up_to_date(sys.executable) and not force:
+ print("virtualenv at %s is already up to date." % virtualenv_path)
+ else:
+ manager.build(sys.executable)
+
+ manager.install_pip_requirements(
+ os.path.join(self.topsrcdir, "build", "zstandard_requirements.txt")
+ )
+
+ try:
+ # `mach` can handle it perfectly fine if `psutil` is missing, so
+ # there's no reason to freak out in this case.
+ manager.install_pip_requirements(
+ os.path.join(self.topsrcdir, "build", "psutil_requirements.txt")
+ )
+ except subprocess.CalledProcessError:
+ print(
+ "Could not install psutil, so telemetry will be missing some "
+ "data. Continuing."
+ )
+
+ if not PY2:
+ # This can fail on some platforms. See
+ # https://bugzilla.mozilla.org/show_bug.cgi?id=1660120
+ try:
+ manager.install_pip_requirements(
+ os.path.join(self.topsrcdir, "build", "glean_requirements.txt")
+ )
+ except subprocess.CalledProcessError:
+ print(
+ "Could not install glean_sdk, so telemetry will not be "
+ "collected. Continuing."
+ )
+ print("Python 3 mach environment created.")
+ python2, _ = find_python2_executable()
+ if not python2:
+ print(
+ "WARNING! Could not find a Python 2 executable to create "
+ "a Python 2 virtualenv",
+ file=sys.stderr,
+ )
+ return 0
+ args = [
+ python2,
+ os.path.join(self.topsrcdir, "mach"),
+ "create-mach-environment",
+ ]
+ if force:
+ args.append("-f")
+ ret = subprocess.call(args)
+ if ret:
+ print(
+ "WARNING! Failed to create a Python 2 mach environment.",
+ file=sys.stderr,
+ )
+ else:
+ print("Python 2 mach environment created.")