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