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.py2941
1 files changed, 2941 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..2297d586b8
--- /dev/null
+++ b/python/mozbuild/mozbuild/mach_commands.py
@@ -0,0 +1,2941 @@
+# 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 errno
+import itertools
+import json
+import logging
+import operator
+import os
+import os.path
+import platform
+import re
+import shutil
+import subprocess
+import sys
+import tempfile
+import time
+from os import path
+from pathlib import Path
+
+import mozpack.path as mozpath
+import yaml
+from mach.decorators import (
+ Command,
+ CommandArgument,
+ CommandArgumentGroup,
+ SettingsProvider,
+ SubCommand,
+)
+from voluptuous import All, Boolean, Required, Schema
+
+import mozbuild.settings # noqa need @SettingsProvider hook to execute
+from mozbuild.base import (
+ BinaryNotFoundException,
+ BuildEnvironmentNotFoundException,
+ MozbuildObject,
+)
+from mozbuild.base import MachCommandConditions as conditions
+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)
+
+
+@Command(
+ "watch",
+ category="post-build",
+ description="Watch and re-build (parts of) the tree.",
+ conditions=[conditions.is_firefox],
+ virtualenv_name="watch",
+)
+@CommandArgument(
+ "-v",
+ "--verbose",
+ action="store_true",
+ help="Verbose output for what commands the watcher is running.",
+)
+def watch(command_context, verbose=False):
+ """Watch and re-build (parts of) the source tree."""
+ if not conditions.is_artifact_build(command_context):
+ print(
+ "WARNING: mach watch only rebuilds the `mach build faster` parts of the tree!"
+ )
+
+ if not command_context.substs.get("WATCHMAN", None):
+ print(
+ "mach watch requires watchman to be installed and found at configure time. 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(command_context.config_environment)
+
+ try:
+ return daemon.watch()
+ except KeyboardInterrupt:
+ # Suppress ugly stack trace when user hits Ctrl-C.
+ sys.exit(3)
+
+
+CARGO_CONFIG_NOT_FOUND_ERROR_MSG = """\
+The sub-command {subcommand} is not currently configured to be used with ./mach cargo.
+To do so, add the corresponding file in <mozilla-root-dir>/build/cargo, following other examples in this directory"""
+
+
+def _cargo_config_yaml_schema():
+ def starts_with_cargo(s):
+ if s.startswith("cargo-"):
+ return s
+ else:
+ raise ValueError
+
+ return Schema(
+ {
+ # The name of the command (not checked for now, but maybe
+ # later)
+ Required("command"): All(str, starts_with_cargo),
+ # Whether `make` should stop immediately in case
+ # of error returned by the command. Default: False
+ "continue_on_error": Boolean,
+ # Whether this command requires pre_export and export build
+ # targets to have run. Defaults to bool(cargo_build_flags).
+ "requires_export": Boolean,
+ # Build flags to use. If this variable is not
+ # defined here, the build flags are generated automatically and are
+ # the same as for `cargo build`. See available substitutions at the
+ # end.
+ "cargo_build_flags": [str],
+ # Extra build flags to use. These flags are added
+ # after the cargo_build_flags both when they are provided or
+ # automatically generated. See available substitutions at the end.
+ "cargo_extra_flags": [str],
+ # Available substitutions for `cargo_*_flags`:
+ # * {arch}: architecture target
+ # * {crate}: current crate name
+ # * {directory}: Directory of the current crate within the source tree
+ # * {features}: Rust features (for `--features`)
+ # * {manifest}: full path of `Cargo.toml` file
+ # * {target}: `--lib` for library, `--bin CRATE` for executables
+ # * {topsrcdir}: Top directory of sources
+ }
+ )
+
+
+@Command(
+ "cargo",
+ category="build",
+ description="Run `cargo <cargo_command>` on a given crate. Defaults to gkrust.",
+ metrics_path=MOZBUILD_METRICS_PATH,
+)
+@CommandArgument(
+ "cargo_command",
+ default=None,
+ help="Target to cargo, must be one of the commands in config/cargo/",
+)
+@CommandArgument(
+ "--all-crates",
+ action="store_true",
+ help="Check all of the crates in the tree.",
+)
+@CommandArgument(
+ "-p", "--package", default=None, help="The specific crate name to check."
+)
+@CommandArgument(
+ "--jobs",
+ "-j",
+ default="0",
+ nargs="?",
+ metavar="jobs",
+ type=int,
+ help="Run the tests in parallel using multiple processes.",
+)
+@CommandArgument("-v", "--verbose", action="store_true", help="Verbose output.")
+@CommandArgument(
+ "--message-format-json",
+ action="store_true",
+ help="Emit error messages as JSON.",
+)
+@CommandArgument(
+ "--continue-on-error",
+ action="store_true",
+ help="Do not return an error exit code if the subcommands errors out.",
+)
+@CommandArgument(
+ "subcommand_args",
+ nargs=argparse.REMAINDER,
+ help="These arguments are passed as-is to the cargo subcommand.",
+)
+def cargo(
+ command_context,
+ cargo_command,
+ all_crates=None,
+ package=None,
+ jobs=0,
+ verbose=False,
+ message_format_json=False,
+ continue_on_error=False,
+ subcommand_args=[],
+):
+
+ from mozbuild.controller.building import BuildDriver
+
+ command_context.log_manager.enable_all_structured_loggers()
+
+ topsrcdir = Path(mozpath.normpath(command_context.topsrcdir))
+ cargodir = Path(topsrcdir / "build" / "cargo")
+
+ cargo_command_basename = "cargo-" + cargo_command + ".yaml"
+ cargo_command_fullname = Path(cargodir / cargo_command_basename)
+ if path.exists(cargo_command_fullname):
+ with open(cargo_command_fullname) as fh:
+ yaml_config = yaml.load(fh, Loader=yaml.FullLoader)
+ schema = _cargo_config_yaml_schema()
+ schema(yaml_config)
+ if not yaml_config:
+ yaml_config = {}
+ else:
+ print(CARGO_CONFIG_NOT_FOUND_ERROR_MSG.format(subcommand=cargo_command))
+ return 1
+
+ # print("yaml_config = ", yaml_config)
+
+ yaml_config.setdefault("continue_on_error", False)
+ continue_on_error = continue_on_error or yaml_config["continue_on_error"] is True
+
+ cargo_build_flags = yaml_config.get("cargo_build_flags")
+ if cargo_build_flags is not None:
+ cargo_build_flags = " ".join(cargo_build_flags)
+ cargo_extra_flags = yaml_config.get("cargo_extra_flags")
+ if cargo_extra_flags is not None:
+ cargo_extra_flags = " ".join(cargo_extra_flags)
+ requires_export = yaml_config.get("requires_export", bool(cargo_build_flags))
+
+ ret = 0
+ if requires_export:
+ # This directory is created during export. If it's not there,
+ # export hasn't run already.
+ deps = Path(command_context.topobjdir) / ".deps"
+ if not deps.exists():
+ build = command_context._spawn(BuildDriver)
+ ret = build.build(
+ command_context.metrics,
+ what=["pre-export", "export"],
+ jobs=jobs,
+ verbose=verbose,
+ mach_context=command_context._mach_context,
+ )
+ else:
+ try:
+ command_context.config_environment
+ except BuildEnvironmentNotFoundException:
+ build = command_context._spawn(BuildDriver)
+ ret = build.configure(
+ command_context.metrics,
+ buildstatus_messages=False,
+ )
+ if ret != 0:
+ return ret
+
+ # XXX duplication with `mach vendor rust`
+ crates_and_roots = {
+ "gkrust": {"directory": "toolkit/library/rust", "library": True},
+ "gkrust-gtest": {"directory": "toolkit/library/gtest/rust", "library": True},
+ "geckodriver": {"directory": "testing/geckodriver", "library": False},
+ }
+
+ if all_crates:
+ crates = crates_and_roots.keys()
+ elif package:
+ crates = [package]
+ else:
+ crates = ["gkrust"]
+
+ if subcommand_args:
+ subcommand_args = " ".join(subcommand_args)
+
+ for crate in crates:
+ crate_info = crates_and_roots.get(crate, None)
+ if not crate_info:
+ print(
+ "Cannot locate crate %s. Please check your spelling or "
+ "add the crate information to the list." % crate
+ )
+ return 1
+
+ targets = [
+ "force-cargo-library-%s" % cargo_command,
+ "force-cargo-host-library-%s" % cargo_command,
+ "force-cargo-program-%s" % cargo_command,
+ "force-cargo-host-program-%s" % cargo_command,
+ ]
+
+ directory = crate_info["directory"]
+ # you can use these variables in 'cargo_build_flags'
+ subst = {
+ "arch": '"$(RUST_TARGET)"',
+ "crate": crate,
+ "directory": directory,
+ "features": '"$(RUST_LIBRARY_FEATURES)"',
+ "manifest": str(Path(topsrcdir / directory / "Cargo.toml")),
+ "target": "--lib" if crate_info["library"] else "--bin " + crate,
+ "topsrcdir": str(topsrcdir),
+ }
+
+ if subcommand_args:
+ targets = targets + [
+ "cargo_extra_cli_flags=%s" % (subcommand_args.format(**subst))
+ ]
+ if cargo_build_flags:
+ targets = targets + [
+ "cargo_build_flags=%s" % (cargo_build_flags.format(**subst))
+ ]
+
+ append_env = {}
+ if cargo_extra_flags:
+ append_env["CARGO_EXTRA_FLAGS"] = cargo_extra_flags.format(**subst)
+ if message_format_json:
+ append_env["USE_CARGO_JSON_MESSAGE_FORMAT"] = "1"
+ if continue_on_error:
+ append_env["CARGO_CONTINUE_ON_ERROR"] = "1"
+ if cargo_build_flags:
+ append_env["CARGO_NO_AUTO_ARG"] = "1"
+ else:
+ append_env[
+ "ADD_RUST_LTOABLE"
+ ] = "force-cargo-library-{s:s} force-cargo-program-{s:s}".format(
+ s=cargo_command
+ )
+
+ ret = command_context._run_make(
+ srcdir=False,
+ directory=directory,
+ ensure_exit_code=0,
+ silent=not verbose,
+ print_directory=False,
+ target=targets,
+ num_jobs=jobs,
+ append_env=append_env,
+ )
+ if ret != 0:
+ return ret
+
+ return 0
+
+
+@SubCommand(
+ "cargo",
+ "vet",
+ description="Run `cargo vet`.",
+)
+@CommandArgument("arguments", nargs=argparse.REMAINDER)
+def cargo_vet(command_context, arguments, stdout=None, env=os.environ):
+ from mozbuild.bootstrap import bootstrap_toolchain
+
+ # Logging of commands enables logging from `bootstrap_toolchain` that we
+ # don't want to expose. Disable them temporarily.
+ logger = logging.getLogger("gecko_taskgraph.generator")
+ level = logger.getEffectiveLevel()
+ logger.setLevel(logging.ERROR)
+
+ env = env.copy()
+ cargo_vet = bootstrap_toolchain("cargo-vet")
+ if cargo_vet:
+ env["PATH"] = os.pathsep.join([cargo_vet, env["PATH"]])
+ logger.setLevel(level)
+ try:
+ cargo = command_context.substs["CARGO"]
+ except (BuildEnvironmentNotFoundException, KeyError):
+ # Default if this tree isn't configured.
+ from mozfile import which
+
+ cargo = which("cargo", path=env["PATH"])
+ if not cargo:
+ raise OSError(
+ errno.ENOENT,
+ (
+ "Could not find 'cargo' on your $PATH. "
+ "Hint: have you run `mach build` or `mach configure`?"
+ ),
+ )
+
+ locked = "--locked" in arguments
+ if locked:
+ # The use of --locked requires .cargo/config to exist, but other things,
+ # like cargo update, don't want it there, so remove it once we're done.
+ topsrcdir = Path(command_context.topsrcdir)
+ shutil.copyfile(
+ topsrcdir / ".cargo" / "config.in", topsrcdir / ".cargo" / "config"
+ )
+
+ try:
+ res = subprocess.run(
+ [cargo, "vet"] + arguments,
+ cwd=command_context.topsrcdir,
+ stdout=stdout,
+ env=env,
+ )
+ finally:
+ if locked:
+ (topsrcdir / ".cargo" / "config").unlink()
+
+ # When the function is invoked without stdout set (the default when running
+ # as a mach subcommand), exit with the returncode from cargo vet.
+ # When the function is invoked with stdout (direct function call), return
+ # the full result from subprocess.run.
+ return res if stdout else res.returncode
+
+
+@Command(
+ "doctor",
+ category="devenv",
+ description="Diagnose and fix common development environment issues.",
+)
+@CommandArgument(
+ "--fix",
+ default=False,
+ action="store_true",
+ help="Attempt to fix found problems.",
+)
+@CommandArgument(
+ "--verbose",
+ default=False,
+ action="store_true",
+ help="Print verbose information found by checks.",
+)
+def doctor(command_context, fix=False, verbose=False):
+ """Diagnose common build environment problems"""
+ from mozbuild.doctor import run_doctor
+
+ return run_doctor(
+ topsrcdir=command_context.topsrcdir,
+ topobjdir=command_context.topobjdir,
+ configure_args=command_context.mozconfig["configure_args"],
+ fix=fix,
+ verbose=verbose,
+ )
+
+
+CLOBBER_CHOICES = {"objdir", "python", "gradle"}
+
+
+@Command(
+ "clobber",
+ category="build",
+ description="Clobber the tree (delete the object directory).",
+ no_auto_log=True,
+)
+@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(command_context, 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 Python's generated files (virtualenvs,
+ ".pyc", "__pycache__", etc).
+
+ 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 - CLOBBER_CHOICES
+ if invalid:
+ print(
+ "Unknown clobber target(s): {}. Choose from {{{}}}".format(
+ ", ".join(invalid), ", ".join(CLOBBER_CHOICES)
+ )
+ )
+ return 1
+
+ ret = 0
+ if "objdir" in what:
+ from mozbuild.controller.clobber import Clobberer
+
+ try:
+ substs = command_context.substs
+ except BuildEnvironmentNotFoundException:
+ substs = {}
+
+ try:
+ Clobberer(
+ command_context.topsrcdir, command_context.topobjdir, substs
+ ).remove_objdir(full)
+ except OSError as e:
+ if sys.platform.startswith("win"):
+ if isinstance(e, WindowsError) and e.winerror in (5, 32):
+ command_context.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(command_context):
+ cmd = [
+ "hg",
+ "--config",
+ "extensions.purge=",
+ "purge",
+ "--all",
+ "-I",
+ "glob:**.py[cdo]",
+ "-I",
+ "glob:**/__pycache__",
+ ]
+ elif conditions.is_git(command_context):
+ cmd = ["git", "clean", "-d", "-f", "-x", "*.py[cdo]", "*/__pycache__/*"]
+ else:
+ cmd = ["find", ".", "-type", "f", "-name", "*.py[cdo]", "-delete"]
+ subprocess.call(cmd, cwd=command_context.topsrcdir)
+ cmd = [
+ "find",
+ ".",
+ "-type",
+ "d",
+ "-name",
+ "__pycache__",
+ "-empty",
+ "-delete",
+ ]
+ ret = subprocess.call(cmd, cwd=command_context.topsrcdir)
+ shutil.rmtree(
+ mozpath.join(command_context.topobjdir, "_virtualenvs"),
+ ignore_errors=True,
+ )
+
+ if "gradle" in what:
+ shutil.rmtree(
+ mozpath.join(command_context.topobjdir, "gradle"), ignore_errors=True
+ )
+
+ return ret
+
+
+@Command(
+ "show-log", category="post-build", description="Display mach logs", no_auto_log=True
+)
+@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(command_context, log_file=None):
+ """Show mach logs
+ If we're in a terminal context, the log is piped to 'less'
+ for more convenient viewing.
+ (https://man7.org/linux/man-pages/man1/less.1.html)
+ """
+ if not log_file:
+ path = command_context._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["LESS"] = "FRX"
+ less = subprocess.Popen(
+ ["less"], stdin=subprocess.PIPE, env=env, encoding="UTF-8"
+ )
+
+ try:
+ # Create a new logger handler with the stream being the stdin of our 'less'
+ # process so that we can pipe the logger output into 'less'
+ less_handler = logging.StreamHandler(stream=less.stdin)
+ less_handler.setFormatter(
+ command_context.log_manager.terminal_handler.formatter
+ )
+ less_handler.setLevel(command_context.log_manager.terminal_handler.level)
+
+ # replace the existing terminal handler with the new one for 'less' while
+ # still keeping the original one to set back later
+ original_handler = command_context.log_manager.replace_terminal_handler(
+ less_handler
+ )
+
+ # Save this value so we can set it back to the original value later
+ original_logging_raise_exceptions = logging.raiseExceptions
+
+ # We need to explicitly disable raising exceptions inside logging so
+ # that we can catch them here ourselves to ignore the ones we want
+ logging.raiseExceptions = False
+
+ # Parses the log file line by line and streams
+ # (to less.stdin) the relevant records we want
+ handle_log_file(command_context, log_file)
+
+ # At this point we've piped the entire log file to
+ # 'less', so we can close the input stream
+ less.stdin.close()
+
+ # Wait for the user to manually terminate `less`
+ less.wait()
+ except OSError as os_error:
+ # (POSIX) errno.EPIPE: BrokenPipeError: [Errno 32] Broken pipe
+ # (Windows) errno.EINVAL: OSError: [Errno 22] Invalid argument
+ if os_error.errno == errno.EPIPE or os_error.errno == errno.EINVAL:
+ # If the user manually terminates 'less' before the entire log file
+ # is piped (without scrolling close enough to the bottom) we will get
+ # one of these errors (depends on the OS) because the logger will still
+ # attempt to stream to the now invalid less.stdin. To prevent a bunch
+ # of errors being shown after a user terminates 'less', we just catch
+ # the first of those exceptions here, and stop parsing the log file.
+ pass
+ else:
+ raise
+ except Exception:
+ raise
+ finally:
+ # Ensure these values are changed back to the originals, regardless of outcome
+ command_context.log_manager.replace_terminal_handler(original_handler)
+ logging.raiseExceptions = original_logging_raise_exceptions
+ else:
+ # Not in a terminal context, so just handle the log file with the
+ # default stream without piping it to a pager (less)
+ handle_log_file(command_context, log_file)
+
+
+def handle_log_file(command_context, log_file):
+ start_time = 0
+ for line in log_file:
+ created, action, params = json.loads(line)
+ if not start_time:
+ start_time = created
+ command_context.log_manager.terminal_handler.formatter.start_time = created
+ if "line" in params:
+ record = logging.makeLogRecord(
+ {
+ "created": created,
+ "name": command_context._logger.name,
+ "levelno": logging.INFO,
+ "msg": "{line}",
+ "params": params,
+ "action": action,
+ }
+ )
+ command_context._logger.handle(record)
+
+
+# Provide commands for inspecting warnings.
+
+
+def database_path(command_context):
+ return command_context._get_state_filename("warnings.json")
+
+
+def get_warnings_database(command_context):
+ from mozbuild.compilation.warnings import WarningsDatabase
+
+ path = database_path(command_context)
+
+ 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(command_context, directory=None, report=None):
+ database = get_warnings_database(command_context)
+
+ if directory:
+ dirpath = join_ensure_dir(command_context.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_warnings(command_context, directory=None, flags=None, report=None):
+ database = get_warnings_database(command_context)
+
+ by_name = sorted(database.warnings)
+
+ topsrcdir = mozpath.normpath(command_context.topsrcdir)
+
+ if directory:
+ directory = mozpath.normsep(directory)
+ dirpath = 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(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
+
+
+@Command("gtest", category="testing", description="Run GTest unit tests (C++ tests).")
+@CommandArgument(
+ "gtest_filter",
+ default="*",
+ 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)."
+ "Test names are of the format SUITE.NAME. Use --list-tests to see all.",
+)
+@CommandArgument("--list-tests", action="store_true", help="list all available tests")
+@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_runner",
+ 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(
+ command_context,
+ shuffle,
+ jobs,
+ gtest_filter,
+ list_tests,
+ 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:
+ command_context.config_environment
+ except Exception:
+ print("Please run |./mach build| before |./mach gtest|.")
+ return 1
+
+ res = command_context._mach_context.commands.dispatch(
+ "build", command_context._mach_context, what=["recurse_gtest"]
+ )
+ if res:
+ print("Could not build xul-gtest")
+ return res
+
+ if command_context.substs.get("MOZ_WIDGET_TOOLKIT") == "cocoa":
+ command_context._run_make(
+ directory="browser/app", target="repackage", ensure_exit_code=True
+ )
+
+ cwd = os.path.join(command_context.topobjdir, "_tests", "gtest")
+
+ if not os.path.isdir(cwd):
+ os.makedirs(cwd)
+
+ if conditions.is_android(command_context):
+ 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 android_gtest(
+ command_context,
+ cwd,
+ shuffle,
+ gtest_filter,
+ package,
+ adb_path,
+ device_serial,
+ remote_test_root,
+ libxul_path,
+ 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 = command_context.get_binary_path("app")
+ args = [app_path, "-unittest", "--gtest_death_test_style=threadsafe"]
+
+ if (
+ sys.platform.startswith("win")
+ and "MOZ_LAUNCHER_PROCESS" in command_context.defines
+ ):
+ args.append("--wait-for-browser")
+
+ if list_tests:
+ args.append("--gtest_list_tests")
+
+ if debug or debugger or debugger_args:
+ args = _prepend_debugger_args(args, debugger, debugger_args)
+ if not args:
+ return 1
+
+ # Use GTest environment variable to control test execution
+ # For details see:
+ # https://google.github.io/googletest/advanced.html#running-test-programs-advanced-options
+ gtest_env = {"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(command_context.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["MOZ_RUN_GTEST"] = "True"
+
+ if shuffle:
+ gtest_env["GTEST_SHUFFLE"] = "True"
+
+ if tbpl_parser:
+ gtest_env["MOZ_TBPL_PARSER"] = "True"
+
+ if enable_webrender:
+ gtest_env["MOZ_WEBRENDER"] = "1"
+ gtest_env["MOZ_ACCELERATED"] = "1"
+ else:
+ gtest_env["MOZ_WEBRENDER"] = "0"
+
+ if jobs == 1:
+ return command_context.run_process(
+ args=args,
+ append_env=gtest_env,
+ cwd=cwd,
+ ensure_exit_code=False,
+ pass_thru=True,
+ )
+
+ import functools
+
+ from mozprocess import ProcessHandlerMixin
+
+ def handle_line(job_id, line):
+ # Prepend the jobId
+ line = "[%d] %s" % (job_id + 1, line.strip())
+ command_context.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(
+ command_context,
+ test_dir,
+ shuffle,
+ gtest_filter,
+ package,
+ adb_path,
+ device_serial,
+ remote_test_root,
+ libxul_path,
+ install,
+):
+ # setup logging for mozrunner
+ from mozlog.commandline import setup_logging
+
+ format_args = {"level": command_context._mach_context.settings["test"]["level"]}
+ default_format = command_context._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 get_adb_path, verify_android_device
+
+ verify_android_device(
+ command_context, install=install, app=package, device_serial=device_serial
+ )
+
+ if not adb_path:
+ adb_path = get_adb_path(command_context)
+ if not libxul_path:
+ libxul_path = os.path.join(
+ command_context.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,
+ ):
+ exit_code = 1
+ tester.cleanup()
+
+ return exit_code
+
+
+@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(command_context, verbose=False):
+ """Package the built product for distribution."""
+ ret = command_context._run_make(
+ directory=".", target="package", silent=not verbose, ensure_exit_code=False
+ )
+ if ret == 0:
+ command_context.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.",
+ )
+ parser.add_argument(
+ "--aab",
+ action="store_true",
+ help="Install as AAB (Android App Bundle)",
+ )
+ 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()
+
+
+@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(command_context, **kwargs):
+ """Install a package."""
+ if conditions.is_android(command_context):
+ from mozrunner.devices.android_device import (
+ InstallIntent,
+ verify_android_device,
+ )
+
+ ret = (
+ verify_android_device(command_context, install=InstallIntent.YES, **kwargs)
+ == 0
+ )
+ else:
+ ret = command_context._run_make(
+ directory=".", target="install", ensure_exit_code=False
+ )
+
+ if ret == 0:
+ command_context.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()
+ group = parser.add_argument_group("The compiled program")
+ group.add_argument(
+ "--app",
+ default="org.mozilla.geckoview_example",
+ help="Android package to run (default: org.mozilla.geckoview_example)",
+ )
+ group.add_argument(
+ "--intent",
+ default="android.intent.action.VIEW",
+ help="Android intent action to launch with "
+ "(default: android.intent.action.VIEW)",
+ )
+ group.add_argument(
+ "--setenv",
+ dest="env",
+ action="append",
+ default=[],
+ help="Set target environment variable, like FOO=BAR",
+ )
+ group.add_argument(
+ "--profile",
+ "-P",
+ default=None,
+ help="Path to Gecko profile, like /path/to/host/profile "
+ "or /path/to/target/profile",
+ )
+ group.add_argument("--url", default=None, help="URL to open")
+ group.add_argument(
+ "--aab",
+ action="store_true",
+ default=False,
+ help="Install app ass App Bundle (AAB).",
+ )
+ group.add_argument(
+ "--no-install",
+ action="store_true",
+ default=False,
+ help="Do not try to install application on device before running "
+ "(default: False)",
+ )
+ group.add_argument(
+ "--no-wait",
+ action="store_true",
+ default=False,
+ help="Do not wait for application to start before returning "
+ "(default: False)",
+ )
+ group.add_argument(
+ "--enable-fission",
+ action="store_true",
+ help="Run the program with Fission (site isolation) enabled.",
+ )
+ group.add_argument(
+ "--fail-if-running",
+ action="store_true",
+ default=False,
+ help="Fail if application is already running (default: False)",
+ )
+ group.add_argument(
+ "--restart",
+ action="store_true",
+ default=False,
+ help="Stop the application if it is already running (default: False)",
+ )
+
+ group = parser.add_argument_group("Debugging")
+ group.add_argument("--debug", action="store_true", help="Enable the lldb debugger.")
+ group.add_argument(
+ "--debugger",
+ default=None,
+ type=str,
+ help="Name of lldb compatible 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(
+ "--no-attach",
+ action="store_true",
+ default=False,
+ help="Start the debugging servers on the device but do not "
+ "attach any debuggers.",
+ )
+ group.add_argument(
+ "--use-existing-process",
+ action="store_true",
+ default=False,
+ help="Select an existing process to debug.",
+ )
+ 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(
+ "--disable-fission",
+ action="store_true",
+ help="Run the program with Fission (site isolation) disabled.",
+ )
+ 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()
+
+
+@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(command_context, **kwargs):
+ """Run the compiled program."""
+ if conditions.is_android(command_context):
+ return _run_android(command_context, **kwargs)
+ if conditions.is_jsshell(command_context):
+ return _run_jsshell(command_context, **kwargs)
+ return _run_desktop(command_context, **kwargs)
+
+
+def _run_android(
+ command_context,
+ app="org.mozilla.geckoview_example",
+ intent=None,
+ env=[],
+ profile=None,
+ url=None,
+ aab=False,
+ no_install=None,
+ no_wait=None,
+ fail_if_running=None,
+ restart=None,
+ enable_fission=False,
+ debug=False,
+ debugger=None,
+ debugger_args=None,
+ no_attach=False,
+ use_existing_process=False,
+):
+ from mozrunner.devices.android_device import (
+ InstallIntent,
+ _get_device,
+ verify_android_device,
+ )
+ 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_runner":
+ activity_name = "org.mozilla.geckoview.test_runner.TestRunnerActivity"
+ elif "fennec" in app or "firefox" in app:
+ activity_name = "org.mozilla.gecko.BrowserApp"
+ else:
+ raise RuntimeError("Application not recognized: {}".format(app))
+
+ # If we want to debug an existing process, we implicitly do not want
+ # to kill it and pave over its installation with a new one.
+ if debug and use_existing_process:
+ no_install = True
+
+ # `verify_android_device` respects `DEVICE_SERIAL` if it is set and sets it otherwise.
+ verify_android_device(
+ command_context,
+ app=app,
+ aab=aab,
+ debugger=debug,
+ 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(command_context.substs, device_serial=device_serial)
+
+ if debug:
+ # This will terminate any existing processes, so we skip it when we
+ # want to attach to an existing one.
+ if not use_existing_process:
+ command_context.log(
+ logging.INFO,
+ "run",
+ {"app": app},
+ "Setting {app} as the device debug app",
+ )
+ device.shell("am set-debug-app -w --persistent %s" % app)
+ else:
+ # Make sure that the app doesn't block waiting for jdb
+ device.shell("am clear-debug-app")
+
+ if not debug or not use_existing_process:
+ 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)
+ command_context.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
+ command_context.log(
+ logging.INFO,
+ "run",
+ {"target_profile": target_profile},
+ 'Using profile from target "{target_profile}"',
+ )
+
+ args = ["--profile", shlex_quote(target_profile)]
+
+ # FIXME: When android switches to using Fission by default,
+ # MOZ_FORCE_DISABLE_FISSION will need to be configured correctly.
+ 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
+ command_context.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.
+ command_context.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,
+ )
+
+ if not debug:
+ return 0
+
+ from mozrunner.devices.android_device import run_lldb_server
+
+ socket_file = run_lldb_server(app, command_context.substs, device_serial)
+ if not socket_file:
+ command_context.log(
+ logging.ERROR,
+ "run",
+ {"msg": "Failed to obtain a socket file!"},
+ "{msg}",
+ )
+ return 1
+
+ # Give lldb-server a chance to start
+ command_context.log(
+ logging.INFO,
+ "run",
+ {"msg": "Pausing to ensure lldb-server has started..."},
+ "{msg}",
+ )
+ time.sleep(1)
+
+ if use_existing_process:
+
+ def _is_geckoview_process(proc_name, pkg_name):
+ if not proc_name.startswith(pkg_name):
+ # Definitely not our package
+ return False
+ if len(proc_name) == len(pkg_name):
+ # Parent process from our package
+ return True
+ if proc_name[len(pkg_name)] == ":":
+ # Child process from our package
+ return True
+ # Process name is a prefix of our package name
+ return False
+
+ # If we're going to attach to an existing process, we need to know
+ # who we're attaching to. Obtain a list of all processes associated
+ # with our desired app.
+ proc_list = [
+ proc[:-1]
+ for proc in device.get_process_list()
+ if _is_geckoview_process(proc[1], app)
+ ]
+
+ if not proc_list:
+ command_context.log(
+ logging.ERROR,
+ "run",
+ {"app": app},
+ "No existing {app} processes found",
+ )
+ return 1
+ elif len(proc_list) == 1:
+ pid = proc_list[0][0]
+ else:
+ # Prompt the user to determine which process we should use
+ entries = [
+ "%2d: %6d %s" % (n, p[0], p[1])
+ for n, p in enumerate(proc_list, start=1)
+ ]
+ prompt = "\n".join(["\nPlease select a process:\n"] + entries) + "\n\n"
+ valid_range = range(1, len(proc_list) + 1)
+
+ while True:
+ response = int(input(prompt).strip())
+ if response in valid_range:
+ break
+ command_context.log(
+ logging.ERROR, "run", {"msg": "Invalid response"}, "{msg}"
+ )
+ pid = proc_list[response - 1][0]
+ else:
+ # We're not using an existing process, so there should only be our
+ # parent process at this time.
+ pids = device.pidof(app_name=app)
+ if len(pids) != 1:
+ command_context.log(
+ logging.ERROR,
+ "run",
+ {"msg": "Not sure which pid to attach to!"},
+ "{msg}",
+ )
+ return 1
+ pid = pids[0]
+
+ command_context.log(
+ logging.INFO, "run", {"pid": str(pid)}, "Debuggee pid set to {pid}..."
+ )
+
+ lldb_connect_url = "unix-abstract-connect://" + socket_file
+ local_jdb_port = device.forward("tcp:0", "jdwp:%d" % pid)
+
+ if no_attach:
+ command_context.log(
+ logging.INFO,
+ "run",
+ {"pid": str(pid), "url": lldb_connect_url},
+ "To debug native code, connect lldb to {url} and attach to pid {pid}",
+ )
+ command_context.log(
+ logging.INFO,
+ "run",
+ {"port": str(local_jdb_port)},
+ "To debug Java code, connect jdb using tcp to localhost:{port}",
+ )
+ return 0
+
+ # Beyond this point we want to be able to automatically clean up after ourselves,
+ # so we enter the following try block.
+ try:
+ command_context.log(
+ logging.INFO, "run", {"msg": "Starting debugger..."}, "{msg}"
+ )
+
+ if not use_existing_process:
+ # The app is waiting for jdb to attach and will not continue running
+ # until we do so.
+ def _jdb_ping(local_jdb_port):
+ jdb_process = subprocess.Popen(
+ ["jdb", "-attach", "localhost:%d" % local_jdb_port],
+ stdin=subprocess.PIPE,
+ stdout=subprocess.DEVNULL,
+ stderr=subprocess.DEVNULL,
+ encoding="utf-8",
+ )
+ # Wait a bit to provide enough time for jdb and lldb to connect
+ # to the debuggee
+ time.sleep(5)
+ # NOTE: jdb cannot detach while the debuggee is frozen in lldb,
+ # so its process might not necessarily exit immediately once the
+ # quit command has been issued.
+ jdb_process.communicate(input="quit\n")
+
+ # We run this in the background while lldb attaches in the foreground
+ from threading import Thread
+
+ jdb_thread = Thread(target=_jdb_ping, args=[local_jdb_port])
+ jdb_thread.start()
+
+ LLDBINIT = """
+settings set target.inline-breakpoint-strategy always
+settings append target.exec-search-paths {obj_xul}
+settings append target.exec-search-paths {obj_mozglue}
+settings append target.exec-search-paths {obj_nss}
+platform select remote-android
+platform connect {connect_url}
+process attach {continue_flag}-p {pid!s}
+""".lstrip()
+
+ obj_xul = os.path.join(command_context.topobjdir, "toolkit", "library", "build")
+ obj_mozglue = os.path.join(command_context.topobjdir, "mozglue", "build")
+ obj_nss = os.path.join(command_context.topobjdir, "security")
+
+ if use_existing_process:
+ continue_flag = ""
+ else:
+ # Tell lldb to continue after attaching; instead we'll break at
+ # the initial SEGVHandler, similarly to how things work when we
+ # attach using Android Studio. Doing this gives Android a chance
+ # to dismiss the "Waiting for Debugger" dialog.
+ continue_flag = "-c "
+
+ try:
+ # Write out our lldb startup commands to a temp file. We'll pass its
+ # name to lldb on its command line.
+ with tempfile.NamedTemporaryFile(
+ mode="wt", encoding="utf-8", newline="\n", delete=False
+ ) as tmp:
+ tmp_lldb_start_script = tmp.name
+ tmp.write(
+ LLDBINIT.format(
+ obj_xul=obj_xul,
+ obj_mozglue=obj_mozglue,
+ obj_nss=obj_nss,
+ connect_url=lldb_connect_url,
+ continue_flag=continue_flag,
+ pid=pid,
+ )
+ )
+
+ our_debugger_args = "-s %s" % tmp_lldb_start_script
+ if debugger_args:
+ full_debugger_args = " ".join([debugger_args, our_debugger_args])
+ else:
+ full_debugger_args = our_debugger_args
+
+ args = _prepend_debugger_args([], debugger, full_debugger_args)
+ if not args:
+ return 1
+
+ return command_context.run_process(
+ args=args, ensure_exit_code=False, pass_thru=True
+ )
+ finally:
+ os.remove(tmp_lldb_start_script)
+ finally:
+ device.remove_forwards("tcp:%d" % local_jdb_port)
+ device.shell("pkill -f lldb-server", enable_run_as=True)
+ if not use_existing_process:
+ device.shell("am clear-debug-app")
+
+
+def _run_jsshell(command_context, params, debug, debugger, debugger_args):
+ try:
+ binpath = command_context.get_binary_path("app")
+ except BinaryNotFoundException as e:
+ command_context.log(logging.ERROR, "run", {"error": str(e)}, "ERROR: {error}")
+ command_context.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:
+ command_context.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:
+ 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 1
+
+ # Prepend the debugger args.
+ args = [debuggerInfo.path] + debuggerInfo.args + args
+
+ return command_context.run_process(
+ args=args, ensure_exit_code=False, pass_thru=True, append_env=extra_env
+ )
+
+
+def _run_desktop(
+ command_context,
+ params,
+ packaged,
+ app,
+ remote,
+ background,
+ noprofile,
+ disable_e10s,
+ enable_crash_reporter,
+ disable_fission,
+ setpref,
+ temp_profile,
+ macos_open,
+ debug,
+ debugger,
+ debugger_args,
+ dmd,
+ mode,
+ stacks,
+ show_dump_stats,
+):
+ from mozprofile import Preferences, Profile
+
+ try:
+ if packaged:
+ binpath = command_context.get_binary_path(where="staged-package")
+ else:
+ binpath = app or command_context.get_binary_path("app")
+ except BinaryNotFoundException as e:
+ command_context.log(logging.ERROR, "run", {"error": str(e)}, "ERROR: {error}")
+ if packaged:
+ command_context.log(
+ logging.INFO,
+ "run",
+ {
+ "help": "It looks like your build isn't packaged. "
+ "You can run |./mach package| to package it."
+ },
+ "{help}",
+ )
+ else:
+ command_context.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 command_context.defines
+ ):
+ args.append("-wait-for-browser")
+
+ no_profile_option_given = all(
+ p not in params for p in ["-profile", "--profile", "-P"]
+ )
+ no_backgroundtask_mode_option_given = all(
+ p not in params for p in ["-backgroundtask", "--backgroundtask"]
+ )
+ if (
+ no_profile_option_given
+ and no_backgroundtask_mode_option_given
+ and not noprofile
+ ):
+ prefs = {
+ "browser.aboutConfig.showWarning": False,
+ "browser.shell.checkDefaultBrowser": False,
+ "general.warnOnAboutConfig": False,
+ }
+ prefs.update(command_context._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(command_context.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
+
+ 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": command_context.topsrcdir,
+ "MOZ_DEVELOPER_OBJ_DIR": command_context.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(
+ command_context.topsrcdir, "browser", "config", "version.txt"
+ )
+ f = open(version_file, "r")
+ extra_env["MOZ_FORCE_DISABLE_E10S"] = f.read().strip()
+
+ if disable_fission:
+ extra_env["MOZ_FORCE_DISABLE_FISSION"] = "1"
+
+ if some_debugging_option:
+ if "INSIDE_EMACS" in os.environ:
+ command_context.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:
+ 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 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 = [debuggerInfo.path] + 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 command_context.run_process(
+ args=args, ensure_exit_code=False, pass_thru=True, append_env=extra_env
+ )
+
+
+@Command(
+ "buildsymbols",
+ category="post-build",
+ description="Produce a package of Breakpad-format symbols.",
+)
+def buildsymbols(command_context):
+ """Produce a package of debug symbols suitable for use with Breakpad."""
+ return command_context._run_make(
+ directory=".", target="buildsymbols", ensure_exit_code=False
+ )
+
+
+@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(command_context, format, output=None, verbose=False):
+ func = {"pretty": _environment_pretty, "json": _environment_json}[
+ 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(command_context, out, verbose)
+ return func(command_context, sys.stdout, verbose)
+
+
+def _environment_pretty(command_context, out, verbose):
+ state_dir = command_context._mach_context.state_dir
+
+ 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" % command_context._mach_context.cwd, file=out)
+ print("os cwd:\n\t%s" % os.getcwd(), file=out)
+ print("mach directory:\n\t%s" % command_context._mach_context.topdir, file=out)
+ print("state directory:\n\t%s" % state_dir, file=out)
+
+ print("object directory:\n\t%s" % command_context.topobjdir, file=out)
+
+ if command_context.mozconfig["path"]:
+ print("mozconfig path:\n\t%s" % command_context.mozconfig["path"], file=out)
+ if command_context.mozconfig["configure_args"]:
+ print("mozconfig configure args:", file=out)
+ for arg in command_context.mozconfig["configure_args"]:
+ print("\t%s" % arg, file=out)
+
+ if command_context.mozconfig["make_extra"]:
+ print("mozconfig extra make args:", file=out)
+ for arg in command_context.mozconfig["make_extra"]:
+ print("\t%s" % arg, file=out)
+
+ if command_context.mozconfig["make_flags"]:
+ print("mozconfig make flags:", file=out)
+ for arg in command_context.mozconfig["make_flags"]:
+ print("\t%s" % arg, file=out)
+
+ config = None
+
+ try:
+ config = command_context.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(command_context, 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(command_context, cls=EnvironmentEncoder, sort_keys=True, fp=out)
+
+
+@Command(
+ "repackage",
+ category="misc",
+ description="Repackage artifacts into different formats.",
+)
+def repackage(command_context):
+ """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.
+ """
+ print("Usage: ./mach repackage [dmg|pkg|installer|mar] [args...]")
+
+
+@SubCommand(
+ "repackage",
+ "deb",
+ description="Repackage a tar file into a .deb for Linux",
+ virtualenv_name="repackage-deb",
+)
+@CommandArgument(
+ "--input", "-i", type=str, required=True, help="Input tarfile filename"
+)
+@CommandArgument("--output", "-o", type=str, required=True, help="Output .deb filename")
+@CommandArgument("--arch", type=str, required=True, help="One of ['x86', 'x86_64']")
+@CommandArgument(
+ "--version",
+ type=str,
+ required=True,
+ help="The Firefox version used to create the installer",
+)
+@CommandArgument(
+ "--build-number",
+ type=str,
+ required=True,
+ help="The release's build number",
+)
+@CommandArgument(
+ "--templates",
+ type=str,
+ required=True,
+ help="Location of the templates used to generate the debian/ directory files",
+)
+@CommandArgument(
+ "--release-product",
+ type=str,
+ required=True,
+ help="The product being shipped. Used to disambiguate beta/devedition etc.",
+)
+@CommandArgument(
+ "--release-type",
+ type=str,
+ required=True,
+ help="The release being shipped. Used to disambiguate nightly/try etc.",
+)
+def repackage_deb(
+ command_context,
+ input,
+ output,
+ arch,
+ version,
+ build_number,
+ templates,
+ release_product,
+ release_type,
+):
+ if not os.path.exists(input):
+ print("Input file does not exist: %s" % input)
+ return 1
+
+ template_dir = os.path.join(
+ command_context.topsrcdir,
+ templates,
+ )
+
+ from fluent.runtime.fallback import FluentLocalization, FluentResourceLoader
+
+ from mozbuild.repackaging.deb import repackage_deb
+
+ repackage_deb(
+ command_context.log,
+ input,
+ output,
+ template_dir,
+ arch,
+ version,
+ build_number,
+ release_product,
+ release_type,
+ FluentLocalization,
+ FluentResourceLoader,
+ )
+
+
+@SubCommand(
+ "repackage",
+ "deb-l10n",
+ description="Repackage a .xpi langpack file into a .deb for Linux",
+)
+@CommandArgument(
+ "--input-xpi-file", type=str, required=True, help="Path to the XPI file"
+)
+@CommandArgument(
+ "--input-tar-file",
+ type=str,
+ required=True,
+ help="Path to tar archive that contains application.ini",
+)
+@CommandArgument(
+ "--version",
+ type=str,
+ required=True,
+ help="The Firefox version used to create the installer",
+)
+@CommandArgument(
+ "--build-number",
+ type=str,
+ required=True,
+ help="The release's build number",
+)
+@CommandArgument("--output", "-o", type=str, required=True, help="Output filename")
+@CommandArgument(
+ "--templates",
+ type=str,
+ required=True,
+ help="Location of the templates used to generate the debian/ directory files",
+)
+def repackage_deb_l10n(
+ command_context,
+ input_xpi_file,
+ input_tar_file,
+ output,
+ version,
+ build_number,
+ templates,
+):
+ for input_file in (input_xpi_file, input_tar_file):
+ if not os.path.exists(input_file):
+ print("Input file does not exist: %s" % input_file)
+ return 1
+
+ template_dir = os.path.join(
+ command_context.topsrcdir,
+ templates,
+ )
+
+ from mozbuild.repackaging.deb import repackage_deb_l10n
+
+ repackage_deb_l10n(
+ input_xpi_file, input_tar_file, output, template_dir, version, build_number
+ )
+
+
+@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(command_context, input, output):
+ if not os.path.exists(input):
+ print("Input file does not exist: %s" % input)
+ return 1
+
+ from mozbuild.repackaging.dmg import repackage_dmg
+
+ repackage_dmg(input, output)
+
+
+@SubCommand("repackage", "pkg", description="Repackage a tar file into a .pkg for OSX")
+@CommandArgument("--input", "-i", type=str, required=True, help="Input filename")
+@CommandArgument("--output", "-o", type=str, required=True, help="Output filename")
+def repackage_pkg(command_context, input, output):
+ if not os.path.exists(input):
+ print("Input file does not exist: %s" % input)
+ return 1
+
+ from mozbuild.repackaging.pkg import repackage_pkg
+
+ repackage_pkg(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(
+ command_context,
+ tag,
+ setupexe,
+ package,
+ output,
+ package_name,
+ sfx_stub,
+ use_upx,
+):
+ from mozbuild.repackaging.installer import repackage_installer
+
+ repackage_installer(
+ topsrcdir=command_context.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 architecture 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(
+ command_context,
+ wsx,
+ version,
+ locale,
+ arch,
+ setupexe,
+ candle,
+ light,
+ output,
+):
+ from mozbuild.repackaging.msi import repackage_msi
+
+ repackage_msi(
+ topsrcdir=command_context.topsrcdir,
+ wsx=wsx,
+ version=version,
+ locale=locale,
+ arch=arch,
+ setupexe=setupexe,
+ candle=candle,
+ light=light,
+ output=output,
+ )
+
+
+@SubCommand("repackage", "msix", description="Repackage into an MSIX")
+@CommandArgument(
+ "--input",
+ type=str,
+ help="Package (ZIP) or directory to repackage. Defaults to $OBJDIR/dist/bin",
+)
+@CommandArgument(
+ "--version",
+ type=str,
+ help="The Firefox version used to create the package "
+ "(Default: generated from package 'application.ini')",
+)
+@CommandArgument(
+ "--channel",
+ type=str,
+ choices=["official", "beta", "aurora", "nightly", "unofficial"],
+ help="Release channel.",
+)
+@CommandArgument(
+ "--distribution-dir",
+ metavar="DISTRIBUTION",
+ nargs="*",
+ dest="distribution_dirs",
+ default=[],
+ help="List of distribution directories to include.",
+)
+@CommandArgument(
+ "--arch",
+ type=str,
+ choices=["x86", "x86_64", "aarch64"],
+ help="The architecture you are building.",
+)
+@CommandArgument(
+ "--vendor",
+ type=str,
+ default="Mozilla",
+ required=False,
+ help="The vendor to use in the Package/Identity/Name string to use in the App Manifest."
+ + " Defaults to 'Mozilla'.",
+)
+@CommandArgument(
+ "--identity-name",
+ type=str,
+ default=None,
+ required=False,
+ help="The Package/Identity/Name string to use in the App Manifest."
+ + " Defaults to '<vendor>.Firefox', '<vendor>.FirefoxBeta', etc.",
+)
+@CommandArgument(
+ "--publisher",
+ type=str,
+ # This default is baked into enough places under `browser/` that we need
+ # not extract a constant.
+ default="CN=Mozilla Corporation, OU=MSIX Packaging",
+ required=False,
+ help="The Package/Identity/Publisher string to use in the App Manifest."
+ + " It must match the subject on the certificate used for signing.",
+)
+@CommandArgument(
+ "--publisher-display-name",
+ type=str,
+ default="Mozilla Corporation",
+ required=False,
+ help="The Package/Properties/PublisherDisplayName string to use in the App Manifest. "
+ + " Defaults to 'Mozilla Corporation'.",
+)
+@CommandArgument(
+ "--makeappx",
+ type=str,
+ default=None,
+ help="makeappx/makemsix binary name (required if you haven't run configure)",
+)
+@CommandArgument(
+ "--verbose",
+ default=False,
+ action="store_true",
+ help="Be verbose. (Default: false)",
+)
+@CommandArgument(
+ "--output", "-o", type=str, help="Output filename (Default: auto-generated)"
+)
+@CommandArgument(
+ "--sign",
+ default=False,
+ action="store_true",
+ help="Sign repackaged MSIX with self-signed certificate for local testing. "
+ "(Default: false)",
+)
+def repackage_msix(
+ command_context,
+ input,
+ version=None,
+ channel=None,
+ distribution_dirs=[],
+ arch=None,
+ identity_name=None,
+ vendor=None,
+ publisher=None,
+ publisher_display_name=None,
+ verbose=False,
+ output=None,
+ makeappx=None,
+ sign=False,
+):
+ from mozbuild.repackaging.msix import repackage_msix
+
+ command_context._set_log_level(verbose)
+
+ firefox_to_msix_channel = {
+ "release": "official",
+ "beta": "beta",
+ "aurora": "aurora",
+ "nightly": "nightly",
+ }
+
+ if not input:
+ if os.path.exists(command_context.bindir):
+ input = command_context.bindir
+ else:
+ command_context.log(
+ logging.ERROR,
+ "repackage-msix-no-input",
+ {},
+ "No build found in objdir, please run ./mach build or pass --input",
+ )
+ return 1
+
+ if not os.path.exists(input):
+ command_context.log(
+ logging.ERROR,
+ "repackage-msix-invalid-input",
+ {"input": input},
+ "Input file or directory for msix repackaging does not exist: {input}",
+ )
+ return 1
+
+ if not channel:
+ # Only try to guess the channel when this is clearly a local build.
+ if input.endswith("bin"):
+ channel = firefox_to_msix_channel.get(
+ command_context.defines.get("MOZ_UPDATE_CHANNEL"), "unofficial"
+ )
+ else:
+ command_context.log(
+ logging.ERROR,
+ "repackage-msix-invalid-channel",
+ {},
+ "Could not determine channel, please set --channel",
+ )
+ return 1
+
+ if not arch:
+ # Only try to guess the arch when this is clearly a local build.
+ if input.endswith("bin"):
+ if command_context.substs["TARGET_CPU"] in ("i686", "x86_64", "aarch64"):
+ arch = command_context.substs["TARGET_CPU"].replace("i686", "x86")
+
+ if not arch:
+ command_context.log(
+ logging.ERROR,
+ "repackage-msix-couldnt-detect-arch",
+ {},
+ "Could not automatically detect architecture for msix repackaging. "
+ "Please pass --arch",
+ )
+ return 1
+
+ output = repackage_msix(
+ input,
+ command_context.topsrcdir,
+ channel=channel,
+ arch=arch,
+ displayname=identity_name,
+ vendor=vendor,
+ publisher=publisher,
+ publisher_display_name=publisher_display_name,
+ version=version,
+ distribution_dirs=distribution_dirs,
+ # Configure this run.
+ force=True,
+ verbose=verbose,
+ log=command_context.log,
+ output=output,
+ makeappx=makeappx,
+ )
+
+ if sign:
+ repackage_sign_msix(command_context, output, force=False, verbose=verbose)
+
+ command_context.log(
+ logging.INFO,
+ "msix",
+ {"output": output},
+ "Wrote MSIX: {output}",
+ )
+
+
+@SubCommand("repackage", "sign-msix", description="Sign an MSIX for local testing")
+@CommandArgument("--input", type=str, required=True, help="MSIX to sign.")
+@CommandArgument(
+ "--force",
+ default=False,
+ action="store_true",
+ help="Force recreating self-signed certificate. (Default: false)",
+)
+@CommandArgument(
+ "--verbose",
+ default=False,
+ action="store_true",
+ help="Be verbose. (Default: false)",
+)
+def repackage_sign_msix(command_context, input, force=False, verbose=False):
+ from mozbuild.repackaging.msix import sign_msix
+
+ command_context._set_log_level(verbose)
+
+ sign_msix(input, force=force, log=command_context.log, verbose=verbose)
+
+ return 0
+
+
+@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 architecture you are building."
+)
+@CommandArgument("--mar-channel-id", type=str, help="Mar channel id")
+def repackage_mar(command_context, input, mar, output, arch, mar_channel_id):
+ from mozbuild.repackaging.mar import repackage_mar
+
+ repackage_mar(
+ command_context.topsrcdir,
+ input,
+ mar,
+ output,
+ arch=arch,
+ mar_channel_id=mar_channel_id,
+ )
+
+
+@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",
+)
+@CommandArgument(
+ "--verbose", action="store_true", help="Log informative status messages."
+)
+def package_l10n(command_context, verbose=False, locales=[]):
+ if "RecursiveMake" not in command_context.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
+
+ locales = sorted(locale for locale in locales if locale != "en-US")
+
+ 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),
+ }
+
+ command_context.log(
+ logging.INFO,
+ "package-multi-locale",
+ {"locales": locales},
+ "Processing chrome Gecko resources for locales {locales}",
+ )
+ command_context._run_make(
+ directory=command_context.topobjdir,
+ target=["chrome-{}".format(locale) for locale in locales],
+ append_env=append_env,
+ pass_thru=False,
+ print_directory=False,
+ ensure_exit_code=True,
+ )
+
+ if command_context.substs["MOZ_BUILD_APP"] == "mobile/android":
+ command_context.log(
+ logging.INFO,
+ "package-multi-locale",
+ {},
+ "Invoking `mach android assemble-app`",
+ )
+ command_context.run_process(
+ [
+ mozpath.join(command_context.topsrcdir, "mach"),
+ "android",
+ "assemble-app",
+ ],
+ append_env=append_env,
+ pass_thru=True,
+ ensure_exit_code=True,
+ cwd=mozpath.join(command_context.topsrcdir),
+ )
+
+ if command_context.substs["MOZ_BUILD_APP"] == "browser":
+ command_context.log(
+ logging.INFO, "package-multi-locale", {}, "Repackaging browser"
+ )
+ command_context._run_make(
+ directory=mozpath.join(command_context.topobjdir, "browser", "app"),
+ target=["tools"],
+ append_env=append_env,
+ pass_thru=True,
+ ensure_exit_code=True,
+ )
+
+ command_context.log(
+ logging.INFO,
+ "package-multi-locale",
+ {},
+ "Invoking multi-locale `mach package`",
+ )
+ target = ["package"]
+ if command_context.substs["MOZ_BUILD_APP"] == "mobile/android":
+ target.append("AB_CD=multi")
+
+ command_context._run_make(
+ directory=command_context.topobjdir,
+ target=target,
+ append_env=append_env,
+ pass_thru=True,
+ ensure_exit_code=True,
+ )
+
+ if command_context.substs["MOZ_BUILD_APP"] == "mobile/android":
+ command_context.log(
+ logging.INFO,
+ "package-multi-locale",
+ {},
+ "Invoking `mach android archive-geckoview`",
+ )
+ command_context.run_process(
+ [
+ mozpath.join(command_context.topsrcdir, "mach"),
+ "android",
+ "archive-geckoview",
+ ],
+ append_env=append_env,
+ pass_thru=True,
+ ensure_exit_code=True,
+ cwd=mozpath.join(command_context.topsrcdir),
+ )
+
+ # This is tricky: most Android build commands will regenerate the
+ # omnijar, producing a `res/multilocale.txt` that does not contain the
+ # set of locales packaged by this command. To avoid regenerating, we
+ # set a special environment variable.
+ print(
+ "Execute `env MOZ_CHROME_MULTILOCALE='{}' ".format(
+ append_env["MOZ_CHROME_MULTILOCALE"]
+ )
+ + "mach android install-geckoview_example` "
+ + "to install the multi-locale geckoview_example and test APKs."
+ )
+
+ return 0
+
+
+def _prepend_debugger_args(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