diff options
Diffstat (limited to 'python/mozbuild/mozbuild/util.py')
-rw-r--r-- | python/mozbuild/mozbuild/util.py | 1407 |
1 files changed, 1407 insertions, 0 deletions
diff --git a/python/mozbuild/mozbuild/util.py b/python/mozbuild/mozbuild/util.py new file mode 100644 index 0000000000..c1f24445ea --- /dev/null +++ b/python/mozbuild/mozbuild/util.py @@ -0,0 +1,1407 @@ +# 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/. + +# This file contains miscellaneous utility functions that don't belong anywhere +# in particular. + +import argparse +import collections +import collections.abc +import copy +import ctypes +import difflib +import errno +import functools +import hashlib +import io +import itertools +import os +import re +import stat +import sys +import time +from collections import OrderedDict +from io import BytesIO, StringIO +from pathlib import Path + +import six +from packaging.version import Version + +MOZBUILD_METRICS_PATH = os.path.abspath( + os.path.join(__file__, "..", "..", "metrics.yaml") +) + +if sys.platform == "win32": + _kernel32 = ctypes.windll.kernel32 + _FILE_ATTRIBUTE_NOT_CONTENT_INDEXED = 0x2000 + system_encoding = "mbcs" +else: + system_encoding = "utf-8" + + +def exec_(object, globals=None, locals=None): + """Wrapper around the exec statement to avoid bogus errors like: + + SyntaxError: unqualified exec is not allowed in function ... + it is a nested function. + + or + + SyntaxError: unqualified exec is not allowed in function ... + it contains a nested function with free variable + + which happen with older versions of python 2.7. + """ + exec(object, globals, locals) + + +def _open(path, mode): + if "b" in mode: + return io.open(path, mode) + return io.open(path, mode, encoding="utf-8", newline="\n") + + +def hash_file(path, hasher=None): + """Hashes a file specified by the path given and returns the hex digest.""" + + # If the default hashing function changes, this may invalidate + # lots of cached data. Don't change it lightly. + h = hasher or hashlib.sha1() + + with open(path, "rb") as fh: + while True: + data = fh.read(8192) + + if not len(data): + break + + h.update(data) + + return h.hexdigest() + + +class EmptyValue(six.text_type): + """A dummy type that behaves like an empty string and sequence. + + This type exists in order to support + :py:class:`mozbuild.frontend.reader.EmptyConfig`. It should likely not be + used elsewhere. + """ + + def __init__(self): + super(EmptyValue, self).__init__() + + +class ReadOnlyNamespace(object): + """A class for objects with immutable attributes set at initialization.""" + + def __init__(self, **kwargs): + for k, v in six.iteritems(kwargs): + super(ReadOnlyNamespace, self).__setattr__(k, v) + + def __delattr__(self, key): + raise Exception("Object does not support deletion.") + + def __setattr__(self, key, value): + raise Exception("Object does not support assignment.") + + def __ne__(self, other): + return not (self == other) + + def __eq__(self, other): + return self is other or ( + hasattr(other, "__dict__") and self.__dict__ == other.__dict__ + ) + + def __repr__(self): + return "<%s %r>" % (self.__class__.__name__, self.__dict__) + + +class ReadOnlyDict(dict): + """A read-only dictionary.""" + + def __init__(self, *args, **kwargs): + dict.__init__(self, *args, **kwargs) + + def __delitem__(self, key): + raise Exception("Object does not support deletion.") + + def __setitem__(self, key, value): + raise Exception("Object does not support assignment.") + + def update(self, *args, **kwargs): + raise Exception("Object does not support update.") + + def __copy__(self, *args, **kwargs): + return ReadOnlyDict(**dict.copy(self, *args, **kwargs)) + + def __deepcopy__(self, memo): + result = {} + for k, v in self.items(): + result[k] = copy.deepcopy(v, memo) + + return ReadOnlyDict(**result) + + +class undefined_default(object): + """Represents an undefined argument value that isn't None.""" + + +undefined = undefined_default() + + +class ReadOnlyDefaultDict(ReadOnlyDict): + """A read-only dictionary that supports default values on retrieval.""" + + def __init__(self, default_factory, *args, **kwargs): + ReadOnlyDict.__init__(self, *args, **kwargs) + self._default_factory = default_factory + + def __missing__(self, key): + value = self._default_factory() + dict.__setitem__(self, key, value) + return value + + +def ensureParentDir(path): + """Ensures the directory parent to the given file exists.""" + d = os.path.dirname(path) + if d and not os.path.exists(path): + try: + os.makedirs(d) + except OSError as error: + if error.errno != errno.EEXIST: + raise + + +def mkdir(path, not_indexed=False): + """Ensure a directory exists. + + If ``not_indexed`` is True, an attribute is set that disables content + indexing on the directory. + """ + try: + os.makedirs(path) + except OSError as e: + if e.errno != errno.EEXIST: + raise + + if not_indexed: + if sys.platform == "win32": + if isinstance(path, six.string_types): + fn = _kernel32.SetFileAttributesW + else: + fn = _kernel32.SetFileAttributesA + + fn(path, _FILE_ATTRIBUTE_NOT_CONTENT_INDEXED) + elif sys.platform == "darwin": + with open(os.path.join(path, ".metadata_never_index"), "a"): + pass + + +def simple_diff(filename, old_lines, new_lines): + """Returns the diff between old_lines and new_lines, in unified diff form, + as a list of lines. + + old_lines and new_lines are lists of non-newline terminated lines to + compare. + old_lines can be None, indicating a file creation. + new_lines can be None, indicating a file deletion. + """ + + old_name = "/dev/null" if old_lines is None else filename + new_name = "/dev/null" if new_lines is None else filename + + return difflib.unified_diff( + old_lines or [], new_lines or [], old_name, new_name, n=4, lineterm="" + ) + + +class FileAvoidWrite(BytesIO): + """File-like object that buffers output and only writes if content changed. + + We create an instance from an existing filename. New content is written to + it. When we close the file object, if the content in the in-memory buffer + differs from what is on disk, then we write out the new content. Otherwise, + the original file is untouched. + + Instances can optionally capture diffs of file changes. This feature is not + enabled by default because it a) doesn't make sense for binary files b) + could add unwanted overhead to calls. + + Additionally, there is dry run mode where the file is not actually written + out, but reports whether the file was existing and would have been updated + still occur, as well as diff capture if requested. + """ + + def __init__(self, filename, capture_diff=False, dry_run=False, readmode="r"): + BytesIO.__init__(self) + self.name = filename + assert type(capture_diff) == bool + assert type(dry_run) == bool + assert "r" in readmode + self._capture_diff = capture_diff + self._write_to_file = not dry_run + self.diff = None + self.mode = readmode + self._binary_mode = "b" in readmode + + def write(self, buf): + BytesIO.write(self, six.ensure_binary(buf)) + + def avoid_writing_to_file(self): + self._write_to_file = False + + def close(self): + """Stop accepting writes, compare file contents, and rewrite if needed. + + Returns a tuple of bools indicating what action was performed: + + (file existed, file updated) + + If ``capture_diff`` was specified at construction time and the + underlying file was changed, ``.diff`` will be populated with the diff + of the result. + """ + # Use binary data if the caller explicitly asked for it. + ensure = six.ensure_binary if self._binary_mode else six.ensure_text + buf = ensure(self.getvalue()) + + BytesIO.close(self) + existed = False + old_content = None + + try: + existing = _open(self.name, self.mode) + existed = True + except IOError: + pass + else: + try: + old_content = existing.read() + if old_content == buf: + return True, False + except IOError: + pass + finally: + existing.close() + + if self._write_to_file: + ensureParentDir(self.name) + # Maintain 'b' if specified. 'U' only applies to modes starting with + # 'r', so it is dropped. + writemode = "w" + if self._binary_mode: + writemode += "b" + buf = six.ensure_binary(buf) + else: + buf = six.ensure_text(buf) + with _open(self.name, writemode) as file: + file.write(buf) + + self._generate_diff(buf, old_content) + + return existed, True + + def _generate_diff(self, new_content, old_content): + """Generate a diff for the changed contents if `capture_diff` is True. + + If the changed contents could not be decoded as utf-8 then generate a + placeholder message instead of a diff. + + Args: + new_content: Str or bytes holding the new file contents. + old_content: Str or bytes holding the original file contents. Should be + None if no old content is being overwritten. + """ + if not self._capture_diff: + return + + try: + if old_content is None: + old_lines = None + else: + if self._binary_mode: + # difflib doesn't work with bytes. + old_content = old_content.decode("utf-8") + + old_lines = old_content.splitlines() + + if self._binary_mode: + # difflib doesn't work with bytes. + new_content = new_content.decode("utf-8") + + new_lines = new_content.splitlines() + + self.diff = simple_diff(self.name, old_lines, new_lines) + # FileAvoidWrite isn't unicode/bytes safe. So, files with non-ascii + # content or opened and written in different modes may involve + # implicit conversion and this will make Python unhappy. Since + # diffing isn't a critical feature, we just ignore the failure. + # This can go away once FileAvoidWrite uses io.BytesIO and + # io.StringIO. But that will require a lot of work. + except (UnicodeDecodeError, UnicodeEncodeError): + self.diff = ["Binary or non-ascii file changed: %s" % self.name] + + def __enter__(self): + return self + + def __exit__(self, type, value, traceback): + if not self.closed: + self.close() + + +def resolve_target_to_make(topobjdir, target): + r""" + Resolve `target` (a target, directory, or file) to a make target. + + `topobjdir` is the object directory; all make targets will be + rooted at or below the top-level Makefile in this directory. + + Returns a pair `(reldir, target)` where `reldir` is a directory + relative to `topobjdir` containing a Makefile and `target` is a + make target (possibly `None`). + + A directory resolves to the nearest directory at or above + containing a Makefile, and target `None`. + + A regular (non-Makefile) file resolves to the nearest directory at + or above the file containing a Makefile, and an appropriate + target. + + A Makefile resolves to the nearest parent strictly above the + Makefile containing a different Makefile, and an appropriate + target. + """ + + target = target.replace(os.sep, "/").lstrip("/") + abs_target = os.path.join(topobjdir, target) + + # For directories, run |make -C dir|. If the directory does not + # contain a Makefile, check parents until we find one. At worst, + # this will terminate at the root. + if os.path.isdir(abs_target): + current = abs_target + + while True: + make_path = os.path.join(current, "Makefile") + if os.path.exists(make_path): + return (current[len(topobjdir) + 1 :], None) + + current = os.path.dirname(current) + + # If it's not in a directory, this is probably a top-level make + # target. Treat it as such. + if "/" not in target: + return (None, target) + + # We have a relative path within the tree. We look for a Makefile + # as far into the path as possible. Then, we compute the make + # target as relative to that directory. + reldir = os.path.dirname(target) + target = os.path.basename(target) + + while True: + make_path = os.path.join(topobjdir, reldir, "Makefile") + + # We append to target every iteration, so the check below + # happens exactly once. + if target != "Makefile" and os.path.exists(make_path): + return (reldir, target) + + target = os.path.join(os.path.basename(reldir), target) + reldir = os.path.dirname(reldir) + + +class List(list): + """A list specialized for moz.build environments. + + We overload the assignment and append operations to require that the + appended thing is a list. This avoids bad surprises coming from appending + a string to a list, which would just add each letter of the string. + """ + + def __init__(self, iterable=None, **kwargs): + if iterable is None: + iterable = [] + if not isinstance(iterable, list): + raise ValueError("List can only be created from other list instances.") + + self._kwargs = kwargs + super(List, self).__init__(iterable) + + def extend(self, l): + if not isinstance(l, list): + raise ValueError("List can only be extended with other list instances.") + + return super(List, self).extend(l) + + def __setitem__(self, key, val): + if isinstance(key, slice): + if not isinstance(val, list): + raise ValueError( + "List can only be sliced with other list " "instances." + ) + if key.step: + raise ValueError("List cannot be sliced with a nonzero step " "value") + # Python 2 and Python 3 do this differently for some reason. + if six.PY2: + return super(List, self).__setslice__(key.start, key.stop, val) + else: + return super(List, self).__setitem__(key, val) + return super(List, self).__setitem__(key, val) + + def __setslice__(self, i, j, sequence): + return self.__setitem__(slice(i, j), sequence) + + def __add__(self, other): + # Allow None and EmptyValue is a special case because it makes undefined + # variable references in moz.build behave better. + other = [] if isinstance(other, (type(None), EmptyValue)) else other + if not isinstance(other, list): + raise ValueError("Only lists can be appended to lists.") + + new_list = self.__class__(self, **self._kwargs) + new_list.extend(other) + return new_list + + def __iadd__(self, other): + other = [] if isinstance(other, (type(None), EmptyValue)) else other + if not isinstance(other, list): + raise ValueError("Only lists can be appended to lists.") + + return super(List, self).__iadd__(other) + + +class UnsortedError(Exception): + def __init__(self, srtd, original): + assert len(srtd) == len(original) + + self.sorted = srtd + self.original = original + + for i, orig in enumerate(original): + s = srtd[i] + + if orig != s: + self.i = i + break + + def __str__(self): + s = StringIO() + + s.write("An attempt was made to add an unsorted sequence to a list. ") + s.write("The incoming list is unsorted starting at element %d. " % self.i) + s.write( + 'We expected "%s" but got "%s"' + % (self.sorted[self.i], self.original[self.i]) + ) + + return s.getvalue() + + +class StrictOrderingOnAppendList(List): + """A list specialized for moz.build environments. + + We overload the assignment and append operations to require that incoming + elements be ordered. This enforces cleaner style in moz.build files. + """ + + @staticmethod + def ensure_sorted(l): + if isinstance(l, StrictOrderingOnAppendList): + return + + def _first_element(e): + # If the list entry is a tuple, we sort based on the first element + # in the tuple. + return e[0] if isinstance(e, tuple) else e + + srtd = sorted(l, key=lambda x: _first_element(x).lower()) + + if srtd != l: + raise UnsortedError(srtd, l) + + def __init__(self, iterable=None, **kwargs): + if iterable is None: + iterable = [] + + StrictOrderingOnAppendList.ensure_sorted(iterable) + + super(StrictOrderingOnAppendList, self).__init__(iterable, **kwargs) + + def extend(self, l): + StrictOrderingOnAppendList.ensure_sorted(l) + + return super(StrictOrderingOnAppendList, self).extend(l) + + def __setitem__(self, key, val): + if isinstance(key, slice): + StrictOrderingOnAppendList.ensure_sorted(val) + return super(StrictOrderingOnAppendList, self).__setitem__(key, val) + + def __add__(self, other): + StrictOrderingOnAppendList.ensure_sorted(other) + + return super(StrictOrderingOnAppendList, self).__add__(other) + + def __iadd__(self, other): + StrictOrderingOnAppendList.ensure_sorted(other) + + return super(StrictOrderingOnAppendList, self).__iadd__(other) + + +class ImmutableStrictOrderingOnAppendList(StrictOrderingOnAppendList): + """Like StrictOrderingOnAppendList, but not allowing mutations of the value.""" + + def append(self, elt): + raise Exception("cannot use append on this type") + + def extend(self, iterable): + raise Exception("cannot use extend on this type") + + def __setslice__(self, i, j, iterable): + raise Exception("cannot assign to slices on this type") + + def __setitem__(self, i, elt): + raise Exception("cannot assign to indexes on this type") + + def __iadd__(self, other): + raise Exception("cannot use += on this type") + + +class StrictOrderingOnAppendListWithAction(StrictOrderingOnAppendList): + """An ordered list that accepts a callable to be applied to each item. + + A callable (action) passed to the constructor is run on each item of input. + The result of running the callable on each item will be stored in place of + the original input, but the original item must be used to enforce sortedness. + """ + + def __init__(self, iterable=(), action=None): + if not callable(action): + raise ValueError( + "A callable action is required to construct " + "a StrictOrderingOnAppendListWithAction" + ) + + self._action = action + if not isinstance(iterable, (tuple, list)): + raise ValueError( + "StrictOrderingOnAppendListWithAction can only be initialized " + "with another list" + ) + iterable = [self._action(i) for i in iterable] + super(StrictOrderingOnAppendListWithAction, self).__init__( + iterable, action=action + ) + + def extend(self, l): + if not isinstance(l, list): + raise ValueError( + "StrictOrderingOnAppendListWithAction can only be extended " + "with another list" + ) + l = [self._action(i) for i in l] + return super(StrictOrderingOnAppendListWithAction, self).extend(l) + + def __setitem__(self, key, val): + if isinstance(key, slice): + if not isinstance(val, list): + raise ValueError( + "StrictOrderingOnAppendListWithAction can only be sliced " + "with another list" + ) + val = [self._action(item) for item in val] + return super(StrictOrderingOnAppendListWithAction, self).__setitem__(key, val) + + def __add__(self, other): + if not isinstance(other, list): + raise ValueError( + "StrictOrderingOnAppendListWithAction can only be added with " + "another list" + ) + return super(StrictOrderingOnAppendListWithAction, self).__add__(other) + + def __iadd__(self, other): + if not isinstance(other, list): + raise ValueError( + "StrictOrderingOnAppendListWithAction can only be added with " + "another list" + ) + other = [self._action(i) for i in other] + return super(StrictOrderingOnAppendListWithAction, self).__iadd__(other) + + +class MozbuildDeletionError(Exception): + pass + + +def FlagsFactory(flags): + """Returns a class which holds optional flags for an item in a list. + + The flags are defined in the dict given as argument, where keys are + the flag names, and values the type used for the value of that flag. + + The resulting class is used by the various <TypeName>WithFlagsFactory + functions below. + """ + assert isinstance(flags, dict) + assert all(isinstance(v, type) for v in flags.values()) + + class Flags(object): + __slots__ = flags.keys() + _flags = flags + + def update(self, **kwargs): + for k, v in six.iteritems(kwargs): + setattr(self, k, v) + + def __getattr__(self, name): + if name not in self.__slots__: + raise AttributeError( + "'%s' object has no attribute '%s'" + % (self.__class__.__name__, name) + ) + try: + return object.__getattr__(self, name) + except AttributeError: + value = self._flags[name]() + self.__setattr__(name, value) + return value + + def __setattr__(self, name, value): + if name not in self.__slots__: + raise AttributeError( + "'%s' object has no attribute '%s'" + % (self.__class__.__name__, name) + ) + if not isinstance(value, self._flags[name]): + raise TypeError( + "'%s' attribute of class '%s' must be '%s'" + % (name, self.__class__.__name__, self._flags[name].__name__) + ) + return object.__setattr__(self, name, value) + + def __delattr__(self, name): + raise MozbuildDeletionError("Unable to delete attributes for this object") + + return Flags + + +class StrictOrderingOnAppendListWithFlags(StrictOrderingOnAppendList): + """A list with flags specialized for moz.build environments. + + Each subclass has a set of typed flags; this class lets us use `isinstance` + for natural testing. + """ + + +def StrictOrderingOnAppendListWithFlagsFactory(flags): + """Returns a StrictOrderingOnAppendList-like object, with optional + flags on each item. + + The flags are defined in the dict given as argument, where keys are + the flag names, and values the type used for the value of that flag. + + Example: + + .. code-block:: python + + FooList = StrictOrderingOnAppendListWithFlagsFactory({ + 'foo': bool, 'bar': unicode + }) + foo = FooList(['a', 'b', 'c']) + foo['a'].foo = True + foo['b'].bar = 'bar' + """ + + class StrictOrderingOnAppendListWithFlagsSpecialization( + StrictOrderingOnAppendListWithFlags + ): + def __init__(self, iterable=None): + if iterable is None: + iterable = [] + StrictOrderingOnAppendListWithFlags.__init__(self, iterable) + self._flags_type = FlagsFactory(flags) + self._flags = dict() + + def __getitem__(self, name): + if name not in self._flags: + if name not in self: + raise KeyError("'%s'" % name) + self._flags[name] = self._flags_type() + return self._flags[name] + + def __setitem__(self, name, value): + if not isinstance(name, slice): + raise TypeError( + "'%s' object does not support item assignment" + % self.__class__.__name__ + ) + result = super( + StrictOrderingOnAppendListWithFlagsSpecialization, self + ).__setitem__(name, value) + # We may have removed items. + for k in set(self._flags.keys()) - set(self): + del self._flags[k] + if isinstance(value, StrictOrderingOnAppendListWithFlags): + self._update_flags(value) + return result + + def _update_flags(self, other): + if self._flags_type._flags != other._flags_type._flags: + raise ValueError( + "Expected a list of strings with flags like %s, not like %s" + % (self._flags_type._flags, other._flags_type._flags) + ) + intersection = set(self._flags.keys()) & set(other._flags.keys()) + if intersection: + raise ValueError( + "Cannot update flags: both lists of strings with flags configure %s" + % intersection + ) + self._flags.update(other._flags) + + def extend(self, l): + result = super( + StrictOrderingOnAppendListWithFlagsSpecialization, self + ).extend(l) + if isinstance(l, StrictOrderingOnAppendListWithFlags): + self._update_flags(l) + return result + + def __add__(self, other): + result = super( + StrictOrderingOnAppendListWithFlagsSpecialization, self + ).__add__(other) + if isinstance(other, StrictOrderingOnAppendListWithFlags): + # Result has flags from other but not from self, since + # internally we duplicate self and then extend with other, and + # only extend knows about flags. Since we don't allow updating + # when the set of flag keys intersect, which we instance we pass + # to _update_flags here matters. This needs to be correct but + # is an implementation detail. + result._update_flags(self) + return result + + def __iadd__(self, other): + result = super( + StrictOrderingOnAppendListWithFlagsSpecialization, self + ).__iadd__(other) + if isinstance(other, StrictOrderingOnAppendListWithFlags): + self._update_flags(other) + return result + + return StrictOrderingOnAppendListWithFlagsSpecialization + + +class HierarchicalStringList(object): + """A hierarchy of lists of strings. + + Each instance of this object contains a list of strings, which can be set or + appended to. A sub-level of the hierarchy is also an instance of this class, + can be added by appending to an attribute instead. + + For example, the moz.build variable EXPORTS is an instance of this class. We + can do: + + EXPORTS += ['foo.h'] + EXPORTS.mozilla.dom += ['bar.h'] + + In this case, we have 3 instances (EXPORTS, EXPORTS.mozilla, and + EXPORTS.mozilla.dom), and the first and last each have one element in their + list. + """ + + __slots__ = ("_strings", "_children") + + def __init__(self): + # Please change ContextDerivedTypedHierarchicalStringList in context.py + # if you make changes here. + self._strings = StrictOrderingOnAppendList() + self._children = {} + + class StringListAdaptor(collections.abc.Sequence): + def __init__(self, hsl): + self._hsl = hsl + + def __getitem__(self, index): + return self._hsl._strings[index] + + def __len__(self): + return len(self._hsl._strings) + + def walk(self): + """Walk over all HierarchicalStringLists in the hierarchy. + + This is a generator of (path, sequence). + + The path is '' for the root level and '/'-delimited strings for + any descendants. The sequence is a read-only sequence of the + strings contained at that level. + """ + + if self._strings: + path_to_here = "" + yield path_to_here, self.StringListAdaptor(self) + + for k, l in sorted(self._children.items()): + for p, v in l.walk(): + path_to_there = "%s/%s" % (k, p) + yield path_to_there.strip("/"), v + + def __setattr__(self, name, value): + if name in self.__slots__: + return object.__setattr__(self, name, value) + + # __setattr__ can be called with a list when a simple assignment is + # used: + # + # EXPORTS.foo = ['file.h'] + # + # In this case, we need to overwrite foo's current list of strings. + # + # However, __setattr__ is also called with a HierarchicalStringList + # to try to actually set the attribute. We want to ignore this case, + # since we don't actually create an attribute called 'foo', but just add + # it to our list of children (using _get_exportvariable()). + self._set_exportvariable(name, value) + + def __getattr__(self, name): + if name.startswith("__"): + return object.__getattr__(self, name) + return self._get_exportvariable(name) + + def __delattr__(self, name): + raise MozbuildDeletionError("Unable to delete attributes for this object") + + def __iadd__(self, other): + if isinstance(other, HierarchicalStringList): + self._strings += other._strings + for c in other._children: + self[c] += other[c] + else: + self._check_list(other) + self._strings += other + return self + + def __getitem__(self, name): + return self._get_exportvariable(name) + + def __setitem__(self, name, value): + self._set_exportvariable(name, value) + + def _get_exportvariable(self, name): + # Please change ContextDerivedTypedHierarchicalStringList in context.py + # if you make changes here. + child = self._children.get(name) + if not child: + child = self._children[name] = HierarchicalStringList() + return child + + def _set_exportvariable(self, name, value): + if name in self._children: + if value is self._get_exportvariable(name): + return + raise KeyError("global_ns", "reassign", "<some variable>.%s" % name) + + exports = self._get_exportvariable(name) + exports._check_list(value) + exports._strings += value + + def _check_list(self, value): + if not isinstance(value, list): + raise ValueError("Expected a list of strings, not %s" % type(value)) + for v in value: + if not isinstance(v, six.string_types): + raise ValueError( + "Expected a list of strings, not an element of %s" % type(v) + ) + + +class LockFile(object): + """LockFile is used by the lock_file method to hold the lock. + + This object should not be used directly, but only through + the lock_file method below. + """ + + def __init__(self, lockfile): + self.lockfile = lockfile + + def __del__(self): + while True: + try: + os.remove(self.lockfile) + break + except OSError as e: + if e.errno == errno.EACCES: + # Another process probably has the file open, we'll retry. + # Just a short sleep since we want to drop the lock ASAP + # (but we need to let some other process close the file + # first). + time.sleep(0.1) + else: + # Re-raise unknown errors + raise + + +def lock_file(lockfile, max_wait=600): + """Create and hold a lockfile of the given name, with the given timeout. + + To release the lock, delete the returned object. + """ + + # FUTURE This function and object could be written as a context manager. + + while True: + try: + fd = os.open(lockfile, os.O_EXCL | os.O_RDWR | os.O_CREAT) + # We created the lockfile, so we're the owner + break + except OSError as e: + if e.errno == errno.EEXIST or ( + sys.platform == "win32" and e.errno == errno.EACCES + ): + pass + else: + # Should not occur + raise + + try: + # The lock file exists, try to stat it to get its age + # and read its contents to report the owner PID + f = open(lockfile, "r") + s = os.stat(lockfile) + except EnvironmentError as e: + if e.errno == errno.ENOENT or e.errno == errno.EACCES: + # We didn't create the lockfile, so it did exist, but it's + # gone now. Just try again + continue + + raise Exception( + "{0} exists but stat() failed: {1}".format(lockfile, e.strerror) + ) + + # We didn't create the lockfile and it's still there, check + # its age + now = int(time.time()) + if now - s[stat.ST_MTIME] > max_wait: + pid = f.readline().rstrip() + raise Exception( + "{0} has been locked for more than " + "{1} seconds (PID {2})".format(lockfile, max_wait, pid) + ) + + # It's not been locked too long, wait a while and retry + f.close() + time.sleep(1) + + # if we get here. we have the lockfile. Convert the os.open file + # descriptor into a Python file object and record our PID in it + f = os.fdopen(fd, "w") + f.write("{0}\n".format(os.getpid())) + f.close() + + return LockFile(lockfile) + + +class OrderedDefaultDict(OrderedDict): + """A combination of OrderedDict and defaultdict.""" + + def __init__(self, default_factory, *args, **kwargs): + OrderedDict.__init__(self, *args, **kwargs) + self._default_factory = default_factory + + def __missing__(self, key): + value = self[key] = self._default_factory() + return value + + +class KeyedDefaultDict(dict): + """Like a defaultdict, but the default_factory function takes the key as + argument""" + + def __init__(self, default_factory, *args, **kwargs): + dict.__init__(self, *args, **kwargs) + self._default_factory = default_factory + + def __missing__(self, key): + value = self._default_factory(key) + dict.__setitem__(self, key, value) + return value + + +class ReadOnlyKeyedDefaultDict(KeyedDefaultDict, ReadOnlyDict): + """Like KeyedDefaultDict, but read-only.""" + + +class memoize(dict): + """A decorator to memoize the results of function calls depending + on its arguments. + Both functions and instance methods are handled, although in the + instance method case, the results are cache in the instance itself. + """ + + def __init__(self, func): + self.func = func + functools.update_wrapper(self, func) + + def __call__(self, *args): + if args not in self: + self[args] = self.func(*args) + return self[args] + + def method_call(self, instance, *args): + name = "_%s" % self.func.__name__ + if not hasattr(instance, name): + setattr(instance, name, {}) + cache = getattr(instance, name) + if args not in cache: + cache[args] = self.func(instance, *args) + return cache[args] + + def __get__(self, instance, cls): + return functools.update_wrapper( + functools.partial(self.method_call, instance), self.func + ) + + +class memoized_property(object): + """A specialized version of the memoize decorator that works for + class instance properties. + """ + + def __init__(self, func): + self.func = func + + def __get__(self, instance, cls): + name = "_%s" % self.func.__name__ + if not hasattr(instance, name): + setattr(instance, name, self.func(instance)) + return getattr(instance, name) + + +def TypedNamedTuple(name, fields): + """Factory for named tuple types with strong typing. + + Arguments are an iterable of 2-tuples. The first member is the + the field name. The second member is a type the field will be validated + to be. + + Construction of instances varies from ``collections.namedtuple``. + + First, if a single tuple argument is given to the constructor, this is + treated as the equivalent of passing each tuple value as a separate + argument into __init__. e.g.:: + + t = (1, 2) + TypedTuple(t) == TypedTuple(1, 2) + + This behavior is meant for moz.build files, so vanilla tuples are + automatically cast to typed tuple instances. + + Second, fields in the tuple are validated to be instances of the specified + type. This is done via an ``isinstance()`` check. To allow multiple types, + pass a tuple as the allowed types field. + """ + cls = collections.namedtuple(name, (name for name, typ in fields)) + + class TypedTuple(cls): + __slots__ = () + + def __new__(klass, *args, **kwargs): + if len(args) == 1 and not kwargs and isinstance(args[0], tuple): + args = args[0] + + return super(TypedTuple, klass).__new__(klass, *args, **kwargs) + + def __init__(self, *args, **kwargs): + for i, (fname, ftype) in enumerate(self._fields): + value = self[i] + + if not isinstance(value, ftype): + raise TypeError( + "field in tuple not of proper type: %s; " + "got %s, expected %s" % (fname, type(value), ftype) + ) + + TypedTuple._fields = fields + + return TypedTuple + + +@memoize +def TypedList(type, base_class=List): + """A list with type coercion. + + The given ``type`` is what list elements are being coerced to. It may do + strict validation, throwing ValueError exceptions. + + A ``base_class`` type can be given for more specific uses than a List. For + example, a Typed StrictOrderingOnAppendList can be created with: + + TypedList(unicode, StrictOrderingOnAppendList) + """ + + class _TypedList(base_class): + @staticmethod + def normalize(e): + if not isinstance(e, type): + e = type(e) + return e + + def _ensure_type(self, l): + if isinstance(l, self.__class__): + return l + + return [self.normalize(e) for e in l] + + def __init__(self, iterable=None, **kwargs): + if iterable is None: + iterable = [] + iterable = self._ensure_type(iterable) + + super(_TypedList, self).__init__(iterable, **kwargs) + + def extend(self, l): + l = self._ensure_type(l) + + return super(_TypedList, self).extend(l) + + def __setitem__(self, key, val): + val = self._ensure_type(val) + + return super(_TypedList, self).__setitem__(key, val) + + def __add__(self, other): + other = self._ensure_type(other) + + return super(_TypedList, self).__add__(other) + + def __iadd__(self, other): + other = self._ensure_type(other) + + return super(_TypedList, self).__iadd__(other) + + def append(self, other): + self += [other] + + return _TypedList + + +def group_unified_files(files, unified_prefix, unified_suffix, files_per_unified_file): + """Return an iterator of (unified_filename, source_filenames) tuples. + + We compile most C and C++ files in "unified mode"; instead of compiling + ``a.cpp``, ``b.cpp``, and ``c.cpp`` separately, we compile a single file + that looks approximately like:: + + #include "a.cpp" + #include "b.cpp" + #include "c.cpp" + + This function handles the details of generating names for the unified + files, and determining which original source files go in which unified + file.""" + + # Our last returned list of source filenames may be short, and we + # don't want the fill value inserted by zip_longest to be an + # issue. So we do a little dance to filter it out ourselves. + dummy_fill_value = ("dummy",) + + def filter_out_dummy(iterable): + return six.moves.filter(lambda x: x != dummy_fill_value, iterable) + + # From the itertools documentation, slightly modified: + def grouper(n, iterable): + "grouper(3, 'ABCDEFG', 'x') --> ABC DEF Gxx" + args = [iter(iterable)] * n + return six.moves.zip_longest(fillvalue=dummy_fill_value, *args) + + for i, unified_group in enumerate(grouper(files_per_unified_file, files)): + just_the_filenames = list(filter_out_dummy(unified_group)) + yield "%s%d.%s" % (unified_prefix, i, unified_suffix), just_the_filenames + + +def pair(iterable): + """Given an iterable, returns an iterable pairing its items. + + For example, + list(pair([1,2,3,4,5,6])) + returns + [(1,2), (3,4), (5,6)] + """ + i = iter(iterable) + return six.moves.zip_longest(i, i) + + +def pairwise(iterable): + """Given an iterable, returns an iterable of overlapped pairs of + its items. Based on the Python itertools documentation. + + For example, + list(pairwise([1,2,3,4,5,6])) + returns + [(1,2), (2,3), (3,4), (4,5), (5,6)] + """ + a, b = itertools.tee(iterable) + next(b, None) + return zip(a, b) + + +VARIABLES_RE = re.compile("\$\((\w+)\)") + + +def expand_variables(s, variables): + """Given a string with $(var) variable references, replace those references + with the corresponding entries from the given `variables` dict. + + If a variable value is not a string, it is iterated and its items are + joined with a whitespace.""" + result = "" + for s, name in pair(VARIABLES_RE.split(s)): + result += s + value = variables.get(name) + if not value: + continue + if not isinstance(value, six.string_types): + value = " ".join(value) + result += value + return result + + +class DefinesAction(argparse.Action): + """An ArgumentParser action to handle -Dvar[=value] type of arguments.""" + + def __call__(self, parser, namespace, values, option_string): + defines = getattr(namespace, self.dest) + if defines is None: + defines = {} + values = values.split("=", 1) + if len(values) == 1: + name, value = values[0], 1 + else: + name, value = values + if value.isdigit(): + value = int(value) + defines[name] = value + setattr(namespace, self.dest, defines) + + +class EnumStringComparisonError(Exception): + pass + + +class EnumString(six.text_type): + """A string type that only can have a limited set of values, similarly to + an Enum, and can only be compared against that set of values. + + The class is meant to be subclassed, where the subclass defines + POSSIBLE_VALUES. The `subclass` method is a helper to create such + subclasses. + """ + + POSSIBLE_VALUES = () + + def __init__(self, value): + if value not in self.POSSIBLE_VALUES: + raise ValueError( + "'%s' is not a valid value for %s" % (value, self.__class__.__name__) + ) + + def __eq__(self, other): + if other not in self.POSSIBLE_VALUES: + raise EnumStringComparisonError( + "Can only compare with %s" + % ", ".join("'%s'" % v for v in self.POSSIBLE_VALUES) + ) + return super(EnumString, self).__eq__(other) + + def __ne__(self, other): + return not (self == other) + + def __hash__(self): + return super(EnumString, self).__hash__() + + @staticmethod + def subclass(*possible_values): + class EnumStringSubclass(EnumString): + POSSIBLE_VALUES = possible_values + + return EnumStringSubclass + + +def _escape_char(c): + # str.encode('unicode_espace') doesn't escape quotes, presumably because + # quoting could be done with either ' or ". + if c == "'": + return "\\'" + return six.text_type(c.encode("unicode_escape")) + + +def ensure_bytes(value, encoding="utf-8"): + if isinstance(value, six.text_type): + return value.encode(encoding) + return value + + +def ensure_unicode(value, encoding="utf-8"): + if isinstance(value, six.binary_type): + return value.decode(encoding) + return value + + +def process_time(): + if six.PY2: + return time.clock() + else: + return time.process_time() + + +def hexdump(buf): + """ + Returns a list of hexdump-like lines corresponding to the given input buffer. + """ + assert six.PY3 + off_format = "%0{}x ".format(len(str(len(buf)))) + lines = [] + for off in range(0, len(buf), 16): + line = off_format % off + chunk = buf[off : min(off + 16, len(buf))] + for n, byte in enumerate(chunk): + line += " %02x" % byte + if n == 7: + line += " " + for n in range(len(chunk), 16): + line += " " + if n == 7: + line += " " + line += " |" + for byte in chunk: + if byte < 127 and byte >= 32: + line += chr(byte) + else: + line += "." + for n in range(len(chunk), 16): + line += " " + line += "|\n" + lines.append(line) + return lines + + +def mozilla_build_version(): + mozilla_build = os.environ.get("MOZILLABUILD") + + version_file = Path(mozilla_build) / "VERSION" + + assert version_file.exists(), ( + f'The MozillaBuild VERSION file was not found at "{version_file}".\n' + "Please check if MozillaBuild is installed correctly and that the" + "`MOZILLABUILD` environment variable is to the correct path." + ) + + with version_file.open() as file: + return Version(file.readline().rstrip("\n")) |