1# This Source Code Form is subject to the terms of the Mozilla Public
2# License, v. 2.0. If a copy of the MPL was not distributed with this file,
3# You can obtain one at http://mozilla.org/MPL/2.0/.
4
5# This file contains miscellaneous utility functions that don't belong anywhere
6# in particular.
7
8from __future__ import absolute_import, print_function, unicode_literals
9
10import argparse
11import collections
12import collections.abc
13import ctypes
14import difflib
15import errno
16import functools
17import hashlib
18import io
19import itertools
20import os
21import pprint
22import re
23import stat
24import sys
25import time
26from collections import OrderedDict
27from io import BytesIO, StringIO
28
29import six
30
31MOZBUILD_METRICS_PATH = os.path.abspath(
32    os.path.join(__file__, "..", "..", "metrics.yaml")
33)
34
35if sys.platform == "win32":
36    _kernel32 = ctypes.windll.kernel32
37    _FILE_ATTRIBUTE_NOT_CONTENT_INDEXED = 0x2000
38    system_encoding = "mbcs"
39else:
40    system_encoding = "utf-8"
41
42
43def exec_(object, globals=None, locals=None):
44    """Wrapper around the exec statement to avoid bogus errors like:
45
46    SyntaxError: unqualified exec is not allowed in function ...
47    it is a nested function.
48
49    or
50
51    SyntaxError: unqualified exec is not allowed in function ...
52    it contains a nested function with free variable
53
54    which happen with older versions of python 2.7.
55    """
56    exec(object, globals, locals)
57
58
59def _open(path, mode):
60    if "b" in mode:
61        return io.open(path, mode)
62    return io.open(path, mode, encoding="utf-8", newline="\n")
63
64
65def hash_file(path, hasher=None):
66    """Hashes a file specified by the path given and returns the hex digest."""
67
68    # If the default hashing function changes, this may invalidate
69    # lots of cached data.  Don't change it lightly.
70    h = hasher or hashlib.sha1()
71
72    with open(path, "rb") as fh:
73        while True:
74            data = fh.read(8192)
75
76            if not len(data):
77                break
78
79            h.update(data)
80
81    return h.hexdigest()
82
83
84class EmptyValue(six.text_type):
85    """A dummy type that behaves like an empty string and sequence.
86
87    This type exists in order to support
88    :py:class:`mozbuild.frontend.reader.EmptyConfig`. It should likely not be
89    used elsewhere.
90    """
91
92    def __init__(self):
93        super(EmptyValue, self).__init__()
94
95
96class ReadOnlyNamespace(object):
97    """A class for objects with immutable attributes set at initialization."""
98
99    def __init__(self, **kwargs):
100        for k, v in six.iteritems(kwargs):
101            super(ReadOnlyNamespace, self).__setattr__(k, v)
102
103    def __delattr__(self, key):
104        raise Exception("Object does not support deletion.")
105
106    def __setattr__(self, key, value):
107        raise Exception("Object does not support assignment.")
108
109    def __ne__(self, other):
110        return not (self == other)
111
112    def __eq__(self, other):
113        return self is other or (
114            hasattr(other, "__dict__") and self.__dict__ == other.__dict__
115        )
116
117    def __repr__(self):
118        return "<%s %r>" % (self.__class__.__name__, self.__dict__)
119
120
121class ReadOnlyDict(dict):
122    """A read-only dictionary."""
123
124    def __init__(self, *args, **kwargs):
125        dict.__init__(self, *args, **kwargs)
126
127    def __delitem__(self, key):
128        raise Exception("Object does not support deletion.")
129
130    def __setitem__(self, key, value):
131        raise Exception("Object does not support assignment.")
132
133    def update(self, *args, **kwargs):
134        raise Exception("Object does not support update.")
135
136
137class undefined_default(object):
138    """Represents an undefined argument value that isn't None."""
139
140
141undefined = undefined_default()
142
143
144class ReadOnlyDefaultDict(ReadOnlyDict):
145    """A read-only dictionary that supports default values on retrieval."""
146
147    def __init__(self, default_factory, *args, **kwargs):
148        ReadOnlyDict.__init__(self, *args, **kwargs)
149        self._default_factory = default_factory
150
151    def __missing__(self, key):
152        value = self._default_factory()
153        dict.__setitem__(self, key, value)
154        return value
155
156
157def ensureParentDir(path):
158    """Ensures the directory parent to the given file exists."""
159    d = os.path.dirname(path)
160    if d and not os.path.exists(path):
161        try:
162            os.makedirs(d)
163        except OSError as error:
164            if error.errno != errno.EEXIST:
165                raise
166
167
168def mkdir(path, not_indexed=False):
169    """Ensure a directory exists.
170
171    If ``not_indexed`` is True, an attribute is set that disables content
172    indexing on the directory.
173    """
174    try:
175        os.makedirs(path)
176    except OSError as e:
177        if e.errno != errno.EEXIST:
178            raise
179
180    if not_indexed:
181        if sys.platform == "win32":
182            if isinstance(path, six.string_types):
183                fn = _kernel32.SetFileAttributesW
184            else:
185                fn = _kernel32.SetFileAttributesA
186
187            fn(path, _FILE_ATTRIBUTE_NOT_CONTENT_INDEXED)
188        elif sys.platform == "darwin":
189            with open(os.path.join(path, ".metadata_never_index"), "a"):
190                pass
191
192
193def simple_diff(filename, old_lines, new_lines):
194    """Returns the diff between old_lines and new_lines, in unified diff form,
195    as a list of lines.
196
197    old_lines and new_lines are lists of non-newline terminated lines to
198    compare.
199    old_lines can be None, indicating a file creation.
200    new_lines can be None, indicating a file deletion.
201    """
202
203    old_name = "/dev/null" if old_lines is None else filename
204    new_name = "/dev/null" if new_lines is None else filename
205
206    return difflib.unified_diff(
207        old_lines or [], new_lines or [], old_name, new_name, n=4, lineterm=""
208    )
209
210
211class FileAvoidWrite(BytesIO):
212    """File-like object that buffers output and only writes if content changed.
213
214    We create an instance from an existing filename. New content is written to
215    it. When we close the file object, if the content in the in-memory buffer
216    differs from what is on disk, then we write out the new content. Otherwise,
217    the original file is untouched.
218
219    Instances can optionally capture diffs of file changes. This feature is not
220    enabled by default because it a) doesn't make sense for binary files b)
221    could add unwanted overhead to calls.
222
223    Additionally, there is dry run mode where the file is not actually written
224    out, but reports whether the file was existing and would have been updated
225    still occur, as well as diff capture if requested.
226    """
227
228    def __init__(self, filename, capture_diff=False, dry_run=False, readmode="rU"):
229        BytesIO.__init__(self)
230        self.name = filename
231        assert type(capture_diff) == bool
232        assert type(dry_run) == bool
233        assert "r" in readmode
234        self._capture_diff = capture_diff
235        self._write_to_file = not dry_run
236        self.diff = None
237        self.mode = readmode
238        self._binary_mode = "b" in readmode
239
240    def write(self, buf):
241        BytesIO.write(self, six.ensure_binary(buf))
242
243    def avoid_writing_to_file(self):
244        self._write_to_file = False
245
246    def close(self):
247        """Stop accepting writes, compare file contents, and rewrite if needed.
248
249        Returns a tuple of bools indicating what action was performed:
250
251            (file existed, file updated)
252
253        If ``capture_diff`` was specified at construction time and the
254        underlying file was changed, ``.diff`` will be populated with the diff
255        of the result.
256        """
257        # Use binary data if the caller explicitly asked for it.
258        ensure = six.ensure_binary if self._binary_mode else six.ensure_text
259        buf = ensure(self.getvalue())
260
261        BytesIO.close(self)
262        existed = False
263        old_content = None
264
265        try:
266            existing = _open(self.name, self.mode)
267            existed = True
268        except IOError:
269            pass
270        else:
271            try:
272                old_content = existing.read()
273                if old_content == buf:
274                    return True, False
275            except IOError:
276                pass
277            finally:
278                existing.close()
279
280        if self._write_to_file:
281            ensureParentDir(self.name)
282            # Maintain 'b' if specified.  'U' only applies to modes starting with
283            # 'r', so it is dropped.
284            writemode = "w"
285            if self._binary_mode:
286                writemode += "b"
287                buf = six.ensure_binary(buf)
288            else:
289                buf = six.ensure_text(buf)
290            with _open(self.name, writemode) as file:
291                file.write(buf)
292
293        self._generate_diff(buf, old_content)
294
295        return existed, True
296
297    def _generate_diff(self, new_content, old_content):
298        """Generate a diff for the changed contents if `capture_diff` is True.
299
300        If the changed contents could not be decoded as utf-8 then generate a
301        placeholder message instead of a diff.
302
303        Args:
304            new_content: Str or bytes holding the new file contents.
305            old_content: Str or bytes holding the original file contents. Should be
306                None if no old content is being overwritten.
307        """
308        if not self._capture_diff:
309            return
310
311        try:
312            if old_content is None:
313                old_lines = None
314            else:
315                if self._binary_mode:
316                    # difflib doesn't work with bytes.
317                    old_content = old_content.decode("utf-8")
318
319                old_lines = old_content.splitlines()
320
321            if self._binary_mode:
322                # difflib doesn't work with bytes.
323                new_content = new_content.decode("utf-8")
324
325            new_lines = new_content.splitlines()
326
327            self.diff = simple_diff(self.name, old_lines, new_lines)
328        # FileAvoidWrite isn't unicode/bytes safe. So, files with non-ascii
329        # content or opened and written in different modes may involve
330        # implicit conversion and this will make Python unhappy. Since
331        # diffing isn't a critical feature, we just ignore the failure.
332        # This can go away once FileAvoidWrite uses io.BytesIO and
333        # io.StringIO. But that will require a lot of work.
334        except (UnicodeDecodeError, UnicodeEncodeError):
335            self.diff = ["Binary or non-ascii file changed: %s" % self.name]
336
337    def __enter__(self):
338        return self
339
340    def __exit__(self, type, value, traceback):
341        if not self.closed:
342            self.close()
343
344
345def resolve_target_to_make(topobjdir, target):
346    r"""
347    Resolve `target` (a target, directory, or file) to a make target.
348
349    `topobjdir` is the object directory; all make targets will be
350    rooted at or below the top-level Makefile in this directory.
351
352    Returns a pair `(reldir, target)` where `reldir` is a directory
353    relative to `topobjdir` containing a Makefile and `target` is a
354    make target (possibly `None`).
355
356    A directory resolves to the nearest directory at or above
357    containing a Makefile, and target `None`.
358
359    A regular (non-Makefile) file resolves to the nearest directory at
360    or above the file containing a Makefile, and an appropriate
361    target.
362
363    A Makefile resolves to the nearest parent strictly above the
364    Makefile containing a different Makefile, and an appropriate
365    target.
366    """
367
368    target = target.replace(os.sep, "/").lstrip("/")
369    abs_target = os.path.join(topobjdir, target)
370
371    # For directories, run |make -C dir|. If the directory does not
372    # contain a Makefile, check parents until we find one. At worst,
373    # this will terminate at the root.
374    if os.path.isdir(abs_target):
375        current = abs_target
376
377        while True:
378            make_path = os.path.join(current, "Makefile")
379            if os.path.exists(make_path):
380                return (current[len(topobjdir) + 1 :], None)
381
382            current = os.path.dirname(current)
383
384    # If it's not in a directory, this is probably a top-level make
385    # target. Treat it as such.
386    if "/" not in target:
387        return (None, target)
388
389    # We have a relative path within the tree. We look for a Makefile
390    # as far into the path as possible. Then, we compute the make
391    # target as relative to that directory.
392    reldir = os.path.dirname(target)
393    target = os.path.basename(target)
394
395    while True:
396        make_path = os.path.join(topobjdir, reldir, "Makefile")
397
398        # We append to target every iteration, so the check below
399        # happens exactly once.
400        if target != "Makefile" and os.path.exists(make_path):
401            return (reldir, target)
402
403        target = os.path.join(os.path.basename(reldir), target)
404        reldir = os.path.dirname(reldir)
405
406
407class List(list):
408    """A list specialized for moz.build environments.
409
410    We overload the assignment and append operations to require that the
411    appended thing is a list. This avoids bad surprises coming from appending
412    a string to a list, which would just add each letter of the string.
413    """
414
415    def __init__(self, iterable=None, **kwargs):
416        if iterable is None:
417            iterable = []
418        if not isinstance(iterable, list):
419            raise ValueError("List can only be created from other list instances.")
420
421        self._kwargs = kwargs
422        return super(List, self).__init__(iterable)
423
424    def extend(self, l):
425        if not isinstance(l, list):
426            raise ValueError("List can only be extended with other list instances.")
427
428        return super(List, self).extend(l)
429
430    def __setitem__(self, key, val):
431        if isinstance(key, slice):
432            if not isinstance(val, list):
433                raise ValueError(
434                    "List can only be sliced with other list " "instances."
435                )
436            if key.step:
437                raise ValueError("List cannot be sliced with a nonzero step " "value")
438            # Python 2 and Python 3 do this differently for some reason.
439            if six.PY2:
440                return super(List, self).__setslice__(key.start, key.stop, val)
441            else:
442                return super(List, self).__setitem__(key, val)
443        return super(List, self).__setitem__(key, val)
444
445    def __setslice__(self, i, j, sequence):
446        return self.__setitem__(slice(i, j), sequence)
447
448    def __add__(self, other):
449        # Allow None and EmptyValue is a special case because it makes undefined
450        # variable references in moz.build behave better.
451        other = [] if isinstance(other, (type(None), EmptyValue)) else other
452        if not isinstance(other, list):
453            raise ValueError("Only lists can be appended to lists.")
454
455        new_list = self.__class__(self, **self._kwargs)
456        new_list.extend(other)
457        return new_list
458
459    def __iadd__(self, other):
460        other = [] if isinstance(other, (type(None), EmptyValue)) else other
461        if not isinstance(other, list):
462            raise ValueError("Only lists can be appended to lists.")
463
464        return super(List, self).__iadd__(other)
465
466
467class UnsortedError(Exception):
468    def __init__(self, srtd, original):
469        assert len(srtd) == len(original)
470
471        self.sorted = srtd
472        self.original = original
473
474        for i, orig in enumerate(original):
475            s = srtd[i]
476
477            if orig != s:
478                self.i = i
479                break
480
481    def __str__(self):
482        s = StringIO()
483
484        s.write("An attempt was made to add an unsorted sequence to a list. ")
485        s.write("The incoming list is unsorted starting at element %d. " % self.i)
486        s.write(
487            'We expected "%s" but got "%s"'
488            % (self.sorted[self.i], self.original[self.i])
489        )
490
491        return s.getvalue()
492
493
494class StrictOrderingOnAppendList(List):
495    """A list specialized for moz.build environments.
496
497    We overload the assignment and append operations to require that incoming
498    elements be ordered. This enforces cleaner style in moz.build files.
499    """
500
501    @staticmethod
502    def ensure_sorted(l):
503        if isinstance(l, StrictOrderingOnAppendList):
504            return
505
506        def _first_element(e):
507            # If the list entry is a tuple, we sort based on the first element
508            # in the tuple.
509            return e[0] if isinstance(e, tuple) else e
510
511        srtd = sorted(l, key=lambda x: _first_element(x).lower())
512
513        if srtd != l:
514            raise UnsortedError(srtd, l)
515
516    def __init__(self, iterable=None, **kwargs):
517        if iterable is None:
518            iterable = []
519
520        StrictOrderingOnAppendList.ensure_sorted(iterable)
521
522        super(StrictOrderingOnAppendList, self).__init__(iterable, **kwargs)
523
524    def extend(self, l):
525        StrictOrderingOnAppendList.ensure_sorted(l)
526
527        return super(StrictOrderingOnAppendList, self).extend(l)
528
529    def __setitem__(self, key, val):
530        if isinstance(key, slice):
531            StrictOrderingOnAppendList.ensure_sorted(val)
532        return super(StrictOrderingOnAppendList, self).__setitem__(key, val)
533
534    def __add__(self, other):
535        StrictOrderingOnAppendList.ensure_sorted(other)
536
537        return super(StrictOrderingOnAppendList, self).__add__(other)
538
539    def __iadd__(self, other):
540        StrictOrderingOnAppendList.ensure_sorted(other)
541
542        return super(StrictOrderingOnAppendList, self).__iadd__(other)
543
544
545class ImmutableStrictOrderingOnAppendList(StrictOrderingOnAppendList):
546    """Like StrictOrderingOnAppendList, but not allowing mutations of the value."""
547
548    def append(self, elt):
549        raise Exception("cannot use append on this type")
550
551    def extend(self, iterable):
552        raise Exception("cannot use extend on this type")
553
554    def __setslice__(self, i, j, iterable):
555        raise Exception("cannot assign to slices on this type")
556
557    def __setitem__(self, i, elt):
558        raise Exception("cannot assign to indexes on this type")
559
560    def __iadd__(self, other):
561        raise Exception("cannot use += on this type")
562
563
564class StrictOrderingOnAppendListWithAction(StrictOrderingOnAppendList):
565    """An ordered list that accepts a callable to be applied to each item.
566
567    A callable (action) passed to the constructor is run on each item of input.
568    The result of running the callable on each item will be stored in place of
569    the original input, but the original item must be used to enforce sortedness.
570    """
571
572    def __init__(self, iterable=(), action=None):
573        if not callable(action):
574            raise ValueError(
575                "A callable action is required to construct "
576                "a StrictOrderingOnAppendListWithAction"
577            )
578
579        self._action = action
580        if not isinstance(iterable, (tuple, list)):
581            raise ValueError(
582                "StrictOrderingOnAppendListWithAction can only be initialized "
583                "with another list"
584            )
585        iterable = [self._action(i) for i in iterable]
586        super(StrictOrderingOnAppendListWithAction, self).__init__(
587            iterable, action=action
588        )
589
590    def extend(self, l):
591        if not isinstance(l, list):
592            raise ValueError(
593                "StrictOrderingOnAppendListWithAction can only be extended "
594                "with another list"
595            )
596        l = [self._action(i) for i in l]
597        return super(StrictOrderingOnAppendListWithAction, self).extend(l)
598
599    def __setitem__(self, key, val):
600        if isinstance(key, slice):
601            if not isinstance(val, list):
602                raise ValueError(
603                    "StrictOrderingOnAppendListWithAction can only be sliced "
604                    "with another list"
605                )
606            val = [self._action(item) for item in val]
607        return super(StrictOrderingOnAppendListWithAction, self).__setitem__(key, val)
608
609    def __add__(self, other):
610        if not isinstance(other, list):
611            raise ValueError(
612                "StrictOrderingOnAppendListWithAction can only be added with "
613                "another list"
614            )
615        return super(StrictOrderingOnAppendListWithAction, self).__add__(other)
616
617    def __iadd__(self, other):
618        if not isinstance(other, list):
619            raise ValueError(
620                "StrictOrderingOnAppendListWithAction can only be added with "
621                "another list"
622            )
623        other = [self._action(i) for i in other]
624        return super(StrictOrderingOnAppendListWithAction, self).__iadd__(other)
625
626
627class MozbuildDeletionError(Exception):
628    pass
629
630
631def FlagsFactory(flags):
632    """Returns a class which holds optional flags for an item in a list.
633
634    The flags are defined in the dict given as argument, where keys are
635    the flag names, and values the type used for the value of that flag.
636
637    The resulting class is used by the various <TypeName>WithFlagsFactory
638    functions below.
639    """
640    assert isinstance(flags, dict)
641    assert all(isinstance(v, type) for v in flags.values())
642
643    class Flags(object):
644        __slots__ = flags.keys()
645        _flags = flags
646
647        def update(self, **kwargs):
648            for k, v in six.iteritems(kwargs):
649                setattr(self, k, v)
650
651        def __getattr__(self, name):
652            if name not in self.__slots__:
653                raise AttributeError(
654                    "'%s' object has no attribute '%s'"
655                    % (self.__class__.__name__, name)
656                )
657            try:
658                return object.__getattr__(self, name)
659            except AttributeError:
660                value = self._flags[name]()
661                self.__setattr__(name, value)
662                return value
663
664        def __setattr__(self, name, value):
665            if name not in self.__slots__:
666                raise AttributeError(
667                    "'%s' object has no attribute '%s'"
668                    % (self.__class__.__name__, name)
669                )
670            if not isinstance(value, self._flags[name]):
671                raise TypeError(
672                    "'%s' attribute of class '%s' must be '%s'"
673                    % (name, self.__class__.__name__, self._flags[name].__name__)
674                )
675            return object.__setattr__(self, name, value)
676
677        def __delattr__(self, name):
678            raise MozbuildDeletionError("Unable to delete attributes for this object")
679
680    return Flags
681
682
683class StrictOrderingOnAppendListWithFlags(StrictOrderingOnAppendList):
684    """A list with flags specialized for moz.build environments.
685
686    Each subclass has a set of typed flags; this class lets us use `isinstance`
687    for natural testing.
688    """
689
690
691def StrictOrderingOnAppendListWithFlagsFactory(flags):
692    """Returns a StrictOrderingOnAppendList-like object, with optional
693    flags on each item.
694
695    The flags are defined in the dict given as argument, where keys are
696    the flag names, and values the type used for the value of that flag.
697
698    Example:
699        FooList = StrictOrderingOnAppendListWithFlagsFactory({
700            'foo': bool, 'bar': unicode
701        })
702        foo = FooList(['a', 'b', 'c'])
703        foo['a'].foo = True
704        foo['b'].bar = 'bar'
705    """
706
707    class StrictOrderingOnAppendListWithFlagsSpecialization(
708        StrictOrderingOnAppendListWithFlags
709    ):
710        def __init__(self, iterable=None):
711            if iterable is None:
712                iterable = []
713            StrictOrderingOnAppendListWithFlags.__init__(self, iterable)
714            self._flags_type = FlagsFactory(flags)
715            self._flags = dict()
716
717        def __getitem__(self, name):
718            if name not in self._flags:
719                if name not in self:
720                    raise KeyError("'%s'" % name)
721                self._flags[name] = self._flags_type()
722            return self._flags[name]
723
724        def __setitem__(self, name, value):
725            if not isinstance(name, slice):
726                raise TypeError(
727                    "'%s' object does not support item assignment"
728                    % self.__class__.__name__
729                )
730            result = super(
731                StrictOrderingOnAppendListWithFlagsSpecialization, self
732            ).__setitem__(name, value)
733            # We may have removed items.
734            for k in set(self._flags.keys()) - set(self):
735                del self._flags[k]
736            if isinstance(value, StrictOrderingOnAppendListWithFlags):
737                self._update_flags(value)
738            return result
739
740        def _update_flags(self, other):
741            if self._flags_type._flags != other._flags_type._flags:
742                raise ValueError(
743                    "Expected a list of strings with flags like %s, not like %s"
744                    % (self._flags_type._flags, other._flags_type._flags)
745                )
746            intersection = set(self._flags.keys()) & set(other._flags.keys())
747            if intersection:
748                raise ValueError(
749                    "Cannot update flags: both lists of strings with flags configure %s"
750                    % intersection
751                )
752            self._flags.update(other._flags)
753
754        def extend(self, l):
755            result = super(
756                StrictOrderingOnAppendListWithFlagsSpecialization, self
757            ).extend(l)
758            if isinstance(l, StrictOrderingOnAppendListWithFlags):
759                self._update_flags(l)
760            return result
761
762        def __add__(self, other):
763            result = super(
764                StrictOrderingOnAppendListWithFlagsSpecialization, self
765            ).__add__(other)
766            if isinstance(other, StrictOrderingOnAppendListWithFlags):
767                # Result has flags from other but not from self, since
768                # internally we duplicate self and then extend with other, and
769                # only extend knows about flags.  Since we don't allow updating
770                # when the set of flag keys intersect, which we instance we pass
771                # to _update_flags here matters.  This needs to be correct but
772                # is an implementation detail.
773                result._update_flags(self)
774            return result
775
776        def __iadd__(self, other):
777            result = super(
778                StrictOrderingOnAppendListWithFlagsSpecialization, self
779            ).__iadd__(other)
780            if isinstance(other, StrictOrderingOnAppendListWithFlags):
781                self._update_flags(other)
782            return result
783
784    return StrictOrderingOnAppendListWithFlagsSpecialization
785
786
787class HierarchicalStringList(object):
788    """A hierarchy of lists of strings.
789
790    Each instance of this object contains a list of strings, which can be set or
791    appended to. A sub-level of the hierarchy is also an instance of this class,
792    can be added by appending to an attribute instead.
793
794    For example, the moz.build variable EXPORTS is an instance of this class. We
795    can do:
796
797    EXPORTS += ['foo.h']
798    EXPORTS.mozilla.dom += ['bar.h']
799
800    In this case, we have 3 instances (EXPORTS, EXPORTS.mozilla, and
801    EXPORTS.mozilla.dom), and the first and last each have one element in their
802    list.
803    """
804
805    __slots__ = ("_strings", "_children")
806
807    def __init__(self):
808        # Please change ContextDerivedTypedHierarchicalStringList in context.py
809        # if you make changes here.
810        self._strings = StrictOrderingOnAppendList()
811        self._children = {}
812
813    class StringListAdaptor(collections.abc.Sequence):
814        def __init__(self, hsl):
815            self._hsl = hsl
816
817        def __getitem__(self, index):
818            return self._hsl._strings[index]
819
820        def __len__(self):
821            return len(self._hsl._strings)
822
823    def walk(self):
824        """Walk over all HierarchicalStringLists in the hierarchy.
825
826        This is a generator of (path, sequence).
827
828        The path is '' for the root level and '/'-delimited strings for
829        any descendants.  The sequence is a read-only sequence of the
830        strings contained at that level.
831        """
832
833        if self._strings:
834            path_to_here = ""
835            yield path_to_here, self.StringListAdaptor(self)
836
837        for k, l in sorted(self._children.items()):
838            for p, v in l.walk():
839                path_to_there = "%s/%s" % (k, p)
840                yield path_to_there.strip("/"), v
841
842    def __setattr__(self, name, value):
843        if name in self.__slots__:
844            return object.__setattr__(self, name, value)
845
846        # __setattr__ can be called with a list when a simple assignment is
847        # used:
848        #
849        # EXPORTS.foo = ['file.h']
850        #
851        # In this case, we need to overwrite foo's current list of strings.
852        #
853        # However, __setattr__ is also called with a HierarchicalStringList
854        # to try to actually set the attribute. We want to ignore this case,
855        # since we don't actually create an attribute called 'foo', but just add
856        # it to our list of children (using _get_exportvariable()).
857        self._set_exportvariable(name, value)
858
859    def __getattr__(self, name):
860        if name.startswith("__"):
861            return object.__getattr__(self, name)
862        return self._get_exportvariable(name)
863
864    def __delattr__(self, name):
865        raise MozbuildDeletionError("Unable to delete attributes for this object")
866
867    def __iadd__(self, other):
868        if isinstance(other, HierarchicalStringList):
869            self._strings += other._strings
870            for c in other._children:
871                self[c] += other[c]
872        else:
873            self._check_list(other)
874            self._strings += other
875        return self
876
877    def __getitem__(self, name):
878        return self._get_exportvariable(name)
879
880    def __setitem__(self, name, value):
881        self._set_exportvariable(name, value)
882
883    def _get_exportvariable(self, name):
884        # Please change ContextDerivedTypedHierarchicalStringList in context.py
885        # if you make changes here.
886        child = self._children.get(name)
887        if not child:
888            child = self._children[name] = HierarchicalStringList()
889        return child
890
891    def _set_exportvariable(self, name, value):
892        if name in self._children:
893            if value is self._get_exportvariable(name):
894                return
895            raise KeyError("global_ns", "reassign", "<some variable>.%s" % name)
896
897        exports = self._get_exportvariable(name)
898        exports._check_list(value)
899        exports._strings += value
900
901    def _check_list(self, value):
902        if not isinstance(value, list):
903            raise ValueError("Expected a list of strings, not %s" % type(value))
904        for v in value:
905            if not isinstance(v, six.string_types):
906                raise ValueError(
907                    "Expected a list of strings, not an element of %s" % type(v)
908                )
909
910
911class LockFile(object):
912    """LockFile is used by the lock_file method to hold the lock.
913
914    This object should not be used directly, but only through
915    the lock_file method below.
916    """
917
918    def __init__(self, lockfile):
919        self.lockfile = lockfile
920
921    def __del__(self):
922        while True:
923            try:
924                os.remove(self.lockfile)
925                break
926            except OSError as e:
927                if e.errno == errno.EACCES:
928                    # Another process probably has the file open, we'll retry.
929                    # Just a short sleep since we want to drop the lock ASAP
930                    # (but we need to let some other process close the file
931                    # first).
932                    time.sleep(0.1)
933                else:
934                    # Re-raise unknown errors
935                    raise
936
937
938def lock_file(lockfile, max_wait=600):
939    """Create and hold a lockfile of the given name, with the given timeout.
940
941    To release the lock, delete the returned object.
942    """
943
944    # FUTURE This function and object could be written as a context manager.
945
946    while True:
947        try:
948            fd = os.open(lockfile, os.O_EXCL | os.O_RDWR | os.O_CREAT)
949            # We created the lockfile, so we're the owner
950            break
951        except OSError as e:
952            if e.errno == errno.EEXIST or (
953                sys.platform == "win32" and e.errno == errno.EACCES
954            ):
955                pass
956            else:
957                # Should not occur
958                raise
959
960        try:
961            # The lock file exists, try to stat it to get its age
962            # and read its contents to report the owner PID
963            f = open(lockfile, "r")
964            s = os.stat(lockfile)
965        except EnvironmentError as e:
966            if e.errno == errno.ENOENT or e.errno == errno.EACCES:
967                # We didn't create the lockfile, so it did exist, but it's
968                # gone now. Just try again
969                continue
970
971            raise Exception(
972                "{0} exists but stat() failed: {1}".format(lockfile, e.strerror)
973            )
974
975        # We didn't create the lockfile and it's still there, check
976        # its age
977        now = int(time.time())
978        if now - s[stat.ST_MTIME] > max_wait:
979            pid = f.readline().rstrip()
980            raise Exception(
981                "{0} has been locked for more than "
982                "{1} seconds (PID {2})".format(lockfile, max_wait, pid)
983            )
984
985        # It's not been locked too long, wait a while and retry
986        f.close()
987        time.sleep(1)
988
989    # if we get here. we have the lockfile. Convert the os.open file
990    # descriptor into a Python file object and record our PID in it
991    f = os.fdopen(fd, "w")
992    f.write("{0}\n".format(os.getpid()))
993    f.close()
994
995    return LockFile(lockfile)
996
997
998class OrderedDefaultDict(OrderedDict):
999    """A combination of OrderedDict and defaultdict."""
1000
1001    def __init__(self, default_factory, *args, **kwargs):
1002        OrderedDict.__init__(self, *args, **kwargs)
1003        self._default_factory = default_factory
1004
1005    def __missing__(self, key):
1006        value = self[key] = self._default_factory()
1007        return value
1008
1009
1010class KeyedDefaultDict(dict):
1011    """Like a defaultdict, but the default_factory function takes the key as
1012    argument"""
1013
1014    def __init__(self, default_factory, *args, **kwargs):
1015        dict.__init__(self, *args, **kwargs)
1016        self._default_factory = default_factory
1017
1018    def __missing__(self, key):
1019        value = self._default_factory(key)
1020        dict.__setitem__(self, key, value)
1021        return value
1022
1023
1024class ReadOnlyKeyedDefaultDict(KeyedDefaultDict, ReadOnlyDict):
1025    """Like KeyedDefaultDict, but read-only."""
1026
1027
1028class memoize(dict):
1029    """A decorator to memoize the results of function calls depending
1030    on its arguments.
1031    Both functions and instance methods are handled, although in the
1032    instance method case, the results are cache in the instance itself.
1033    """
1034
1035    def __init__(self, func):
1036        self.func = func
1037        functools.update_wrapper(self, func)
1038
1039    def __call__(self, *args):
1040        if args not in self:
1041            self[args] = self.func(*args)
1042        return self[args]
1043
1044    def method_call(self, instance, *args):
1045        name = "_%s" % self.func.__name__
1046        if not hasattr(instance, name):
1047            setattr(instance, name, {})
1048        cache = getattr(instance, name)
1049        if args not in cache:
1050            cache[args] = self.func(instance, *args)
1051        return cache[args]
1052
1053    def __get__(self, instance, cls):
1054        return functools.update_wrapper(
1055            functools.partial(self.method_call, instance), self.func
1056        )
1057
1058
1059class memoized_property(object):
1060    """A specialized version of the memoize decorator that works for
1061    class instance properties.
1062    """
1063
1064    def __init__(self, func):
1065        self.func = func
1066
1067    def __get__(self, instance, cls):
1068        name = "_%s" % self.func.__name__
1069        if not hasattr(instance, name):
1070            setattr(instance, name, self.func(instance))
1071        return getattr(instance, name)
1072
1073
1074def TypedNamedTuple(name, fields):
1075    """Factory for named tuple types with strong typing.
1076
1077    Arguments are an iterable of 2-tuples. The first member is the
1078    the field name. The second member is a type the field will be validated
1079    to be.
1080
1081    Construction of instances varies from ``collections.namedtuple``.
1082
1083    First, if a single tuple argument is given to the constructor, this is
1084    treated as the equivalent of passing each tuple value as a separate
1085    argument into __init__. e.g.::
1086
1087        t = (1, 2)
1088        TypedTuple(t) == TypedTuple(1, 2)
1089
1090    This behavior is meant for moz.build files, so vanilla tuples are
1091    automatically cast to typed tuple instances.
1092
1093    Second, fields in the tuple are validated to be instances of the specified
1094    type. This is done via an ``isinstance()`` check. To allow multiple types,
1095    pass a tuple as the allowed types field.
1096    """
1097    cls = collections.namedtuple(name, (name for name, typ in fields))
1098
1099    class TypedTuple(cls):
1100        __slots__ = ()
1101
1102        def __new__(klass, *args, **kwargs):
1103            if len(args) == 1 and not kwargs and isinstance(args[0], tuple):
1104                args = args[0]
1105
1106            return super(TypedTuple, klass).__new__(klass, *args, **kwargs)
1107
1108        def __init__(self, *args, **kwargs):
1109            for i, (fname, ftype) in enumerate(self._fields):
1110                value = self[i]
1111
1112                if not isinstance(value, ftype):
1113                    raise TypeError(
1114                        "field in tuple not of proper type: %s; "
1115                        "got %s, expected %s" % (fname, type(value), ftype)
1116                    )
1117
1118    TypedTuple._fields = fields
1119
1120    return TypedTuple
1121
1122
1123@memoize
1124def TypedList(type, base_class=List):
1125    """A list with type coercion.
1126
1127    The given ``type`` is what list elements are being coerced to. It may do
1128    strict validation, throwing ValueError exceptions.
1129
1130    A ``base_class`` type can be given for more specific uses than a List. For
1131    example, a Typed StrictOrderingOnAppendList can be created with:
1132
1133       TypedList(unicode, StrictOrderingOnAppendList)
1134    """
1135
1136    class _TypedList(base_class):
1137        @staticmethod
1138        def normalize(e):
1139            if not isinstance(e, type):
1140                e = type(e)
1141            return e
1142
1143        def _ensure_type(self, l):
1144            if isinstance(l, self.__class__):
1145                return l
1146
1147            return [self.normalize(e) for e in l]
1148
1149        def __init__(self, iterable=None, **kwargs):
1150            if iterable is None:
1151                iterable = []
1152            iterable = self._ensure_type(iterable)
1153
1154            super(_TypedList, self).__init__(iterable, **kwargs)
1155
1156        def extend(self, l):
1157            l = self._ensure_type(l)
1158
1159            return super(_TypedList, self).extend(l)
1160
1161        def __setitem__(self, key, val):
1162            val = self._ensure_type(val)
1163
1164            return super(_TypedList, self).__setitem__(key, val)
1165
1166        def __add__(self, other):
1167            other = self._ensure_type(other)
1168
1169            return super(_TypedList, self).__add__(other)
1170
1171        def __iadd__(self, other):
1172            other = self._ensure_type(other)
1173
1174            return super(_TypedList, self).__iadd__(other)
1175
1176        def append(self, other):
1177            self += [other]
1178
1179    return _TypedList
1180
1181
1182def group_unified_files(files, unified_prefix, unified_suffix, files_per_unified_file):
1183    """Return an iterator of (unified_filename, source_filenames) tuples.
1184
1185    We compile most C and C++ files in "unified mode"; instead of compiling
1186    ``a.cpp``, ``b.cpp``, and ``c.cpp`` separately, we compile a single file
1187    that looks approximately like::
1188
1189       #include "a.cpp"
1190       #include "b.cpp"
1191       #include "c.cpp"
1192
1193    This function handles the details of generating names for the unified
1194    files, and determining which original source files go in which unified
1195    file."""
1196
1197    # Make sure the input list is sorted. If it's not, bad things could happen!
1198    files = sorted(files)
1199
1200    # Our last returned list of source filenames may be short, and we
1201    # don't want the fill value inserted by zip_longest to be an
1202    # issue.  So we do a little dance to filter it out ourselves.
1203    dummy_fill_value = ("dummy",)
1204
1205    def filter_out_dummy(iterable):
1206        return six.moves.filter(lambda x: x != dummy_fill_value, iterable)
1207
1208    # From the itertools documentation, slightly modified:
1209    def grouper(n, iterable):
1210        "grouper(3, 'ABCDEFG', 'x') --> ABC DEF Gxx"
1211        args = [iter(iterable)] * n
1212        return six.moves.zip_longest(fillvalue=dummy_fill_value, *args)
1213
1214    for i, unified_group in enumerate(grouper(files_per_unified_file, files)):
1215        just_the_filenames = list(filter_out_dummy(unified_group))
1216        yield "%s%d.%s" % (unified_prefix, i, unified_suffix), just_the_filenames
1217
1218
1219def pair(iterable):
1220    """Given an iterable, returns an iterable pairing its items.
1221
1222    For example,
1223        list(pair([1,2,3,4,5,6]))
1224    returns
1225        [(1,2), (3,4), (5,6)]
1226    """
1227    i = iter(iterable)
1228    return six.moves.zip_longest(i, i)
1229
1230
1231def pairwise(iterable):
1232    """Given an iterable, returns an iterable of overlapped pairs of
1233    its items. Based on the Python itertools documentation.
1234
1235    For example,
1236        list(pairwise([1,2,3,4,5,6]))
1237    returns
1238        [(1,2), (2,3), (3,4), (4,5), (5,6)]
1239    """
1240    a, b = itertools.tee(iterable)
1241    next(b, None)
1242    return zip(a, b)
1243
1244
1245VARIABLES_RE = re.compile("\$\((\w+)\)")
1246
1247
1248def expand_variables(s, variables):
1249    """Given a string with $(var) variable references, replace those references
1250    with the corresponding entries from the given `variables` dict.
1251
1252    If a variable value is not a string, it is iterated and its items are
1253    joined with a whitespace."""
1254    result = ""
1255    for s, name in pair(VARIABLES_RE.split(s)):
1256        result += s
1257        value = variables.get(name)
1258        if not value:
1259            continue
1260        if not isinstance(value, six.string_types):
1261            value = " ".join(value)
1262        result += value
1263    return result
1264
1265
1266class DefinesAction(argparse.Action):
1267    """An ArgumentParser action to handle -Dvar[=value] type of arguments."""
1268
1269    def __call__(self, parser, namespace, values, option_string):
1270        defines = getattr(namespace, self.dest)
1271        if defines is None:
1272            defines = {}
1273        values = values.split("=", 1)
1274        if len(values) == 1:
1275            name, value = values[0], 1
1276        else:
1277            name, value = values
1278            if value.isdigit():
1279                value = int(value)
1280        defines[name] = value
1281        setattr(namespace, self.dest, defines)
1282
1283
1284class EnumStringComparisonError(Exception):
1285    pass
1286
1287
1288class EnumString(six.text_type):
1289    """A string type that only can have a limited set of values, similarly to
1290    an Enum, and can only be compared against that set of values.
1291
1292    The class is meant to be subclassed, where the subclass defines
1293    POSSIBLE_VALUES. The `subclass` method is a helper to create such
1294    subclasses.
1295    """
1296
1297    POSSIBLE_VALUES = ()
1298
1299    def __init__(self, value):
1300        if value not in self.POSSIBLE_VALUES:
1301            raise ValueError(
1302                "'%s' is not a valid value for %s" % (value, self.__class__.__name__)
1303            )
1304
1305    def __eq__(self, other):
1306        if other not in self.POSSIBLE_VALUES:
1307            raise EnumStringComparisonError(
1308                "Can only compare with %s"
1309                % ", ".join("'%s'" % v for v in self.POSSIBLE_VALUES)
1310            )
1311        return super(EnumString, self).__eq__(other)
1312
1313    def __ne__(self, other):
1314        return not (self == other)
1315
1316    def __hash__(self):
1317        return super(EnumString, self).__hash__()
1318
1319    @staticmethod
1320    def subclass(*possible_values):
1321        class EnumStringSubclass(EnumString):
1322            POSSIBLE_VALUES = possible_values
1323
1324        return EnumStringSubclass
1325
1326
1327def _escape_char(c):
1328    # str.encode('unicode_espace') doesn't escape quotes, presumably because
1329    # quoting could be done with either ' or ".
1330    if c == "'":
1331        return "\\'"
1332    return six.text_type(c.encode("unicode_escape"))
1333
1334
1335if six.PY2:  # Delete when we get rid of Python 2.
1336    # Mapping table between raw characters below \x80 and their escaped
1337    # counterpart, when they differ
1338    _INDENTED_REPR_TABLE = {
1339        c: e
1340        for c, e in map(lambda x: (x, _escape_char(x)), map(unichr, range(128)))
1341        if c != e
1342    }
1343    # Regexp matching all characters to escape.
1344    _INDENTED_REPR_RE = re.compile(
1345        "([" + "".join(_INDENTED_REPR_TABLE.values()) + "]+)"
1346    )
1347
1348
1349def write_indented_repr(f, o, indent=4):
1350    """Write an indented representation (similar to repr()) of the object to the
1351    given file `f`.
1352
1353    One notable difference with repr is that the returned representation
1354    assumes `from __future__ import unicode_literals`.
1355    """
1356    if six.PY3:
1357        pprint.pprint(o, stream=f, indent=indent)
1358        return
1359    # Delete everything below when we get rid of Python 2.
1360    one_indent = " " * indent
1361
1362    def recurse_indented_repr(o, level):
1363        if isinstance(o, dict):
1364            yield "{\n"
1365            for k, v in sorted(o.items()):
1366                yield one_indent * (level + 1)
1367                for d in recurse_indented_repr(k, level + 1):
1368                    yield d
1369                yield ": "
1370                for d in recurse_indented_repr(v, level + 1):
1371                    yield d
1372                yield ",\n"
1373            yield one_indent * level
1374            yield "}"
1375        elif isinstance(o, bytes):
1376            yield "b"
1377            yield repr(o)
1378        elif isinstance(o, six.text_type):
1379            yield "'"
1380            # We want a readable string (non escaped unicode), but some
1381            # special characters need escaping (e.g. \n, \t, etc.)
1382            for i, s in enumerate(_INDENTED_REPR_RE.split(o)):
1383                if i % 2:
1384                    for c in s:
1385                        yield _INDENTED_REPR_TABLE[c]
1386                else:
1387                    yield s
1388            yield "'"
1389        elif hasattr(o, "__iter__"):
1390            yield "[\n"
1391            for i in o:
1392                yield one_indent * (level + 1)
1393                for d in recurse_indented_repr(i, level + 1):
1394                    yield d
1395                yield ",\n"
1396            yield one_indent * level
1397            yield "]"
1398        else:
1399            yield repr(o)
1400
1401    result = "".join(recurse_indented_repr(o, 0)) + "\n"
1402    f.write(result)
1403
1404
1405def patch_main():
1406    """This is a hack to work around the fact that Windows multiprocessing needs
1407    to import the original main module, and assumes that it corresponds to a file
1408    ending in .py.
1409
1410    We do this by a sort of two-level function interposing. The first
1411    level interposes forking.get_command_line() with our version defined
1412    in my_get_command_line(). Our version of get_command_line will
1413    replace the command string with the contents of the fork_interpose()
1414    function to be used in the subprocess.
1415
1416    The subprocess then gets an interposed imp.find_module(), which we
1417    hack up to find the main module name multiprocessing will assume, since we
1418    know what this will be based on the main module in the parent. If we're not
1419    looking for our main module, then the original find_module will suffice.
1420
1421    See also: http://bugs.python.org/issue19946
1422    And: https://bugzilla.mozilla.org/show_bug.cgi?id=914563
1423    """
1424    # XXX In Python 3.4 the multiprocessing module was re-written and the below
1425    # code is no longer valid. The Python issue19946 also claims to be fixed in
1426    # this version. It's not clear whether this hack is still needed in 3.4+ or
1427    # not, but at least some basic mach commands appear to work without it. So
1428    # skip it in 3.4+ until we determine it's still needed.
1429    if sys.platform == "win32" and sys.version_info < (3, 4):
1430        import os
1431        from multiprocessing import forking
1432
1433        global orig_command_line
1434
1435        # Figure out what multiprocessing will assume our main module
1436        # is called (see python/Lib/multiprocessing/forking.py).
1437        main_path = getattr(sys.modules["__main__"], "__file__", None)
1438        if main_path is None:
1439            # If someone deleted or modified __main__, there's nothing left for
1440            # us to do.
1441            return
1442        main_file_name = os.path.basename(main_path)
1443        main_module_name, ext = os.path.splitext(main_file_name)
1444        if ext == ".py":
1445            # If main is a .py file, everything ought to work as expected.
1446            return
1447
1448        def my_get_command_line():
1449            with open(
1450                os.path.join(os.path.dirname(__file__), "fork_interpose.py"), "rU"
1451            ) as fork_file:
1452                fork_code = fork_file.read()
1453            # Add our relevant globals.
1454            fork_string = (
1455                "main_file_name = '%s'\n" % main_file_name
1456                + "main_module_name = '%s'\n" % main_module_name
1457                + fork_code
1458            )
1459            cmdline = orig_command_line()
1460            # We don't catch errors if "-c" is not found because it's not clear
1461            # what we should do if the original command line is not of the form
1462            # "python ... -c 'script'".
1463            cmdline[cmdline.index("-c") + 1] = fork_string
1464            return cmdline
1465
1466        orig_command_line = forking.get_command_line
1467        forking.get_command_line = my_get_command_line
1468
1469
1470def ensure_bytes(value, encoding="utf-8"):
1471    if isinstance(value, six.text_type):
1472        return value.encode(encoding)
1473    return value
1474
1475
1476def ensure_unicode(value, encoding="utf-8"):
1477    if isinstance(value, six.binary_type):
1478        return value.decode(encoding)
1479    return value
1480
1481
1482def ensure_subprocess_env(env, encoding="utf-8"):
1483    """Ensure the environment is in the correct format for the `subprocess`
1484    module.
1485
1486    This will convert all keys and values to bytes on Python 2, and text on
1487    Python 3.
1488
1489    Args:
1490        env (dict): Environment to ensure.
1491        encoding (str): Encoding to use when converting to/from bytes/text
1492                        (default: utf-8).
1493    """
1494    ensure = ensure_bytes if sys.version_info[0] < 3 else ensure_unicode
1495    return {ensure(k, encoding): ensure(v, encoding) for k, v in six.iteritems(env)}
1496
1497
1498def process_time():
1499    if six.PY2:
1500        return time.clock()
1501    else:
1502        return time.process_time()
1503
1504
1505def hexdump(buf):
1506    """
1507    Returns a list of hexdump-like lines corresponding to the given input buffer.
1508    """
1509    assert six.PY3
1510    off_format = "%0{}x ".format(len(str(len(buf))))
1511    lines = []
1512    for off in range(0, len(buf), 16):
1513        line = off_format % off
1514        chunk = buf[off : min(off + 16, len(buf))]
1515        for n, byte in enumerate(chunk):
1516            line += " %02x" % byte
1517            if n == 7:
1518                line += " "
1519        for n in range(len(chunk), 16):
1520            line += "   "
1521            if n == 7:
1522                line += " "
1523        line += "  |"
1524        for byte in chunk:
1525            if byte < 127 and byte >= 32:
1526                line += chr(byte)
1527            else:
1528                line += "."
1529        for n in range(len(chunk), 16):
1530            line += " "
1531        line += "|\n"
1532        lines.append(line)
1533    return lines
1534