Source code

Revision control

Other Tools

# 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 platform
import re
import shutil
import subprocess
import sys
import tempfile
import time
import mozbuild.settings # noqa need @SettingsProvider hook to execute
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,
)
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
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 (parts of) 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 (parts of) the source tree."""
if not conditions.is_artifact_build(self):
print(
"WARNING: mach watch only rebuilds the `mach build faster` parts of the tree!"
)
if not self.substs.get("WATCHMAN", None):
print(
"mach watch requires watchman to be installed and found at configure time. See "
)
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 "
)
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",
"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
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 = _prepend_debugger_args(args, debugger, debugger_args)
if not args:
return 1
# Use GTest environment variable to control test execution
# For details see:
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
@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()
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(
"--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(
"--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,
debug=False,
debugger=None,
debugger_args=None,
no_attach=False,
use_existing_process=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))
# 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(
self,
app=app,
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(self.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:
self.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)
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}"',
)
if enable_fission:
env.append("MOZ_FORCE_ENABLE_FISSION=1")
args = ["--profile", shlex_quote(target_profile)]
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,
)
if not debug:
return 0
from mozrunner.devices.android_device import run_lldb_server
socket_file = run_lldb_server(app, self.substs, device_serial)
if not socket_file:
self.log(
logging.ERROR,
"run",
{"msg": "Failed to obtain a socket file!"},
"{msg}",
)
return 1
# Give lldb-server a chance to start
self.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:
self.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
self.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:
self.log(
logging.ERROR,
"run",
{"msg": "Not sure which pid to attach to!"},
"{msg}",
)
return 1
pid = pids[0]
self.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:
self.log(
logging.INFO,
"run",
{"pid": str(pid), "url": lldb_connect_url},
"To debug native code, connect lldb to {url} and attach to pid {pid}",
)
self.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:
self.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(self.topobjdir, "toolkit", "library", "build")
obj_mozglue = os.path.join(self.topobjdir, "mozglue", "build")
obj_nss = os.path.join(self.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 self.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(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
# 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,