summaryrefslogtreecommitdiffstats
path: root/python/mozbuild/mozbuild/base.py
diff options
context:
space:
mode:
Diffstat (limited to 'python/mozbuild/mozbuild/base.py')
-rw-r--r--python/mozbuild/mozbuild/base.py1110
1 files changed, 1110 insertions, 0 deletions
diff --git a/python/mozbuild/mozbuild/base.py b/python/mozbuild/mozbuild/base.py
new file mode 100644
index 0000000000..9822a9b76e
--- /dev/null
+++ b/python/mozbuild/mozbuild/base.py
@@ -0,0 +1,1110 @@
+# 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 errno
+import io
+import json
+import logging
+import multiprocessing
+import os
+import subprocess
+import sys
+from pathlib import Path
+
+import mozpack.path as mozpath
+import six
+from mach.mixin.process import ProcessExecutionMixin
+from mozboot.mozconfig import MozconfigFindException
+from mozfile import which
+from mozversioncontrol import (
+ GitRepository,
+ HgRepository,
+ InvalidRepoPath,
+ MissingConfigureInfo,
+ MissingVCSTool,
+ get_repository_from_build_config,
+ get_repository_object,
+)
+
+from .backend.configenvironment import ConfigEnvironment, ConfigStatusFailure
+from .configure import ConfigureSandbox
+from .controller.clobber import Clobberer
+from .mozconfig import MozconfigLoader, MozconfigLoadException
+from .util import memoize, memoized_property
+
+try:
+ import psutil
+except Exception:
+ psutil = None
+
+
+class BadEnvironmentException(Exception):
+ """Base class for errors raised when the build environment is not sane."""
+
+
+class BuildEnvironmentNotFoundException(BadEnvironmentException, AttributeError):
+ """Raised when we could not find a build environment."""
+
+
+class ObjdirMismatchException(BadEnvironmentException):
+ """Raised when the current dir is an objdir and doesn't match the mozconfig."""
+
+ def __init__(self, objdir1, objdir2):
+ self.objdir1 = objdir1
+ self.objdir2 = objdir2
+
+ def __str__(self):
+ return "Objdir mismatch: %s != %s" % (self.objdir1, self.objdir2)
+
+
+class BinaryNotFoundException(Exception):
+ """Raised when the binary is not found in the expected location."""
+
+ def __init__(self, path):
+ self.path = path
+
+ def __str__(self):
+ return "Binary expected at {} does not exist.".format(self.path)
+
+ def help(self):
+ return "It looks like your program isn't built. You can run |./mach build| to build it."
+
+
+class MozbuildObject(ProcessExecutionMixin):
+ """Base class providing basic functionality useful to many modules.
+
+ Modules in this package typically require common functionality such as
+ accessing the current config, getting the location of the source directory,
+ running processes, etc. This classes provides that functionality. Other
+ modules can inherit from this class to obtain this functionality easily.
+ """
+
+ def __init__(
+ self,
+ topsrcdir,
+ settings,
+ log_manager,
+ topobjdir=None,
+ mozconfig=MozconfigLoader.AUTODETECT,
+ virtualenv_name=None,
+ ):
+ """Create a new Mozbuild object instance.
+
+ Instances are bound to a source directory, a ConfigSettings instance,
+ and a LogManager instance. The topobjdir may be passed in as well. If
+ it isn't, it will be calculated from the active mozconfig.
+ """
+ self.topsrcdir = mozpath.realpath(topsrcdir)
+ self.settings = settings
+
+ self.populate_logger()
+ self.log_manager = log_manager
+
+ self._make = None
+ self._topobjdir = mozpath.realpath(topobjdir) if topobjdir else topobjdir
+ self._mozconfig = mozconfig
+ self._config_environment = None
+ self._virtualenv_name = virtualenv_name or "common"
+ self._virtualenv_manager = None
+
+ @classmethod
+ def from_environment(cls, cwd=None, detect_virtualenv_mozinfo=True, **kwargs):
+ """Create a MozbuildObject by detecting the proper one from the env.
+
+ This examines environment state like the current working directory and
+ creates a MozbuildObject from the found source directory, mozconfig, etc.
+
+ The role of this function is to identify a topsrcdir, topobjdir, and
+ mozconfig file.
+
+ If the current working directory is inside a known objdir, we always
+ use the topsrcdir and mozconfig associated with that objdir.
+
+ If the current working directory is inside a known srcdir, we use that
+ topsrcdir and look for mozconfigs using the default mechanism, which
+ looks inside environment variables.
+
+ If the current Python interpreter is running from a virtualenv inside
+ an objdir, we use that as our objdir.
+
+ If we're not inside a srcdir or objdir, an exception is raised.
+
+ detect_virtualenv_mozinfo determines whether we should look for a
+ mozinfo.json file relative to the virtualenv directory. This was
+ added to facilitate testing. Callers likely shouldn't change the
+ default.
+ """
+
+ cwd = os.path.realpath(cwd or os.getcwd())
+ topsrcdir = None
+ topobjdir = None
+ mozconfig = MozconfigLoader.AUTODETECT
+
+ def load_mozinfo(path):
+ info = json.load(io.open(path, "rt", encoding="utf-8"))
+ topsrcdir = info.get("topsrcdir")
+ topobjdir = os.path.dirname(path)
+ mozconfig = info.get("mozconfig")
+ return topsrcdir, topobjdir, mozconfig
+
+ for dir_path in [str(path) for path in [cwd] + list(Path(cwd).parents)]:
+ # If we find a mozinfo.json, we are in the objdir.
+ mozinfo_path = os.path.join(dir_path, "mozinfo.json")
+ if os.path.isfile(mozinfo_path):
+ topsrcdir, topobjdir, mozconfig = load_mozinfo(mozinfo_path)
+ break
+
+ if not topsrcdir:
+ # See if we're running from a Python virtualenv that's inside an objdir.
+ # sys.prefix would look like "$objdir/_virtualenvs/$virtualenv/".
+ # Note that virtualenv-based objdir detection work for instrumented builds,
+ # because they aren't created in the scoped "instrumentated" objdir.
+ # However, working-directory-ancestor-based objdir resolution should fully
+ # cover that case.
+ mozinfo_path = os.path.join(sys.prefix, "..", "..", "mozinfo.json")
+ if detect_virtualenv_mozinfo and os.path.isfile(mozinfo_path):
+ topsrcdir, topobjdir, mozconfig = load_mozinfo(mozinfo_path)
+
+ if not topsrcdir:
+ topsrcdir = str(Path(__file__).parent.parent.parent.parent.resolve())
+
+ topsrcdir = mozpath.realpath(topsrcdir)
+ if topobjdir:
+ topobjdir = mozpath.realpath(topobjdir)
+
+ if topsrcdir == topobjdir:
+ raise BadEnvironmentException(
+ "The object directory appears "
+ "to be the same as your source directory (%s). This build "
+ "configuration is not supported." % topsrcdir
+ )
+
+ # If we can't resolve topobjdir, oh well. We'll figure out when we need
+ # one.
+ return cls(
+ topsrcdir, None, None, topobjdir=topobjdir, mozconfig=mozconfig, **kwargs
+ )
+
+ def resolve_mozconfig_topobjdir(self, default=None):
+ topobjdir = self.mozconfig.get("topobjdir") or default
+ if not topobjdir:
+ return None
+
+ if "@CONFIG_GUESS@" in topobjdir:
+ topobjdir = topobjdir.replace("@CONFIG_GUESS@", self.resolve_config_guess())
+
+ if not os.path.isabs(topobjdir):
+ topobjdir = os.path.abspath(os.path.join(self.topsrcdir, topobjdir))
+
+ return mozpath.normsep(os.path.normpath(topobjdir))
+
+ def build_out_of_date(self, output, dep_file):
+ if not os.path.isfile(output):
+ print(" Output reference file not found: %s" % output)
+ return True
+ if not os.path.isfile(dep_file):
+ print(" Dependency file not found: %s" % dep_file)
+ return True
+
+ deps = []
+ with io.open(dep_file, "r", encoding="utf-8", newline="\n") as fh:
+ deps = fh.read().splitlines()
+
+ mtime = os.path.getmtime(output)
+ for f in deps:
+ try:
+ dep_mtime = os.path.getmtime(f)
+ except OSError as e:
+ if e.errno == errno.ENOENT:
+ print(" Input not found: %s" % f)
+ return True
+ raise
+ if dep_mtime > mtime:
+ print(" %s is out of date with respect to %s" % (output, f))
+ return True
+ return False
+
+ def backend_out_of_date(self, backend_file):
+ if not os.path.isfile(backend_file):
+ return True
+
+ # Check if any of our output files have been removed since
+ # we last built the backend, re-generate the backend if
+ # so.
+ outputs = []
+ with io.open(backend_file, "r", encoding="utf-8", newline="\n") as fh:
+ outputs = fh.read().splitlines()
+ for output in outputs:
+ if not os.path.isfile(mozpath.join(self.topobjdir, output)):
+ return True
+
+ dep_file = "%s.in" % backend_file
+ return self.build_out_of_date(backend_file, dep_file)
+
+ @property
+ def topobjdir(self):
+ if self._topobjdir is None:
+ self._topobjdir = self.resolve_mozconfig_topobjdir(
+ default="obj-@CONFIG_GUESS@"
+ )
+
+ return self._topobjdir
+
+ @property
+ def virtualenv_manager(self):
+ from mach.site import CommandSiteManager
+ from mozboot.util import get_state_dir
+
+ if self._virtualenv_manager is None:
+ self._virtualenv_manager = CommandSiteManager.from_environment(
+ self.topsrcdir,
+ lambda: get_state_dir(
+ specific_to_topsrcdir=True, topsrcdir=self.topsrcdir
+ ),
+ self._virtualenv_name,
+ os.path.join(self.topobjdir, "_virtualenvs"),
+ )
+
+ return self._virtualenv_manager
+
+ @staticmethod
+ @memoize
+ def get_base_mozconfig_info(topsrcdir, path, env_mozconfig):
+ # env_mozconfig is only useful for unittests, which change the value of
+ # the environment variable, which has an impact on autodetection (when
+ # path is MozconfigLoader.AUTODETECT), and memoization wouldn't account
+ # for it without the explicit (unused) argument.
+ out = six.StringIO()
+ env = os.environ
+ if path and path != MozconfigLoader.AUTODETECT:
+ env = dict(env)
+ env["MOZCONFIG"] = path
+
+ # We use python configure to get mozconfig content and the value for
+ # --target (from mozconfig if necessary, guessed otherwise).
+
+ # Modified configure sandbox that replaces '--help' dependencies with
+ # `always`, such that depends functions with a '--help' dependency are
+ # not automatically executed when including files. We don't want all of
+ # those from init.configure to execute, only a subset.
+ class ReducedConfigureSandbox(ConfigureSandbox):
+ def depends_impl(self, *args, **kwargs):
+ args = tuple(
+ a
+ if not isinstance(a, six.string_types) or a != "--help"
+ else self._always.sandboxed
+ for a in args
+ )
+ return super(ReducedConfigureSandbox, self).depends_impl(
+ *args, **kwargs
+ )
+
+ # This may be called recursively from configure itself for $reasons,
+ # so avoid logging to the same logger (configure uses "moz.configure")
+ logger = logging.getLogger("moz.configure.reduced")
+ handler = logging.StreamHandler(out)
+ logger.addHandler(handler)
+ # If this were true, logging would still propagate to "moz.configure".
+ logger.propagate = False
+ sandbox = ReducedConfigureSandbox(
+ {},
+ environ=env,
+ argv=["mach"],
+ logger=logger,
+ )
+ base_dir = os.path.join(topsrcdir, "build", "moz.configure")
+ try:
+ sandbox.include_file(os.path.join(base_dir, "init.configure"))
+ # Force mozconfig options injection before getting the target.
+ sandbox._value_for(sandbox["mozconfig_options"])
+ return {
+ "mozconfig": sandbox._value_for(sandbox["mozconfig"]),
+ "target": sandbox._value_for(sandbox["real_target"]),
+ "project": sandbox._value_for(sandbox._options["project"]),
+ "artifact-builds": sandbox._value_for(
+ sandbox._options["artifact-builds"]
+ ),
+ }
+ except SystemExit:
+ print(out.getvalue())
+ raise
+
+ @property
+ def base_mozconfig_info(self):
+ return self.get_base_mozconfig_info(
+ self.topsrcdir, self._mozconfig, os.environ.get("MOZCONFIG")
+ )
+
+ @property
+ def mozconfig(self):
+ """Returns information about the current mozconfig file.
+
+ This a dict as returned by MozconfigLoader.read_mozconfig()
+ """
+ return self.base_mozconfig_info["mozconfig"]
+
+ @property
+ def config_environment(self):
+ """Returns the ConfigEnvironment for the current build configuration.
+
+ This property is only available once configure has executed.
+
+ If configure's output is not available, this will raise.
+ """
+ if self._config_environment:
+ return self._config_environment
+
+ config_status = os.path.join(self.topobjdir, "config.status")
+
+ if not os.path.exists(config_status) or not os.path.getsize(config_status):
+ raise BuildEnvironmentNotFoundException(
+ "config.status not available. Run configure."
+ )
+
+ try:
+ self._config_environment = ConfigEnvironment.from_config_status(
+ config_status
+ )
+ except ConfigStatusFailure as e:
+ six.raise_from(
+ BuildEnvironmentNotFoundException(
+ "config.status is outdated or broken. Run configure."
+ ),
+ e,
+ )
+
+ return self._config_environment
+
+ @property
+ def defines(self):
+ return self.config_environment.defines
+
+ @property
+ def substs(self):
+ return self.config_environment.substs
+
+ @property
+ def distdir(self):
+ return os.path.join(self.topobjdir, "dist")
+
+ @property
+ def bindir(self):
+ return os.path.join(self.topobjdir, "dist", "bin")
+
+ @property
+ def includedir(self):
+ return os.path.join(self.topobjdir, "dist", "include")
+
+ @property
+ def statedir(self):
+ return os.path.join(self.topobjdir, ".mozbuild")
+
+ @property
+ def platform(self):
+ """Returns current platform and architecture name"""
+ import mozinfo
+
+ platform_name = None
+ bits = str(mozinfo.info["bits"])
+ if mozinfo.isLinux:
+ platform_name = "linux" + bits
+ elif mozinfo.isWin:
+ platform_name = "win" + bits
+ elif mozinfo.isMac:
+ platform_name = "macosx" + bits
+
+ return platform_name, bits + "bit"
+
+ @memoized_property
+ def repository(self):
+ """Get a `mozversioncontrol.Repository` object for the
+ top source directory."""
+ # We try to obtain a repo using the configured VCS info first.
+ # If we don't have a configure context, fall back to auto-detection.
+ try:
+ return get_repository_from_build_config(self)
+ except (
+ BuildEnvironmentNotFoundException,
+ MissingConfigureInfo,
+ MissingVCSTool,
+ ):
+ pass
+
+ return get_repository_object(self.topsrcdir)
+
+ def reload_config_environment(self):
+ """Force config.status to be re-read and return the new value
+ of ``self.config_environment``.
+ """
+ self._config_environment = None
+ return self.config_environment
+
+ def mozbuild_reader(
+ self, config_mode="build", vcs_revision=None, vcs_check_clean=True
+ ):
+ """Obtain a ``BuildReader`` for evaluating moz.build files.
+
+ Given arguments, returns a ``mozbuild.frontend.reader.BuildReader``
+ that can be used to evaluate moz.build files for this repo.
+
+ ``config_mode`` is either ``build`` or ``empty``. If ``build``,
+ ``self.config_environment`` is used. This requires a configured build
+ system to work. If ``empty``, an empty config is used. ``empty`` is
+ appropriate for file-based traversal mode where ``Files`` metadata is
+ read.
+
+ If ``vcs_revision`` is defined, it specifies a version control revision
+ to use to obtain files content. The default is to use the filesystem.
+ This mode is only supported with Mercurial repositories.
+
+ If ``vcs_revision`` is not defined and the version control checkout is
+ sparse, this implies ``vcs_revision='.'``.
+
+ If ``vcs_revision`` is ``.`` (denotes the parent of the working
+ directory), we will verify that the working directory is clean unless
+ ``vcs_check_clean`` is False. This prevents confusion due to uncommitted
+ file changes not being reflected in the reader.
+ """
+ from mozpack.files import MercurialRevisionFinder
+
+ from mozbuild.frontend.reader import BuildReader, EmptyConfig, default_finder
+
+ if config_mode == "build":
+ config = self.config_environment
+ elif config_mode == "empty":
+ config = EmptyConfig(self.topsrcdir)
+ else:
+ raise ValueError("unknown config_mode value: %s" % config_mode)
+
+ try:
+ repo = self.repository
+ except InvalidRepoPath:
+ repo = None
+
+ if (
+ repo
+ and repo != "SOURCE"
+ and not vcs_revision
+ and repo.sparse_checkout_present()
+ ):
+ vcs_revision = "."
+
+ if vcs_revision is None:
+ finder = default_finder
+ else:
+ # If we failed to detect the repo prior, check again to raise its
+ # exception.
+ if not repo:
+ self.repository
+ assert False
+
+ if repo.name != "hg":
+ raise Exception("do not support VCS reading mode for %s" % repo.name)
+
+ if vcs_revision == "." and vcs_check_clean:
+ with repo:
+ if not repo.working_directory_clean():
+ raise Exception(
+ "working directory is not clean; "
+ "refusing to use a VCS-based finder"
+ )
+
+ finder = MercurialRevisionFinder(
+ self.topsrcdir, rev=vcs_revision, recognize_repo_paths=True
+ )
+
+ return BuildReader(config, finder=finder)
+
+ def is_clobber_needed(self):
+ if not os.path.exists(self.topobjdir):
+ return False
+ return Clobberer(self.topsrcdir, self.topobjdir).clobber_needed()
+
+ def get_binary_path(self, what="app", validate_exists=True, where="default"):
+ """Obtain the path to a compiled binary for this build configuration.
+
+ The what argument is the program or tool being sought after. See the
+ code implementation for supported values.
+
+ If validate_exists is True (the default), we will ensure the found path
+ exists before returning, raising an exception if it doesn't.
+
+ If where is 'staged-package', we will return the path to the binary in
+ the package staging directory.
+
+ If no arguments are specified, we will return the main binary for the
+ configured XUL application.
+ """
+
+ if where not in ("default", "staged-package"):
+ raise Exception("Don't know location %s" % where)
+
+ substs = self.substs
+
+ stem = self.distdir
+ if where == "staged-package":
+ stem = os.path.join(stem, substs["MOZ_APP_NAME"])
+
+ if substs["OS_ARCH"] == "Darwin" and "MOZ_MACBUNDLE_NAME" in substs:
+ stem = os.path.join(stem, substs["MOZ_MACBUNDLE_NAME"], "Contents", "MacOS")
+ elif where == "default":
+ stem = os.path.join(stem, "bin")
+
+ leaf = None
+
+ leaf = (substs["MOZ_APP_NAME"] if what == "app" else what) + substs[
+ "BIN_SUFFIX"
+ ]
+ path = os.path.join(stem, leaf)
+
+ if validate_exists and not os.path.exists(path):
+ raise BinaryNotFoundException(path)
+
+ return path
+
+ def resolve_config_guess(self):
+ return self.base_mozconfig_info["target"].alias
+
+ def notify(self, msg):
+ """Show a desktop notification with the supplied message
+
+ On Linux and Mac, this will show a desktop notification with the message,
+ but on Windows we can only flash the screen.
+ """
+ if "MOZ_NOSPAM" in os.environ or "MOZ_AUTOMATION" in os.environ:
+ return
+
+ try:
+ if sys.platform.startswith("darwin"):
+ notifier = which("terminal-notifier")
+ if not notifier:
+ raise Exception(
+ "Install terminal-notifier to get "
+ "a notification when the build finishes."
+ )
+ self.run_process(
+ [
+ notifier,
+ "-title",
+ "Mozilla Build System",
+ "-group",
+ "mozbuild",
+ "-message",
+ msg,
+ ],
+ ensure_exit_code=False,
+ )
+ elif sys.platform.startswith("win"):
+ from ctypes import POINTER, WINFUNCTYPE, Structure, sizeof, windll
+ from ctypes.wintypes import BOOL, DWORD, HANDLE, UINT
+
+ class FLASHWINDOW(Structure):
+ _fields_ = [
+ ("cbSize", UINT),
+ ("hwnd", HANDLE),
+ ("dwFlags", DWORD),
+ ("uCount", UINT),
+ ("dwTimeout", DWORD),
+ ]
+
+ FlashWindowExProto = WINFUNCTYPE(BOOL, POINTER(FLASHWINDOW))
+ FlashWindowEx = FlashWindowExProto(("FlashWindowEx", windll.user32))
+ FLASHW_CAPTION = 0x01
+ FLASHW_TRAY = 0x02
+ FLASHW_TIMERNOFG = 0x0C
+
+ # GetConsoleWindows returns NULL if no console is attached. We
+ # can't flash nothing.
+ console = windll.kernel32.GetConsoleWindow()
+ if not console:
+ return
+
+ params = FLASHWINDOW(
+ sizeof(FLASHWINDOW),
+ console,
+ FLASHW_CAPTION | FLASHW_TRAY | FLASHW_TIMERNOFG,
+ 3,
+ 0,
+ )
+ FlashWindowEx(params)
+ else:
+ notifier = which("notify-send")
+ if not notifier:
+ raise Exception(
+ "Install notify-send (usually part of "
+ "the libnotify package) to get a notification when "
+ "the build finishes."
+ )
+ self.run_process(
+ [
+ notifier,
+ "--app-name=Mozilla Build System",
+ "Mozilla Build System",
+ msg,
+ ],
+ ensure_exit_code=False,
+ )
+ except Exception as e:
+ self.log(
+ logging.WARNING,
+ "notifier-failed",
+ {"error": str(e)},
+ "Notification center failed: {error}",
+ )
+
+ def _ensure_objdir_exists(self):
+ if os.path.isdir(self.statedir):
+ return
+
+ os.makedirs(self.statedir)
+
+ def _ensure_state_subdir_exists(self, subdir):
+ path = os.path.join(self.statedir, subdir)
+
+ if os.path.isdir(path):
+ return
+
+ os.makedirs(path)
+
+ def _get_state_filename(self, filename, subdir=None):
+ path = self.statedir
+
+ if subdir:
+ path = os.path.join(path, subdir)
+
+ return os.path.join(path, filename)
+
+ def _wrap_path_argument(self, arg):
+ return PathArgument(arg, self.topsrcdir, self.topobjdir)
+
+ def _run_make(
+ self,
+ directory=None,
+ filename=None,
+ target=None,
+ log=True,
+ srcdir=False,
+ line_handler=None,
+ append_env=None,
+ explicit_env=None,
+ ignore_errors=False,
+ ensure_exit_code=0,
+ silent=True,
+ print_directory=True,
+ pass_thru=False,
+ num_jobs=0,
+ job_size=0,
+ keep_going=False,
+ ):
+ """Invoke make.
+
+ directory -- Relative directory to look for Makefile in.
+ filename -- Explicit makefile to run.
+ target -- Makefile target(s) to make. Can be a string or iterable of
+ strings.
+ srcdir -- If True, invoke make from the source directory tree.
+ Otherwise, make will be invoked from the object directory.
+ silent -- If True (the default), run make in silent mode.
+ print_directory -- If True (the default), have make print directories
+ while doing traversal.
+ """
+ self._ensure_objdir_exists()
+
+ args = [self.substs["GMAKE"]]
+
+ if directory:
+ args.extend(["-C", directory.replace(os.sep, "/")])
+
+ if filename:
+ args.extend(["-f", filename])
+
+ if num_jobs == 0 and self.mozconfig["make_flags"]:
+ flags = iter(self.mozconfig["make_flags"])
+ for flag in flags:
+ if flag == "-j":
+ try:
+ flag = flags.next()
+ except StopIteration:
+ break
+ try:
+ num_jobs = int(flag)
+ except ValueError:
+ args.append(flag)
+ elif flag.startswith("-j"):
+ try:
+ num_jobs = int(flag[2:])
+ except (ValueError, IndexError):
+ break
+ else:
+ args.append(flag)
+
+ if num_jobs == 0:
+ if job_size == 0:
+ job_size = 2.0 if self.substs.get("CC_TYPE") == "gcc" else 1.0 # GiB
+
+ cpus = multiprocessing.cpu_count()
+ if not psutil or not job_size:
+ num_jobs = cpus
+ else:
+ mem_gb = psutil.virtual_memory().total / 1024 ** 3
+ from_mem = round(mem_gb / job_size)
+ num_jobs = max(1, min(cpus, from_mem))
+ print(
+ " Parallelism determined by memory: using %d jobs for %d cores "
+ "based on %.1f GiB RAM and estimated job size of %.1f GiB"
+ % (num_jobs, cpus, mem_gb, job_size)
+ )
+
+ args.append("-j%d" % num_jobs)
+
+ if ignore_errors:
+ args.append("-k")
+
+ if silent:
+ args.append("-s")
+
+ # Print entering/leaving directory messages. Some consumers look at
+ # these to measure progress.
+ if print_directory:
+ args.append("-w")
+
+ if keep_going:
+ args.append("-k")
+
+ if isinstance(target, list):
+ args.extend(target)
+ elif target:
+ args.append(target)
+
+ fn = self._run_command_in_objdir
+
+ if srcdir:
+ fn = self._run_command_in_srcdir
+
+ append_env = dict(append_env or ())
+ append_env["MACH"] = "1"
+
+ params = {
+ "args": args,
+ "line_handler": line_handler,
+ "append_env": append_env,
+ "explicit_env": explicit_env,
+ "log_level": logging.INFO,
+ "require_unix_environment": False,
+ "ensure_exit_code": ensure_exit_code,
+ "pass_thru": pass_thru,
+ # Make manages its children, so mozprocess doesn't need to bother.
+ # Having mozprocess manage children can also have side-effects when
+ # building on Windows. See bug 796840.
+ "ignore_children": True,
+ }
+
+ if log:
+ params["log_name"] = "make"
+
+ return fn(**params)
+
+ def _run_command_in_srcdir(self, **args):
+ return self.run_process(cwd=self.topsrcdir, **args)
+
+ def _run_command_in_objdir(self, **args):
+ return self.run_process(cwd=self.topobjdir, **args)
+
+ def _is_windows(self):
+ return os.name in ("nt", "ce")
+
+ def _is_osx(self):
+ return "darwin" in str(sys.platform).lower()
+
+ def _spawn(self, cls):
+ """Create a new MozbuildObject-derived class instance from ourselves.
+
+ This is used as a convenience method to create other
+ MozbuildObject-derived class instances. It can only be used on
+ classes that have the same constructor arguments as us.
+ """
+
+ return cls(
+ self.topsrcdir, self.settings, self.log_manager, topobjdir=self.topobjdir
+ )
+
+ def activate_virtualenv(self):
+ self.virtualenv_manager.activate()
+
+ def _set_log_level(self, verbose):
+ self.log_manager.terminal_handler.setLevel(
+ logging.INFO if not verbose else logging.DEBUG
+ )
+
+ def _ensure_zstd(self):
+ try:
+ import zstandard # noqa: F401
+ except (ImportError, AttributeError):
+ self.activate_virtualenv()
+ self.virtualenv_manager.install_pip_requirements(
+ os.path.join(self.topsrcdir, "build", "zstandard_requirements.txt")
+ )
+
+
+class MachCommandBase(MozbuildObject):
+ """Base class for mach command providers that wish to be MozbuildObjects.
+
+ This provides a level of indirection so MozbuildObject can be refactored
+ without having to change everything that inherits from it.
+ """
+
+ def __init__(self, context, virtualenv_name=None, metrics=None, no_auto_log=False):
+ # Attempt to discover topobjdir through environment detection, as it is
+ # more reliable than mozconfig when cwd is inside an objdir.
+ topsrcdir = context.topdir
+ topobjdir = None
+ detect_virtualenv_mozinfo = True
+ if hasattr(context, "detect_virtualenv_mozinfo"):
+ detect_virtualenv_mozinfo = getattr(context, "detect_virtualenv_mozinfo")
+ try:
+ dummy = MozbuildObject.from_environment(
+ cwd=context.cwd, detect_virtualenv_mozinfo=detect_virtualenv_mozinfo
+ )
+ topsrcdir = dummy.topsrcdir
+ topobjdir = dummy._topobjdir
+ if topobjdir:
+ # If we're inside a objdir and the found mozconfig resolves to
+ # another objdir, we abort. The reasoning here is that if you
+ # are inside an objdir you probably want to perform actions on
+ # that objdir, not another one. This prevents accidental usage
+ # of the wrong objdir when the current objdir is ambiguous.
+ config_topobjdir = dummy.resolve_mozconfig_topobjdir()
+
+ if config_topobjdir and not Path(topobjdir).samefile(
+ Path(config_topobjdir)
+ ):
+ raise ObjdirMismatchException(topobjdir, config_topobjdir)
+ except BuildEnvironmentNotFoundException:
+ pass
+ except ObjdirMismatchException as e:
+ print(
+ "Ambiguous object directory detected. We detected that "
+ "both %s and %s could be object directories. This is "
+ "typically caused by having a mozconfig pointing to a "
+ "different object directory from the current working "
+ "directory. To solve this problem, ensure you do not have a "
+ "default mozconfig in searched paths." % (e.objdir1, e.objdir2)
+ )
+ sys.exit(1)
+
+ except MozconfigLoadException as e:
+ print(e)
+ sys.exit(1)
+
+ MozbuildObject.__init__(
+ self,
+ topsrcdir,
+ context.settings,
+ context.log_manager,
+ topobjdir=topobjdir,
+ virtualenv_name=virtualenv_name,
+ )
+
+ self._mach_context = context
+ self.metrics = metrics
+
+ # Incur mozconfig processing so we have unified error handling for
+ # errors. Otherwise, the exceptions could bubble back to mach's error
+ # handler.
+ try:
+ self.mozconfig
+
+ except MozconfigFindException as e:
+ print(e)
+ sys.exit(1)
+
+ except MozconfigLoadException as e:
+ print(e)
+ sys.exit(1)
+
+ # Always keep a log of the last command, but don't do that for mach
+ # invokations from scripts (especially not the ones done by the build
+ # system itself).
+ try:
+ fileno = getattr(sys.stdout, "fileno", lambda: None)()
+ except io.UnsupportedOperation:
+ fileno = None
+ if fileno and os.isatty(fileno) and not no_auto_log:
+ self._ensure_state_subdir_exists(".")
+ logfile = self._get_state_filename("last_log.json")
+ try:
+ fd = open(logfile, "wt")
+ self.log_manager.add_json_handler(fd)
+ except Exception as e:
+ self.log(
+ logging.WARNING,
+ "mach",
+ {"error": str(e)},
+ "Log will not be kept for this command: {error}.",
+ )
+
+ def _sub_mach(self, argv):
+ return subprocess.call(
+ [sys.executable, os.path.join(self.topsrcdir, "mach")] + argv
+ )
+
+
+class MachCommandConditions(object):
+ """A series of commonly used condition functions which can be applied to
+ mach commands with providers deriving from MachCommandBase.
+ """
+
+ @staticmethod
+ def is_firefox(cls):
+ """Must have a Firefox build."""
+ if hasattr(cls, "substs"):
+ return cls.substs.get("MOZ_BUILD_APP") == "browser"
+ return False
+
+ @staticmethod
+ def is_jsshell(cls):
+ """Must have a jsshell build."""
+ if hasattr(cls, "substs"):
+ return cls.substs.get("MOZ_BUILD_APP") == "js"
+ return False
+
+ @staticmethod
+ def is_thunderbird(cls):
+ """Must have a Thunderbird build."""
+ if hasattr(cls, "substs"):
+ return cls.substs.get("MOZ_BUILD_APP") == "comm/mail"
+ return False
+
+ @staticmethod
+ def is_firefox_or_thunderbird(cls):
+ """Must have a Firefox or Thunderbird build."""
+ return MachCommandConditions.is_firefox(
+ cls
+ ) or MachCommandConditions.is_thunderbird(cls)
+
+ @staticmethod
+ def is_android(cls):
+ """Must have an Android build."""
+ if hasattr(cls, "substs"):
+ return cls.substs.get("MOZ_WIDGET_TOOLKIT") == "android"
+ return False
+
+ @staticmethod
+ def is_not_android(cls):
+ """Must not have an Android build."""
+ if hasattr(cls, "substs"):
+ return cls.substs.get("MOZ_WIDGET_TOOLKIT") != "android"
+ return False
+
+ @staticmethod
+ def is_firefox_or_android(cls):
+ """Must have a Firefox or Android build."""
+ return MachCommandConditions.is_firefox(
+ cls
+ ) or MachCommandConditions.is_android(cls)
+
+ @staticmethod
+ def has_build(cls):
+ """Must have a build."""
+ return MachCommandConditions.is_firefox_or_android(
+ cls
+ ) or MachCommandConditions.is_thunderbird(cls)
+
+ @staticmethod
+ def has_build_or_shell(cls):
+ """Must have a build or a shell build."""
+ return MachCommandConditions.has_build(cls) or MachCommandConditions.is_jsshell(
+ cls
+ )
+
+ @staticmethod
+ def is_hg(cls):
+ """Must have a mercurial source checkout."""
+ try:
+ return isinstance(cls.repository, HgRepository)
+ except InvalidRepoPath:
+ return False
+
+ @staticmethod
+ def is_git(cls):
+ """Must have a git source checkout."""
+ try:
+ return isinstance(cls.repository, GitRepository)
+ except InvalidRepoPath:
+ return False
+
+ @staticmethod
+ def is_artifact_build(cls):
+ """Must be an artifact build."""
+ if hasattr(cls, "substs"):
+ return getattr(cls, "substs", {}).get("MOZ_ARTIFACT_BUILDS")
+ return False
+
+ @staticmethod
+ def is_non_artifact_build(cls):
+ """Must not be an artifact build."""
+ if hasattr(cls, "substs"):
+ return not MachCommandConditions.is_artifact_build(cls)
+ return False
+
+ @staticmethod
+ def is_buildapp_in(cls, apps):
+ """Must have a build for one of the given app"""
+ for app in apps:
+ attr = getattr(MachCommandConditions, "is_{}".format(app), None)
+ if attr and attr(cls):
+ return True
+ return False
+
+
+class PathArgument(object):
+ """Parse a filesystem path argument and transform it in various ways."""
+
+ def __init__(self, arg, topsrcdir, topobjdir, cwd=None):
+ self.arg = arg
+ self.topsrcdir = topsrcdir
+ self.topobjdir = topobjdir
+ self.cwd = os.getcwd() if cwd is None else cwd
+
+ def relpath(self):
+ """Return a path relative to the topsrcdir or topobjdir.
+
+ If the argument is a path to a location in one of the base directories
+ (topsrcdir or topobjdir), then strip off the base directory part and
+ just return the path within the base directory."""
+
+ abspath = os.path.abspath(os.path.join(self.cwd, self.arg))
+
+ # If that path is within topsrcdir or topobjdir, return an equivalent
+ # path relative to that base directory.
+ for base_dir in [self.topobjdir, self.topsrcdir]:
+ if abspath.startswith(os.path.abspath(base_dir)):
+ return mozpath.relpath(abspath, base_dir)
+
+ return mozpath.normsep(self.arg)
+
+ def srcdir_path(self):
+ return mozpath.join(self.topsrcdir, self.relpath())
+
+ def objdir_path(self):
+ return mozpath.join(self.topobjdir, self.relpath())
+
+
+class ExecutionSummary(dict):
+ """Helper for execution summaries."""
+
+ def __init__(self, summary_format, **data):
+ self._summary_format = ""
+ assert "execution_time" in data
+ self.extend(summary_format, **data)
+
+ def extend(self, summary_format, **data):
+ self._summary_format += summary_format
+ self.update(data)
+
+ def __str__(self):
+ return self._summary_format.format(**self)
+
+ def __getattr__(self, key):
+ return self[key]