1# Copyright 2012-2020 The Meson development team 2 3# Licensed under the Apache License, Version 2.0 (the "License"); 4# you may not use this file except in compliance with the License. 5# You may obtain a copy of the License at 6 7# http://www.apache.org/licenses/LICENSE-2.0 8 9# Unless required by applicable law or agreed to in writing, software 10# distributed under the License is distributed on an "AS IS" BASIS, 11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12# See the License for the specific language governing permissions and 13# limitations under the License. 14 15"""A library of random helper functionality.""" 16from pathlib import Path 17import sys 18import stat 19import time 20import platform, subprocess, operator, os, shlex, shutil, re 21import collections 22from enum import Enum 23from functools import lru_cache, wraps 24from itertools import tee, filterfalse 25import typing as T 26import uuid 27import textwrap 28 29from mesonbuild import mlog 30 31if T.TYPE_CHECKING: 32 from .build import ConfigurationData 33 from .coredata import OptionDictType, UserOption 34 from .compilers.compilers import CompilerType 35 from .interpreterbase import ObjectHolder 36 37_T = T.TypeVar('_T') 38_U = T.TypeVar('_U') 39 40have_fcntl = False 41have_msvcrt = False 42# TODO: this is such a hack, this really should be either in coredata or in the 43# interpreter 44# {subproject: project_meson_version} 45project_meson_versions = collections.defaultdict(str) # type: T.DefaultDict[str, str] 46 47try: 48 import fcntl 49 have_fcntl = True 50except Exception: 51 pass 52 53try: 54 import msvcrt 55 have_msvcrt = True 56except Exception: 57 pass 58 59from glob import glob 60 61if os.path.basename(sys.executable) == 'meson.exe': 62 # In Windows and using the MSI installed executable. 63 python_command = [sys.executable, 'runpython'] 64else: 65 python_command = [sys.executable] 66meson_command = None 67 68GIT = shutil.which('git') 69def git(cmd: T.List[str], workingdir: str, **kwargs: T.Any) -> subprocess.CompletedProcess: 70 pc = subprocess.run([GIT, '-C', workingdir] + cmd, 71 # Redirect stdin to DEVNULL otherwise git messes up the 72 # console and ANSI colors stop working on Windows. 73 stdin=subprocess.DEVNULL, **kwargs) 74 # Sometimes git calls git recursively, such as `git submodule update 75 # --recursive` which will be without the above workaround, so set the 76 # console mode again just in case. 77 mlog.setup_console() 78 return pc 79 80 81def set_meson_command(mainfile: str) -> None: 82 global python_command 83 global meson_command 84 # On UNIX-like systems `meson` is a Python script 85 # On Windows `meson` and `meson.exe` are wrapper exes 86 if not mainfile.endswith('.py'): 87 meson_command = [mainfile] 88 elif os.path.isabs(mainfile) and mainfile.endswith('mesonmain.py'): 89 # Can't actually run meson with an absolute path to mesonmain.py, it must be run as -m mesonbuild.mesonmain 90 meson_command = python_command + ['-m', 'mesonbuild.mesonmain'] 91 else: 92 # Either run uninstalled, or full path to meson-script.py 93 meson_command = python_command + [mainfile] 94 # We print this value for unit tests. 95 if 'MESON_COMMAND_TESTS' in os.environ: 96 mlog.log('meson_command is {!r}'.format(meson_command)) 97 98 99def is_ascii_string(astring: T.Union[str, bytes]) -> bool: 100 try: 101 if isinstance(astring, str): 102 astring.encode('ascii') 103 elif isinstance(astring, bytes): 104 astring.decode('ascii') 105 except UnicodeDecodeError: 106 return False 107 return True 108 109 110def check_direntry_issues(direntry_array: T.Union[T.List[T.Union[str, bytes]], str, bytes]) -> None: 111 import locale 112 # Warn if the locale is not UTF-8. This can cause various unfixable issues 113 # such as os.stat not being able to decode filenames with unicode in them. 114 # There is no way to reset both the preferred encoding and the filesystem 115 # encoding, so we can just warn about it. 116 e = locale.getpreferredencoding() 117 if e.upper() != 'UTF-8' and not is_windows(): 118 if not isinstance(direntry_array, list): 119 direntry_array = [direntry_array] 120 for de in direntry_array: 121 if is_ascii_string(de): 122 continue 123 mlog.warning(textwrap.dedent(''' 124 You are using {!r} which is not a Unicode-compatible 125 locale but you are trying to access a file system entry called {!r} which is 126 not pure ASCII. This may cause problems. 127 '''.format(e, de)), file=sys.stderr) 128 129 130# Put this in objects that should not get dumped to pickle files 131# by accident. 132import threading 133an_unpicklable_object = threading.Lock() 134 135 136class MesonException(Exception): 137 '''Exceptions thrown by Meson''' 138 139 file = None # type: T.Optional[str] 140 lineno = None # type: T.Optional[int] 141 colno = None # type: T.Optional[int] 142 143 144class EnvironmentException(MesonException): 145 '''Exceptions thrown while processing and creating the build environment''' 146 147 148class FileMode: 149 # The first triad is for owner permissions, the second for group permissions, 150 # and the third for others (everyone else). 151 # For the 1st character: 152 # 'r' means can read 153 # '-' means not allowed 154 # For the 2nd character: 155 # 'w' means can write 156 # '-' means not allowed 157 # For the 3rd character: 158 # 'x' means can execute 159 # 's' means can execute and setuid/setgid is set (owner/group triads only) 160 # 'S' means cannot execute and setuid/setgid is set (owner/group triads only) 161 # 't' means can execute and sticky bit is set ("others" triads only) 162 # 'T' means cannot execute and sticky bit is set ("others" triads only) 163 # '-' means none of these are allowed 164 # 165 # The meanings of 'rwx' perms is not obvious for directories; see: 166 # https://www.hackinglinuxexposed.com/articles/20030424.html 167 # 168 # For information on this notation such as setuid/setgid/sticky bits, see: 169 # https://en.wikipedia.org/wiki/File_system_permissions#Symbolic_notation 170 symbolic_perms_regex = re.compile('[r-][w-][xsS-]' # Owner perms 171 '[r-][w-][xsS-]' # Group perms 172 '[r-][w-][xtT-]') # Others perms 173 174 def __init__(self, perms: T.Optional[str] = None, owner: T.Optional[str] = None, 175 group: T.Optional[str] = None): 176 self.perms_s = perms 177 self.perms = self.perms_s_to_bits(perms) 178 self.owner = owner 179 self.group = group 180 181 def __repr__(self) -> str: 182 ret = '<FileMode: {!r} owner={} group={}' 183 return ret.format(self.perms_s, self.owner, self.group) 184 185 @classmethod 186 def perms_s_to_bits(cls, perms_s: T.Optional[str]) -> int: 187 ''' 188 Does the opposite of stat.filemode(), converts strings of the form 189 'rwxr-xr-x' to st_mode enums which can be passed to os.chmod() 190 ''' 191 if perms_s is None: 192 # No perms specified, we will not touch the permissions 193 return -1 194 eg = 'rwxr-xr-x' 195 if not isinstance(perms_s, str): 196 msg = 'Install perms must be a string. For example, {!r}' 197 raise MesonException(msg.format(eg)) 198 if len(perms_s) != 9 or not cls.symbolic_perms_regex.match(perms_s): 199 msg = 'File perms {!r} must be exactly 9 chars. For example, {!r}' 200 raise MesonException(msg.format(perms_s, eg)) 201 perms = 0 202 # Owner perms 203 if perms_s[0] == 'r': 204 perms |= stat.S_IRUSR 205 if perms_s[1] == 'w': 206 perms |= stat.S_IWUSR 207 if perms_s[2] == 'x': 208 perms |= stat.S_IXUSR 209 elif perms_s[2] == 'S': 210 perms |= stat.S_ISUID 211 elif perms_s[2] == 's': 212 perms |= stat.S_IXUSR 213 perms |= stat.S_ISUID 214 # Group perms 215 if perms_s[3] == 'r': 216 perms |= stat.S_IRGRP 217 if perms_s[4] == 'w': 218 perms |= stat.S_IWGRP 219 if perms_s[5] == 'x': 220 perms |= stat.S_IXGRP 221 elif perms_s[5] == 'S': 222 perms |= stat.S_ISGID 223 elif perms_s[5] == 's': 224 perms |= stat.S_IXGRP 225 perms |= stat.S_ISGID 226 # Others perms 227 if perms_s[6] == 'r': 228 perms |= stat.S_IROTH 229 if perms_s[7] == 'w': 230 perms |= stat.S_IWOTH 231 if perms_s[8] == 'x': 232 perms |= stat.S_IXOTH 233 elif perms_s[8] == 'T': 234 perms |= stat.S_ISVTX 235 elif perms_s[8] == 't': 236 perms |= stat.S_IXOTH 237 perms |= stat.S_ISVTX 238 return perms 239 240class File: 241 def __init__(self, is_built: bool, subdir: str, fname: str): 242 self.is_built = is_built 243 self.subdir = subdir 244 self.fname = fname 245 246 def __str__(self) -> str: 247 return self.relative_name() 248 249 def __repr__(self) -> str: 250 ret = '<File: {0}' 251 if not self.is_built: 252 ret += ' (not built)' 253 ret += '>' 254 return ret.format(self.relative_name()) 255 256 @staticmethod 257 @lru_cache(maxsize=None) 258 def from_source_file(source_root: str, subdir: str, fname: str) -> 'File': 259 if not os.path.isfile(os.path.join(source_root, subdir, fname)): 260 raise MesonException('File %s does not exist.' % fname) 261 return File(False, subdir, fname) 262 263 @staticmethod 264 def from_built_file(subdir: str, fname: str) -> 'File': 265 return File(True, subdir, fname) 266 267 @staticmethod 268 def from_absolute_file(fname: str) -> 'File': 269 return File(False, '', fname) 270 271 @lru_cache(maxsize=None) 272 def rel_to_builddir(self, build_to_src: str) -> str: 273 if self.is_built: 274 return self.relative_name() 275 else: 276 return os.path.join(build_to_src, self.subdir, self.fname) 277 278 @lru_cache(maxsize=None) 279 def absolute_path(self, srcdir: str, builddir: str) -> str: 280 absdir = srcdir 281 if self.is_built: 282 absdir = builddir 283 return os.path.join(absdir, self.relative_name()) 284 285 def endswith(self, ending: str) -> bool: 286 return self.fname.endswith(ending) 287 288 def split(self, s: str) -> T.List[str]: 289 return self.fname.split(s) 290 291 def __eq__(self, other) -> bool: 292 if not isinstance(other, File): 293 return NotImplemented 294 return (self.fname, self.subdir, self.is_built) == (other.fname, other.subdir, other.is_built) 295 296 def __hash__(self) -> int: 297 return hash((self.fname, self.subdir, self.is_built)) 298 299 @lru_cache(maxsize=None) 300 def relative_name(self) -> str: 301 return os.path.join(self.subdir, self.fname) 302 303 304def get_compiler_for_source(compilers: T.Iterable['CompilerType'], src: str) -> 'CompilerType': 305 """Given a set of compilers and a source, find the compiler for that source type.""" 306 for comp in compilers: 307 if comp.can_compile(src): 308 return comp 309 raise MesonException('No specified compiler can handle file {!s}'.format(src)) 310 311 312def classify_unity_sources(compilers: T.Iterable['CompilerType'], sources: T.Iterable[str]) -> T.Dict['CompilerType', T.List[str]]: 313 compsrclist = {} # type: T.Dict[CompilerType, T.List[str]] 314 for src in sources: 315 comp = get_compiler_for_source(compilers, src) 316 if comp not in compsrclist: 317 compsrclist[comp] = [src] 318 else: 319 compsrclist[comp].append(src) 320 return compsrclist 321 322 323class OrderedEnum(Enum): 324 """ 325 An Enum which additionally offers homogeneous ordered comparison. 326 """ 327 def __ge__(self, other): 328 if self.__class__ is other.__class__: 329 return self.value >= other.value 330 return NotImplemented 331 332 def __gt__(self, other): 333 if self.__class__ is other.__class__: 334 return self.value > other.value 335 return NotImplemented 336 337 def __le__(self, other): 338 if self.__class__ is other.__class__: 339 return self.value <= other.value 340 return NotImplemented 341 342 def __lt__(self, other): 343 if self.__class__ is other.__class__: 344 return self.value < other.value 345 return NotImplemented 346 347 348class MachineChoice(OrderedEnum): 349 350 """Enum class representing one of the two abstract machine names used in 351 most places: the build, and host, machines. 352 """ 353 354 BUILD = 0 355 HOST = 1 356 357 def get_lower_case_name(self) -> str: 358 return PerMachine('build', 'host')[self] 359 360 def get_prefix(self) -> str: 361 return PerMachine('build.', '')[self] 362 363 364class PerMachine(T.Generic[_T]): 365 def __init__(self, build: _T, host: _T): 366 self.build = build 367 self.host = host 368 369 def __getitem__(self, machine: MachineChoice) -> _T: 370 return { 371 MachineChoice.BUILD: self.build, 372 MachineChoice.HOST: self.host, 373 }[machine] 374 375 def __setitem__(self, machine: MachineChoice, val: _T) -> None: 376 setattr(self, machine.get_lower_case_name(), val) 377 378 def miss_defaulting(self) -> "PerMachineDefaultable[T.Optional[_T]]": 379 """Unset definition duplicated from their previous to None 380 381 This is the inverse of ''default_missing''. By removing defaulted 382 machines, we can elaborate the original and then redefault them and thus 383 avoid repeating the elaboration explicitly. 384 """ 385 unfreeze = PerMachineDefaultable() # type: PerMachineDefaultable[T.Optional[_T]] 386 unfreeze.build = self.build 387 unfreeze.host = self.host 388 if unfreeze.host == unfreeze.build: 389 unfreeze.host = None 390 return unfreeze 391 392 393class PerThreeMachine(PerMachine[_T]): 394 """Like `PerMachine` but includes `target` too. 395 396 It turns out just one thing do we need track the target machine. There's no 397 need to computer the `target` field so we don't bother overriding the 398 `__getitem__`/`__setitem__` methods. 399 """ 400 def __init__(self, build: _T, host: _T, target: _T): 401 super().__init__(build, host) 402 self.target = target 403 404 def miss_defaulting(self) -> "PerThreeMachineDefaultable[T.Optional[_T]]": 405 """Unset definition duplicated from their previous to None 406 407 This is the inverse of ''default_missing''. By removing defaulted 408 machines, we can elaborate the original and then redefault them and thus 409 avoid repeating the elaboration explicitly. 410 """ 411 unfreeze = PerThreeMachineDefaultable() # type: PerThreeMachineDefaultable[T.Optional[_T]] 412 unfreeze.build = self.build 413 unfreeze.host = self.host 414 unfreeze.target = self.target 415 if unfreeze.target == unfreeze.host: 416 unfreeze.target = None 417 if unfreeze.host == unfreeze.build: 418 unfreeze.host = None 419 return unfreeze 420 421 def matches_build_machine(self, machine: MachineChoice) -> bool: 422 return self.build == self[machine] 423 424 425class PerMachineDefaultable(PerMachine[T.Optional[_T]]): 426 """Extends `PerMachine` with the ability to default from `None`s. 427 """ 428 def __init__(self): 429 super().__init__(None, None) 430 431 def default_missing(self) -> "PerMachine[T.Optional[_T]]": 432 """Default host to build 433 434 This allows just specifying nothing in the native case, and just host in the 435 cross non-compiler case. 436 """ 437 freeze = PerMachine(self.build, self.host) 438 if freeze.host is None: 439 freeze.host = freeze.build 440 return freeze 441 442 443class PerThreeMachineDefaultable(PerMachineDefaultable, PerThreeMachine[T.Optional[_T]]): 444 """Extends `PerThreeMachine` with the ability to default from `None`s. 445 """ 446 def __init__(self): 447 PerThreeMachine.__init__(self, None, None, None) 448 449 def default_missing(self) -> "PerThreeMachine[T.Optional[_T]]": 450 """Default host to build and target to host. 451 452 This allows just specifying nothing in the native case, just host in the 453 cross non-compiler case, and just target in the native-built 454 cross-compiler case. 455 """ 456 freeze = PerThreeMachine(self.build, self.host, self.target) 457 if freeze.host is None: 458 freeze.host = freeze.build 459 if freeze.target is None: 460 freeze.target = freeze.host 461 return freeze 462 463 464def is_sunos() -> bool: 465 return platform.system().lower() == 'sunos' 466 467 468def is_osx() -> bool: 469 return platform.system().lower() == 'darwin' 470 471 472def is_linux() -> bool: 473 return platform.system().lower() == 'linux' 474 475 476def is_android() -> bool: 477 return platform.system().lower() == 'android' 478 479 480def is_haiku() -> bool: 481 return platform.system().lower() == 'haiku' 482 483 484def is_openbsd() -> bool: 485 return platform.system().lower() == 'openbsd' 486 487 488def is_windows() -> bool: 489 platname = platform.system().lower() 490 return platname == 'windows' or 'mingw' in platname 491 492 493def is_cygwin() -> bool: 494 return platform.system().lower().startswith('cygwin') 495 496 497def is_debianlike() -> bool: 498 return os.path.isfile('/etc/debian_version') 499 500 501def is_dragonflybsd() -> bool: 502 return platform.system().lower() == 'dragonfly' 503 504 505def is_netbsd() -> bool: 506 return platform.system().lower() == 'netbsd' 507 508 509def is_freebsd() -> bool: 510 return platform.system().lower() == 'freebsd' 511 512def is_irix() -> bool: 513 return platform.system().startswith('irix') 514 515def is_hurd() -> bool: 516 return platform.system().lower() == 'gnu' 517 518def is_qnx() -> bool: 519 return platform.system().lower() == 'qnx' 520 521def exe_exists(arglist: T.List[str]) -> bool: 522 try: 523 if subprocess.run(arglist, timeout=10).returncode == 0: 524 return True 525 except (FileNotFoundError, subprocess.TimeoutExpired): 526 pass 527 return False 528 529 530@lru_cache(maxsize=None) 531def darwin_get_object_archs(objpath: str) -> T.List[str]: 532 ''' 533 For a specific object (executable, static library, dylib, etc), run `lipo` 534 to fetch the list of archs supported by it. Supports both thin objects and 535 'fat' objects. 536 ''' 537 _, stdo, stderr = Popen_safe(['lipo', '-info', objpath]) 538 if not stdo: 539 mlog.debug('lipo {}: {}'.format(objpath, stderr)) 540 return None 541 stdo = stdo.rsplit(': ', 1)[1] 542 # Convert from lipo-style archs to meson-style CPUs 543 stdo = stdo.replace('i386', 'x86') 544 stdo = stdo.replace('arm64', 'aarch64') 545 # Add generic name for armv7 and armv7s 546 if 'armv7' in stdo: 547 stdo += ' arm' 548 return stdo.split() 549 550 551def detect_vcs(source_dir: T.Union[str, Path]) -> T.Optional[T.Dict[str, str]]: 552 vcs_systems = [ 553 dict(name = 'git', cmd = 'git', repo_dir = '.git', get_rev = 'git describe --dirty=+', rev_regex = '(.*)', dep = '.git/logs/HEAD'), 554 dict(name = 'mercurial', cmd = 'hg', repo_dir = '.hg', get_rev = 'hg id -i', rev_regex = '(.*)', dep = '.hg/dirstate'), 555 dict(name = 'subversion', cmd = 'svn', repo_dir = '.svn', get_rev = 'svn info', rev_regex = 'Revision: (.*)', dep = '.svn/wc.db'), 556 dict(name = 'bazaar', cmd = 'bzr', repo_dir = '.bzr', get_rev = 'bzr revno', rev_regex = '(.*)', dep = '.bzr'), 557 ] 558 if isinstance(source_dir, str): 559 source_dir = Path(source_dir) 560 561 parent_paths_and_self = collections.deque(source_dir.parents) 562 # Prepend the source directory to the front so we can check it; 563 # source_dir.parents doesn't include source_dir 564 parent_paths_and_self.appendleft(source_dir) 565 for curdir in parent_paths_and_self: 566 for vcs in vcs_systems: 567 if Path.is_dir(curdir.joinpath(vcs['repo_dir'])) and shutil.which(vcs['cmd']): 568 vcs['wc_dir'] = str(curdir) 569 return vcs 570 return None 571 572# a helper class which implements the same version ordering as RPM 573class Version: 574 def __init__(self, s: str): 575 self._s = s 576 577 # split into numeric, alphabetic and non-alphanumeric sequences 578 sequences1 = re.finditer(r'(\d+|[a-zA-Z]+|[^a-zA-Z\d]+)', s) 579 580 # non-alphanumeric separators are discarded 581 sequences2 = [m for m in sequences1 if not re.match(r'[^a-zA-Z\d]+', m.group(1))] 582 583 # numeric sequences are converted from strings to ints 584 sequences3 = [int(m.group(1)) if m.group(1).isdigit() else m.group(1) for m in sequences2] 585 586 self._v = sequences3 587 588 def __str__(self): 589 return '%s (V=%s)' % (self._s, str(self._v)) 590 591 def __repr__(self): 592 return '<Version: {}>'.format(self._s) 593 594 def __lt__(self, other): 595 if isinstance(other, Version): 596 return self.__cmp(other, operator.lt) 597 return NotImplemented 598 599 def __gt__(self, other): 600 if isinstance(other, Version): 601 return self.__cmp(other, operator.gt) 602 return NotImplemented 603 604 def __le__(self, other): 605 if isinstance(other, Version): 606 return self.__cmp(other, operator.le) 607 return NotImplemented 608 609 def __ge__(self, other): 610 if isinstance(other, Version): 611 return self.__cmp(other, operator.ge) 612 return NotImplemented 613 614 def __eq__(self, other): 615 if isinstance(other, Version): 616 return self._v == other._v 617 return NotImplemented 618 619 def __ne__(self, other): 620 if isinstance(other, Version): 621 return self._v != other._v 622 return NotImplemented 623 624 def __cmp(self, other: 'Version', comparator: T.Callable[[T.Any, T.Any], bool]) -> bool: 625 # compare each sequence in order 626 for ours, theirs in zip(self._v, other._v): 627 # sort a non-digit sequence before a digit sequence 628 ours_is_int = isinstance(ours, int) 629 theirs_is_int = isinstance(theirs, int) 630 if ours_is_int != theirs_is_int: 631 return comparator(ours_is_int, theirs_is_int) 632 633 if ours != theirs: 634 return comparator(ours, theirs) 635 636 # if equal length, all components have matched, so equal 637 # otherwise, the version with a suffix remaining is greater 638 return comparator(len(self._v), len(other._v)) 639 640 641def _version_extract_cmpop(vstr2: str) -> T.Tuple[T.Callable[[T.Any, T.Any], bool], str]: 642 if vstr2.startswith('>='): 643 cmpop = operator.ge 644 vstr2 = vstr2[2:] 645 elif vstr2.startswith('<='): 646 cmpop = operator.le 647 vstr2 = vstr2[2:] 648 elif vstr2.startswith('!='): 649 cmpop = operator.ne 650 vstr2 = vstr2[2:] 651 elif vstr2.startswith('=='): 652 cmpop = operator.eq 653 vstr2 = vstr2[2:] 654 elif vstr2.startswith('='): 655 cmpop = operator.eq 656 vstr2 = vstr2[1:] 657 elif vstr2.startswith('>'): 658 cmpop = operator.gt 659 vstr2 = vstr2[1:] 660 elif vstr2.startswith('<'): 661 cmpop = operator.lt 662 vstr2 = vstr2[1:] 663 else: 664 cmpop = operator.eq 665 666 return (cmpop, vstr2) 667 668 669def version_compare(vstr1: str, vstr2: str) -> bool: 670 (cmpop, vstr2) = _version_extract_cmpop(vstr2) 671 return cmpop(Version(vstr1), Version(vstr2)) 672 673 674def version_compare_many(vstr1: str, conditions: T.Union[str, T.Iterable[str]]) -> T.Tuple[bool, T.List[str], T.List[str]]: 675 if isinstance(conditions, str): 676 conditions = [conditions] 677 found = [] 678 not_found = [] 679 for req in conditions: 680 if not version_compare(vstr1, req): 681 not_found.append(req) 682 else: 683 found.append(req) 684 return not_found == [], not_found, found 685 686 687# determine if the minimum version satisfying the condition |condition| exceeds 688# the minimum version for a feature |minimum| 689def version_compare_condition_with_min(condition: str, minimum: str) -> bool: 690 if condition.startswith('>='): 691 cmpop = operator.le 692 condition = condition[2:] 693 elif condition.startswith('<='): 694 return False 695 elif condition.startswith('!='): 696 return False 697 elif condition.startswith('=='): 698 cmpop = operator.le 699 condition = condition[2:] 700 elif condition.startswith('='): 701 cmpop = operator.le 702 condition = condition[1:] 703 elif condition.startswith('>'): 704 cmpop = operator.lt 705 condition = condition[1:] 706 elif condition.startswith('<'): 707 return False 708 else: 709 cmpop = operator.le 710 711 # Declaring a project(meson_version: '>=0.46') and then using features in 712 # 0.46.0 is valid, because (knowing the meson versioning scheme) '0.46.0' is 713 # the lowest version which satisfies the constraint '>=0.46'. 714 # 715 # But this will fail here, because the minimum version required by the 716 # version constraint ('0.46') is strictly less (in our version comparison) 717 # than the minimum version needed for the feature ('0.46.0'). 718 # 719 # Map versions in the constraint of the form '0.46' to '0.46.0', to embed 720 # this knowledge of the meson versioning scheme. 721 condition = condition.strip() 722 if re.match(r'^\d+.\d+$', condition): 723 condition += '.0' 724 725 return cmpop(Version(minimum), Version(condition)) 726 727 728def default_libdir() -> str: 729 if is_debianlike(): 730 try: 731 pc = subprocess.Popen(['dpkg-architecture', '-qDEB_HOST_MULTIARCH'], 732 stdout=subprocess.PIPE, 733 stderr=subprocess.DEVNULL) 734 (stdo, _) = pc.communicate() 735 if pc.returncode == 0: 736 archpath = stdo.decode().strip() 737 return 'lib/' + archpath 738 except Exception: 739 pass 740 if is_freebsd() or is_irix(): 741 return 'lib' 742 if os.path.isdir('/usr/lib64') and not os.path.islink('/usr/lib64'): 743 return 'lib64' 744 return 'lib' 745 746 747def default_libexecdir() -> str: 748 # There is no way to auto-detect this, so it must be set at build time 749 return 'libexec' 750 751 752def default_prefix() -> str: 753 return 'c:/' if is_windows() else '/usr/local' 754 755 756def get_library_dirs() -> T.List[str]: 757 if is_windows(): 758 return ['C:/mingw/lib'] # TODO: get programmatically 759 if is_osx(): 760 return ['/usr/lib'] # TODO: get programmatically 761 # The following is probably Debian/Ubuntu specific. 762 # /usr/local/lib is first because it contains stuff 763 # installed by the sysadmin and is probably more up-to-date 764 # than /usr/lib. If you feel that this search order is 765 # problematic, please raise the issue on the mailing list. 766 unixdirs = ['/usr/local/lib', '/usr/lib', '/lib'] 767 768 if is_freebsd(): 769 return unixdirs 770 # FIXME: this needs to be further genericized for aarch64 etc. 771 machine = platform.machine() 772 if machine in ('i386', 'i486', 'i586', 'i686'): 773 plat = 'i386' 774 elif machine.startswith('arm'): 775 plat = 'arm' 776 else: 777 plat = '' 778 779 # Solaris puts 32-bit libraries in the main /lib & /usr/lib directories 780 # and 64-bit libraries in platform specific subdirectories. 781 if is_sunos(): 782 if machine == 'i86pc': 783 plat = 'amd64' 784 elif machine.startswith('sun4'): 785 plat = 'sparcv9' 786 787 usr_platdir = Path('/usr/lib/') / plat 788 if usr_platdir.is_dir(): 789 unixdirs += [str(x) for x in (usr_platdir).iterdir() if x.is_dir()] 790 if os.path.exists('/usr/lib64'): 791 unixdirs.append('/usr/lib64') 792 793 lib_platdir = Path('/lib/') / plat 794 if lib_platdir.is_dir(): 795 unixdirs += [str(x) for x in (lib_platdir).iterdir() if x.is_dir()] 796 if os.path.exists('/lib64'): 797 unixdirs.append('/lib64') 798 799 return unixdirs 800 801 802def has_path_sep(name: str, sep: str = '/\\') -> bool: 803 'Checks if any of the specified @sep path separators are in @name' 804 for each in sep: 805 if each in name: 806 return True 807 return False 808 809 810if is_windows(): 811 # shlex.split is not suitable for splitting command line on Window (https://bugs.python.org/issue1724822); 812 # shlex.quote is similarly problematic. Below are "proper" implementations of these functions according to 813 # https://docs.microsoft.com/en-us/cpp/c-language/parsing-c-command-line-arguments and 814 # https://blogs.msdn.microsoft.com/twistylittlepassagesallalike/2011/04/23/everyone-quotes-command-line-arguments-the-wrong-way/ 815 816 _whitespace = ' \t\n\r' 817 _find_unsafe_char = re.compile(r'[{}"]'.format(_whitespace)).search 818 819 def quote_arg(arg: str) -> str: 820 if arg and not _find_unsafe_char(arg): 821 return arg 822 823 result = '"' 824 num_backslashes = 0 825 for c in arg: 826 if c == '\\': 827 num_backslashes += 1 828 else: 829 if c == '"': 830 # Escape all backslashes and the following double quotation mark 831 num_backslashes = num_backslashes * 2 + 1 832 833 result += num_backslashes * '\\' + c 834 num_backslashes = 0 835 836 # Escape all backslashes, but let the terminating double quotation 837 # mark we add below be interpreted as a metacharacter 838 result += (num_backslashes * 2) * '\\' + '"' 839 return result 840 841 def split_args(cmd: str) -> T.List[str]: 842 result = [] 843 arg = '' 844 num_backslashes = 0 845 num_quotes = 0 846 in_quotes = False 847 for c in cmd: 848 if c == '\\': 849 num_backslashes += 1 850 else: 851 if c == '"' and not (num_backslashes % 2): 852 # unescaped quote, eat it 853 arg += (num_backslashes // 2) * '\\' 854 num_quotes += 1 855 in_quotes = not in_quotes 856 elif c in _whitespace and not in_quotes: 857 if arg or num_quotes: 858 # reached the end of the argument 859 result.append(arg) 860 arg = '' 861 num_quotes = 0 862 else: 863 if c == '"': 864 # escaped quote 865 num_backslashes = (num_backslashes - 1) // 2 866 867 arg += num_backslashes * '\\' + c 868 869 num_backslashes = 0 870 871 if arg or num_quotes: 872 result.append(arg) 873 874 return result 875else: 876 def quote_arg(arg: str) -> str: 877 return shlex.quote(arg) 878 879 def split_args(cmd: str) -> T.List[str]: 880 return shlex.split(cmd) 881 882 883def join_args(args: T.Iterable[str]) -> str: 884 return ' '.join([quote_arg(x) for x in args]) 885 886 887def do_replacement(regex: T.Pattern[str], line: str, variable_format: str, 888 confdata: 'ConfigurationData') -> T.Tuple[str, T.Set[str]]: 889 missing_variables = set() # type: T.Set[str] 890 if variable_format == 'cmake': 891 start_tag = '${' 892 backslash_tag = '\\${' 893 else: 894 assert variable_format in ['meson', 'cmake@'] 895 start_tag = '@' 896 backslash_tag = '\\@' 897 898 def variable_replace(match: T.Match[str]) -> str: 899 # Pairs of escape characters before '@' or '\@' 900 if match.group(0).endswith('\\'): 901 num_escapes = match.end(0) - match.start(0) 902 return '\\' * (num_escapes // 2) 903 # Single escape character and '@' 904 elif match.group(0) == backslash_tag: 905 return start_tag 906 # Template variable to be replaced 907 else: 908 varname = match.group(1) 909 if varname in confdata: 910 (var, desc) = confdata.get(varname) 911 if isinstance(var, str): 912 pass 913 elif isinstance(var, int): 914 var = str(var) 915 else: 916 msg = 'Tried to replace variable {!r} value with ' \ 917 'something other than a string or int: {!r}' 918 raise MesonException(msg.format(varname, var)) 919 else: 920 missing_variables.add(varname) 921 var = '' 922 return var 923 return re.sub(regex, variable_replace, line), missing_variables 924 925def do_define(regex: T.Pattern[str], line: str, confdata: 'ConfigurationData', variable_format: str) -> str: 926 def get_cmake_define(line: str, confdata: 'ConfigurationData') -> str: 927 arr = line.split() 928 define_value=[] 929 for token in arr[2:]: 930 try: 931 (v, desc) = confdata.get(token) 932 define_value += [v] 933 except KeyError: 934 define_value += [token] 935 return ' '.join(define_value) 936 937 arr = line.split() 938 if variable_format == 'meson' and len(arr) != 2: 939 raise MesonException('#mesondefine does not contain exactly two tokens: %s' % line.strip()) 940 941 varname = arr[1] 942 try: 943 (v, desc) = confdata.get(varname) 944 except KeyError: 945 return '/* #undef %s */\n' % varname 946 if isinstance(v, bool): 947 if v: 948 return '#define %s\n' % varname 949 else: 950 return '#undef %s\n' % varname 951 elif isinstance(v, int): 952 return '#define %s %d\n' % (varname, v) 953 elif isinstance(v, str): 954 if variable_format == 'meson': 955 result = v 956 else: 957 result = get_cmake_define(line, confdata) 958 result = '#define %s %s\n' % (varname, result) 959 (result, missing_variable) = do_replacement(regex, result, variable_format, confdata) 960 return result 961 else: 962 raise MesonException('#mesondefine argument "%s" is of unknown type.' % varname) 963 964def do_conf_str (data: list, confdata: 'ConfigurationData', variable_format: str, 965 encoding: str = 'utf-8') -> T.Tuple[T.List[str],T.Set[str], bool]: 966 def line_is_valid(line : str, variable_format: str): 967 if variable_format == 'meson': 968 if '#cmakedefine' in line: 969 return False 970 else: #cmake format 971 if '#mesondefine' in line: 972 return False 973 return True 974 975 # Only allow (a-z, A-Z, 0-9, _, -) as valid characters for a define 976 # Also allow escaping '@' with '\@' 977 if variable_format in ['meson', 'cmake@']: 978 regex = re.compile(r'(?:\\\\)+(?=\\?@)|\\@|@([-a-zA-Z0-9_]+)@') 979 elif variable_format == 'cmake': 980 regex = re.compile(r'(?:\\\\)+(?=\\?\$)|\\\${|\${([-a-zA-Z0-9_]+)}') 981 else: 982 raise MesonException('Format "{}" not handled'.format(variable_format)) 983 984 search_token = '#mesondefine' 985 if variable_format != 'meson': 986 search_token = '#cmakedefine' 987 988 result = [] 989 missing_variables = set() 990 # Detect when the configuration data is empty and no tokens were found 991 # during substitution so we can warn the user to use the `copy:` kwarg. 992 confdata_useless = not confdata.keys() 993 for line in data: 994 if line.startswith(search_token): 995 confdata_useless = False 996 line = do_define(regex, line, confdata, variable_format) 997 else: 998 if not line_is_valid(line,variable_format): 999 raise MesonException('Format "{}" mismatched'.format(variable_format)) 1000 line, missing = do_replacement(regex, line, variable_format, confdata) 1001 missing_variables.update(missing) 1002 if missing: 1003 confdata_useless = False 1004 result.append(line) 1005 1006 return result, missing_variables, confdata_useless 1007 1008def do_conf_file(src: str, dst: str, confdata: 'ConfigurationData', variable_format: str, 1009 encoding: str = 'utf-8') -> T.Tuple[T.Set[str], bool]: 1010 try: 1011 with open(src, encoding=encoding, newline='') as f: 1012 data = f.readlines() 1013 except Exception as e: 1014 raise MesonException('Could not read input file %s: %s' % (src, str(e))) 1015 1016 (result, missing_variables, confdata_useless) = do_conf_str(data, confdata, variable_format, encoding) 1017 dst_tmp = dst + '~' 1018 try: 1019 with open(dst_tmp, 'w', encoding=encoding, newline='') as f: 1020 f.writelines(result) 1021 except Exception as e: 1022 raise MesonException('Could not write output file %s: %s' % (dst, str(e))) 1023 shutil.copymode(src, dst_tmp) 1024 replace_if_different(dst, dst_tmp) 1025 return missing_variables, confdata_useless 1026 1027CONF_C_PRELUDE = '''/* 1028 * Autogenerated by the Meson build system. 1029 * Do not edit, your changes will be lost. 1030 */ 1031 1032#pragma once 1033 1034''' 1035 1036CONF_NASM_PRELUDE = '''; Autogenerated by the Meson build system. 1037; Do not edit, your changes will be lost. 1038 1039''' 1040 1041def dump_conf_header(ofilename: str, cdata: 'ConfigurationData', output_format: str) -> None: 1042 if output_format == 'c': 1043 prelude = CONF_C_PRELUDE 1044 prefix = '#' 1045 elif output_format == 'nasm': 1046 prelude = CONF_NASM_PRELUDE 1047 prefix = '%' 1048 1049 ofilename_tmp = ofilename + '~' 1050 with open(ofilename_tmp, 'w', encoding='utf-8') as ofile: 1051 ofile.write(prelude) 1052 for k in sorted(cdata.keys()): 1053 (v, desc) = cdata.get(k) 1054 if desc: 1055 if output_format == 'c': 1056 ofile.write('/* %s */\n' % desc) 1057 elif output_format == 'nasm': 1058 for line in desc.split('\n'): 1059 ofile.write('; %s\n' % line) 1060 if isinstance(v, bool): 1061 if v: 1062 ofile.write('%sdefine %s\n\n' % (prefix, k)) 1063 else: 1064 ofile.write('%sundef %s\n\n' % (prefix, k)) 1065 elif isinstance(v, (int, str)): 1066 ofile.write('%sdefine %s %s\n\n' % (prefix, k, v)) 1067 else: 1068 raise MesonException('Unknown data type in configuration file entry: ' + k) 1069 replace_if_different(ofilename, ofilename_tmp) 1070 1071 1072def replace_if_different(dst: str, dst_tmp: str) -> None: 1073 # If contents are identical, don't touch the file to prevent 1074 # unnecessary rebuilds. 1075 different = True 1076 try: 1077 with open(dst, 'rb') as f1, open(dst_tmp, 'rb') as f2: 1078 if f1.read() == f2.read(): 1079 different = False 1080 except FileNotFoundError: 1081 pass 1082 if different: 1083 os.replace(dst_tmp, dst) 1084 else: 1085 os.unlink(dst_tmp) 1086 1087 1088@T.overload 1089def unholder(item: 'ObjectHolder[_T]') -> _T: ... 1090 1091@T.overload 1092def unholder(item: T.List['ObjectHolder[_T]']) -> T.List[_T]: ... 1093 1094@T.overload 1095def unholder(item: T.List[_T]) -> T.List[_T]: ... 1096 1097@T.overload 1098def unholder(item: T.List[T.Union[_T, 'ObjectHolder[_T]']]) -> T.List[_T]: ... 1099 1100def unholder(item): 1101 """Get the held item of an object holder or list of object holders.""" 1102 if isinstance(item, list): 1103 return [i.held_object if hasattr(i, 'held_object') else i for i in item] 1104 if hasattr(item, 'held_object'): 1105 return item.held_object 1106 return item 1107 1108 1109def listify(item: T.Any, flatten: bool = True) -> T.List[T.Any]: 1110 ''' 1111 Returns a list with all args embedded in a list if they are not a list. 1112 This function preserves order. 1113 @flatten: Convert lists of lists to a flat list 1114 ''' 1115 if not isinstance(item, list): 1116 return [item] 1117 result = [] # type: T.List[T.Any] 1118 for i in item: 1119 if flatten and isinstance(i, list): 1120 result += listify(i, flatten=True) 1121 else: 1122 result.append(i) 1123 return result 1124 1125 1126def extract_as_list(dict_object: T.Dict[_T, _U], key: _T, pop: bool = False) -> T.List[_U]: 1127 ''' 1128 Extracts all values from given dict_object and listifies them. 1129 ''' 1130 fetch = dict_object.get 1131 if pop: 1132 fetch = dict_object.pop 1133 # If there's only one key, we don't return a list with one element 1134 return listify(fetch(key, []), flatten=True) 1135 1136 1137def typeslistify(item: 'T.Union[_T, T.Sequence[_T]]', 1138 types: 'T.Union[T.Type[_T], T.Tuple[T.Type[_T]]]') -> T.List[_T]: 1139 ''' 1140 Ensure that type(@item) is one of @types or a 1141 list of items all of which are of type @types 1142 ''' 1143 if isinstance(item, types): 1144 item = T.cast(T.List[_T], [item]) 1145 if not isinstance(item, list): 1146 raise MesonException('Item must be a list or one of {!r}'.format(types)) 1147 for i in item: 1148 if i is not None and not isinstance(i, types): 1149 raise MesonException('List item must be one of {!r}'.format(types)) 1150 return item 1151 1152 1153def stringlistify(item: T.Union[T.Any, T.Sequence[T.Any]]) -> T.List[str]: 1154 return typeslistify(item, str) 1155 1156 1157def expand_arguments(args: T.Iterable[str]) -> T.Optional[T.List[str]]: 1158 expended_args = [] # type: T.List[str] 1159 for arg in args: 1160 if not arg.startswith('@'): 1161 expended_args.append(arg) 1162 continue 1163 1164 args_file = arg[1:] 1165 try: 1166 with open(args_file) as f: 1167 extended_args = f.read().split() 1168 expended_args += extended_args 1169 except Exception as e: 1170 mlog.error('Expanding command line arguments:', args_file, 'not found') 1171 mlog.exception(e) 1172 return None 1173 return expended_args 1174 1175 1176def partition(pred: T.Callable[[_T], object], iterable: T.Iterator[_T]) -> T.Tuple[T.Iterator[_T], T.Iterator[_T]]: 1177 """Use a predicate to partition entries into false entries and true 1178 entries. 1179 1180 >>> x, y = partition(is_odd, range(10)) 1181 >>> (list(x), list(y)) 1182 ([0, 2, 4, 6, 8], [1, 3, 5, 7, 9]) 1183 """ 1184 t1, t2 = tee(iterable) 1185 return filterfalse(pred, t1), filter(pred, t2) 1186 1187 1188def Popen_safe(args: T.List[str], write: T.Optional[str] = None, 1189 stdout: T.Union[T.BinaryIO, int] = subprocess.PIPE, 1190 stderr: T.Union[T.BinaryIO, int] = subprocess.PIPE, 1191 **kwargs: T.Any) -> T.Tuple[subprocess.Popen, str, str]: 1192 import locale 1193 encoding = locale.getpreferredencoding() 1194 # Redirect stdin to DEVNULL otherwise the command run by us here might mess 1195 # up the console and ANSI colors will stop working on Windows. 1196 if 'stdin' not in kwargs: 1197 kwargs['stdin'] = subprocess.DEVNULL 1198 if sys.version_info < (3, 6) or not sys.stdout.encoding or encoding.upper() != 'UTF-8': 1199 p, o, e = Popen_safe_legacy(args, write=write, stdout=stdout, stderr=stderr, **kwargs) 1200 else: 1201 p = subprocess.Popen(args, universal_newlines=True, close_fds=False, 1202 stdout=stdout, stderr=stderr, **kwargs) 1203 o, e = p.communicate(write) 1204 # Sometimes the command that we run will call another command which will be 1205 # without the above stdin workaround, so set the console mode again just in 1206 # case. 1207 mlog.setup_console() 1208 return p, o, e 1209 1210 1211def Popen_safe_legacy(args: T.List[str], write: T.Optional[str] = None, 1212 stdout: T.Union[T.BinaryIO, int] = subprocess.PIPE, 1213 stderr: T.Union[T.BinaryIO, int] = subprocess.PIPE, 1214 **kwargs: T.Any) -> T.Tuple[subprocess.Popen, str, str]: 1215 p = subprocess.Popen(args, universal_newlines=False, close_fds=False, 1216 stdout=stdout, stderr=stderr, **kwargs) 1217 input_ = None # type: T.Optional[bytes] 1218 if write is not None: 1219 input_ = write.encode('utf-8') 1220 o, e = p.communicate(input_) 1221 if o is not None: 1222 if sys.stdout.encoding: 1223 o = o.decode(encoding=sys.stdout.encoding, errors='replace').replace('\r\n', '\n') 1224 else: 1225 o = o.decode(errors='replace').replace('\r\n', '\n') 1226 if e is not None: 1227 if sys.stderr.encoding: 1228 e = e.decode(encoding=sys.stderr.encoding, errors='replace').replace('\r\n', '\n') 1229 else: 1230 e = e.decode(errors='replace').replace('\r\n', '\n') 1231 return p, o, e 1232 1233 1234def iter_regexin_iter(regexiter: T.Iterable[str], initer: T.Iterable[str]) -> T.Optional[str]: 1235 ''' 1236 Takes each regular expression in @regexiter and tries to search for it in 1237 every item in @initer. If there is a match, returns that match. 1238 Else returns False. 1239 ''' 1240 for regex in regexiter: 1241 for ii in initer: 1242 if not isinstance(ii, str): 1243 continue 1244 match = re.search(regex, ii) 1245 if match: 1246 return match.group() 1247 return None 1248 1249 1250def _substitute_values_check_errors(command: T.List[str], values: T.Dict[str, str]) -> None: 1251 # Error checking 1252 inregex = ['@INPUT([0-9]+)?@', '@PLAINNAME@', '@BASENAME@'] # type: T.List[str] 1253 outregex = ['@OUTPUT([0-9]+)?@', '@OUTDIR@'] # type: T.List[str] 1254 if '@INPUT@' not in values: 1255 # Error out if any input-derived templates are present in the command 1256 match = iter_regexin_iter(inregex, command) 1257 if match: 1258 m = 'Command cannot have {!r}, since no input files were specified' 1259 raise MesonException(m.format(match)) 1260 else: 1261 if len(values['@INPUT@']) > 1: 1262 # Error out if @PLAINNAME@ or @BASENAME@ is present in the command 1263 match = iter_regexin_iter(inregex[1:], command) 1264 if match: 1265 raise MesonException('Command cannot have {!r} when there is ' 1266 'more than one input file'.format(match)) 1267 # Error out if an invalid @INPUTnn@ template was specified 1268 for each in command: 1269 if not isinstance(each, str): 1270 continue 1271 match2 = re.search(inregex[0], each) 1272 if match2 and match2.group() not in values: 1273 m = 'Command cannot have {!r} since there are only {!r} inputs' 1274 raise MesonException(m.format(match2.group(), len(values['@INPUT@']))) 1275 if '@OUTPUT@' not in values: 1276 # Error out if any output-derived templates are present in the command 1277 match = iter_regexin_iter(outregex, command) 1278 if match: 1279 m = 'Command cannot have {!r} since there are no outputs' 1280 raise MesonException(m.format(match)) 1281 else: 1282 # Error out if an invalid @OUTPUTnn@ template was specified 1283 for each in command: 1284 if not isinstance(each, str): 1285 continue 1286 match2 = re.search(outregex[0], each) 1287 if match2 and match2.group() not in values: 1288 m = 'Command cannot have {!r} since there are only {!r} outputs' 1289 raise MesonException(m.format(match2.group(), len(values['@OUTPUT@']))) 1290 1291 1292def substitute_values(command: T.List[str], values: T.Dict[str, str]) -> T.List[str]: 1293 ''' 1294 Substitute the template strings in the @values dict into the list of 1295 strings @command and return a new list. For a full list of the templates, 1296 see get_filenames_templates_dict() 1297 1298 If multiple inputs/outputs are given in the @values dictionary, we 1299 substitute @INPUT@ and @OUTPUT@ only if they are the entire string, not 1300 just a part of it, and in that case we substitute *all* of them. 1301 ''' 1302 # Error checking 1303 _substitute_values_check_errors(command, values) 1304 # Substitution 1305 outcmd = [] # type: T.List[str] 1306 rx_keys = [re.escape(key) for key in values if key not in ('@INPUT@', '@OUTPUT@')] 1307 value_rx = re.compile('|'.join(rx_keys)) if rx_keys else None 1308 for vv in command: 1309 if not isinstance(vv, str): 1310 outcmd.append(vv) 1311 elif '@INPUT@' in vv: 1312 inputs = values['@INPUT@'] 1313 if vv == '@INPUT@': 1314 outcmd += inputs 1315 elif len(inputs) == 1: 1316 outcmd.append(vv.replace('@INPUT@', inputs[0])) 1317 else: 1318 raise MesonException("Command has '@INPUT@' as part of a " 1319 "string and more than one input file") 1320 elif '@OUTPUT@' in vv: 1321 outputs = values['@OUTPUT@'] 1322 if vv == '@OUTPUT@': 1323 outcmd += outputs 1324 elif len(outputs) == 1: 1325 outcmd.append(vv.replace('@OUTPUT@', outputs[0])) 1326 else: 1327 raise MesonException("Command has '@OUTPUT@' as part of a " 1328 "string and more than one output file") 1329 # Append values that are exactly a template string. 1330 # This is faster than a string replace. 1331 elif vv in values: 1332 outcmd.append(values[vv]) 1333 # Substitute everything else with replacement 1334 elif value_rx: 1335 outcmd.append(value_rx.sub(lambda m: values[m.group(0)], vv)) 1336 else: 1337 outcmd.append(vv) 1338 return outcmd 1339 1340 1341def get_filenames_templates_dict(inputs: T.List[str], outputs: T.List[str]) -> T.Dict[str, T.Union[str, T.List[str]]]: 1342 ''' 1343 Create a dictionary with template strings as keys and values as values for 1344 the following templates: 1345 1346 @INPUT@ - the full path to one or more input files, from @inputs 1347 @OUTPUT@ - the full path to one or more output files, from @outputs 1348 @OUTDIR@ - the full path to the directory containing the output files 1349 1350 If there is only one input file, the following keys are also created: 1351 1352 @PLAINNAME@ - the filename of the input file 1353 @BASENAME@ - the filename of the input file with the extension removed 1354 1355 If there is more than one input file, the following keys are also created: 1356 1357 @INPUT0@, @INPUT1@, ... one for each input file 1358 1359 If there is more than one output file, the following keys are also created: 1360 1361 @OUTPUT0@, @OUTPUT1@, ... one for each output file 1362 ''' 1363 values = {} # type: T.Dict[str, T.Union[str, T.List[str]]] 1364 # Gather values derived from the input 1365 if inputs: 1366 # We want to substitute all the inputs. 1367 values['@INPUT@'] = inputs 1368 for (ii, vv) in enumerate(inputs): 1369 # Write out @INPUT0@, @INPUT1@, ... 1370 values['@INPUT{}@'.format(ii)] = vv 1371 if len(inputs) == 1: 1372 # Just one value, substitute @PLAINNAME@ and @BASENAME@ 1373 values['@PLAINNAME@'] = plain = os.path.basename(inputs[0]) 1374 values['@BASENAME@'] = os.path.splitext(plain)[0] 1375 if outputs: 1376 # Gather values derived from the outputs, similar to above. 1377 values['@OUTPUT@'] = outputs 1378 for (ii, vv) in enumerate(outputs): 1379 values['@OUTPUT{}@'.format(ii)] = vv 1380 # Outdir should be the same for all outputs 1381 values['@OUTDIR@'] = os.path.dirname(outputs[0]) 1382 # Many external programs fail on empty arguments. 1383 if values['@OUTDIR@'] == '': 1384 values['@OUTDIR@'] = '.' 1385 return values 1386 1387 1388def _make_tree_writable(topdir: str) -> None: 1389 # Ensure all files and directories under topdir are writable 1390 # (and readable) by owner. 1391 for d, _, files in os.walk(topdir): 1392 os.chmod(d, os.stat(d).st_mode | stat.S_IWRITE | stat.S_IREAD) 1393 for fname in files: 1394 fpath = os.path.join(d, fname) 1395 if os.path.isfile(fpath): 1396 os.chmod(fpath, os.stat(fpath).st_mode | stat.S_IWRITE | stat.S_IREAD) 1397 1398 1399def windows_proof_rmtree(f: str) -> None: 1400 # On Windows if anyone is holding a file open you can't 1401 # delete it. As an example an anti virus scanner might 1402 # be scanning files you are trying to delete. The only 1403 # way to fix this is to try again and again. 1404 delays = [0.1, 0.1, 0.2, 0.2, 0.2, 0.5, 0.5, 1, 1, 1, 1, 2] 1405 # Start by making the tree wriable. 1406 _make_tree_writable(f) 1407 for d in delays: 1408 try: 1409 shutil.rmtree(f) 1410 return 1411 except FileNotFoundError: 1412 return 1413 except OSError: 1414 time.sleep(d) 1415 # Try one last time and throw if it fails. 1416 shutil.rmtree(f) 1417 1418 1419def windows_proof_rm(fpath: str) -> None: 1420 """Like windows_proof_rmtree, but for a single file.""" 1421 if os.path.isfile(fpath): 1422 os.chmod(fpath, os.stat(fpath).st_mode | stat.S_IWRITE | stat.S_IREAD) 1423 delays = [0.1, 0.1, 0.2, 0.2, 0.2, 0.5, 0.5, 1, 1, 1, 1, 2] 1424 for d in delays: 1425 try: 1426 os.unlink(fpath) 1427 return 1428 except FileNotFoundError: 1429 return 1430 except OSError: 1431 time.sleep(d) 1432 os.unlink(fpath) 1433 1434 1435def detect_subprojects(spdir_name: str, current_dir: str = '', 1436 result: T.Optional[T.Dict[str, T.List[str]]] = None) -> T.Optional[T.Dict[str, T.List[str]]]: 1437 if result is None: 1438 result = {} 1439 spdir = os.path.join(current_dir, spdir_name) 1440 if not os.path.exists(spdir): 1441 return result 1442 for trial in glob(os.path.join(spdir, '*')): 1443 basename = os.path.basename(trial) 1444 if trial == 'packagecache': 1445 continue 1446 append_this = True 1447 if os.path.isdir(trial): 1448 detect_subprojects(spdir_name, trial, result) 1449 elif trial.endswith('.wrap') and os.path.isfile(trial): 1450 basename = os.path.splitext(basename)[0] 1451 else: 1452 append_this = False 1453 if append_this: 1454 if basename in result: 1455 result[basename].append(trial) 1456 else: 1457 result[basename] = [trial] 1458 return result 1459 1460 1461def substring_is_in_list(substr: str, strlist: T.List[str]) -> bool: 1462 for s in strlist: 1463 if substr in s: 1464 return True 1465 return False 1466 1467 1468class OrderedSet(T.MutableSet[_T]): 1469 """A set that preserves the order in which items are added, by first 1470 insertion. 1471 """ 1472 def __init__(self, iterable: T.Optional[T.Iterable[_T]] = None): 1473 # typing.OrderedDict is new in 3.7.2, so we can't use that, but we can 1474 # use MutableMapping, which is fine in this case. 1475 self.__container = collections.OrderedDict() # type: T.MutableMapping[_T, None] 1476 if iterable: 1477 self.update(iterable) 1478 1479 def __contains__(self, value: object) -> bool: 1480 return value in self.__container 1481 1482 def __iter__(self) -> T.Iterator[_T]: 1483 return iter(self.__container.keys()) 1484 1485 def __len__(self) -> int: 1486 return len(self.__container) 1487 1488 def __repr__(self) -> str: 1489 # Don't print 'OrderedSet("")' for an empty set. 1490 if self.__container: 1491 return 'OrderedSet("{}")'.format( 1492 '", "'.join(repr(e) for e in self.__container.keys())) 1493 return 'OrderedSet()' 1494 1495 def __reversed__(self) -> T.Iterator[_T]: 1496 # Mypy is complaining that sets cant be reversed, which is true for 1497 # unordered sets, but this is an ordered, set so reverse() makes sense. 1498 return reversed(self.__container.keys()) # type: ignore 1499 1500 def add(self, value: _T) -> None: 1501 self.__container[value] = None 1502 1503 def discard(self, value: _T) -> None: 1504 if value in self.__container: 1505 del self.__container[value] 1506 1507 def update(self, iterable: T.Iterable[_T]) -> None: 1508 for item in iterable: 1509 self.__container[item] = None 1510 1511 def difference(self, set_: T.Union[T.Set[_T], 'OrderedSet[_T]']) -> 'OrderedSet[_T]': 1512 return type(self)(e for e in self if e not in set_) 1513 1514class BuildDirLock: 1515 1516 def __init__(self, builddir: str): 1517 self.lockfilename = os.path.join(builddir, 'meson-private/meson.lock') 1518 1519 def __enter__(self): 1520 self.lockfile = open(self.lockfilename, 'w') 1521 try: 1522 if have_fcntl: 1523 fcntl.flock(self.lockfile, fcntl.LOCK_EX | fcntl.LOCK_NB) 1524 elif have_msvcrt: 1525 msvcrt.locking(self.lockfile.fileno(), msvcrt.LK_NBLCK, 1) 1526 except (BlockingIOError, PermissionError): 1527 self.lockfile.close() 1528 raise MesonException('Some other Meson process is already using this build directory. Exiting.') 1529 1530 def __exit__(self, *args): 1531 if have_fcntl: 1532 fcntl.flock(self.lockfile, fcntl.LOCK_UN) 1533 elif have_msvcrt: 1534 msvcrt.locking(self.lockfile.fileno(), msvcrt.LK_UNLCK, 1) 1535 self.lockfile.close() 1536 1537def relpath(path: str, start: str) -> str: 1538 # On Windows a relative path can't be evaluated for paths on two different 1539 # drives (i.e. c:\foo and f:\bar). The only thing left to do is to use the 1540 # original absolute path. 1541 try: 1542 return os.path.relpath(path, start) 1543 except (TypeError, ValueError): 1544 return path 1545 1546def path_is_in_root(path: Path, root: Path, resolve: bool = False) -> bool: 1547 # Check wheter a path is within the root directory root 1548 try: 1549 if resolve: 1550 path.resolve().relative_to(root.resolve()) 1551 else: 1552 path.relative_to(root) 1553 except ValueError: 1554 return False 1555 return True 1556 1557class LibType(Enum): 1558 1559 """Enumeration for library types.""" 1560 1561 SHARED = 0 1562 STATIC = 1 1563 PREFER_SHARED = 2 1564 PREFER_STATIC = 3 1565 1566 1567class ProgressBarFallback: # lgtm [py/iter-returns-non-self] 1568 ''' 1569 Fallback progress bar implementation when tqdm is not found 1570 1571 Since this class is not an actual iterator, but only provides a minimal 1572 fallback, it is safe to ignore the 'Iterator does not return self from 1573 __iter__ method' warning. 1574 ''' 1575 def __init__(self, iterable: T.Optional[T.Iterable[str]] = None, total: T.Optional[int] = None, 1576 bar_type: T.Optional[str] = None, desc: T.Optional[str] = None): 1577 if iterable is not None: 1578 self.iterable = iter(iterable) 1579 return 1580 self.total = total 1581 self.done = 0 1582 self.printed_dots = 0 1583 if self.total and bar_type == 'download': 1584 print('Download size:', self.total) 1585 if desc: 1586 print('{}: '.format(desc), end='') 1587 1588 # Pretend to be an iterator when called as one and don't print any 1589 # progress 1590 def __iter__(self) -> T.Iterator[str]: 1591 return self.iterable 1592 1593 def __next__(self) -> str: 1594 return next(self.iterable) 1595 1596 def print_dot(self) -> None: 1597 print('.', end='') 1598 sys.stdout.flush() 1599 self.printed_dots += 1 1600 1601 def update(self, progress: int) -> None: 1602 self.done += progress 1603 if not self.total: 1604 # Just print one dot per call if we don't have a total length 1605 self.print_dot() 1606 return 1607 ratio = int(self.done / self.total * 10) 1608 while self.printed_dots < ratio: 1609 self.print_dot() 1610 1611 def close(self) -> None: 1612 print('') 1613 1614try: 1615 from tqdm import tqdm 1616except ImportError: 1617 # ideally we would use a typing.Protocol here, but it's part of typing_extensions until 3.8 1618 ProgressBar = ProgressBarFallback # type: T.Union[T.Type[ProgressBarFallback], T.Type[ProgressBarTqdm]] 1619else: 1620 class ProgressBarTqdm(tqdm): 1621 def __init__(self, *args, bar_type: T.Optional[str] = None, **kwargs): 1622 if bar_type == 'download': 1623 kwargs.update({'unit': 'bytes', 'leave': True}) 1624 else: 1625 kwargs.update({'leave': False}) 1626 kwargs['ncols'] = 100 1627 super().__init__(*args, **kwargs) 1628 1629 ProgressBar = ProgressBarTqdm 1630 1631 1632def get_wine_shortpath(winecmd: T.List[str], wine_paths: T.Sequence[str]) -> str: 1633 """Get A short version of @wine_paths to avoid reaching WINEPATH number 1634 of char limit. 1635 """ 1636 1637 wine_paths = list(OrderedSet(wine_paths)) 1638 1639 getShortPathScript = '%s.bat' % str(uuid.uuid4()).lower()[:5] 1640 with open(getShortPathScript, mode='w') as f: 1641 f.write("@ECHO OFF\nfor %%x in (%*) do (\n echo|set /p=;%~sx\n)\n") 1642 f.flush() 1643 try: 1644 with open(os.devnull, 'w') as stderr: 1645 wine_path = subprocess.check_output( 1646 winecmd + 1647 ['cmd', '/C', getShortPathScript] + wine_paths, 1648 stderr=stderr).decode('utf-8') 1649 except subprocess.CalledProcessError as e: 1650 print("Could not get short paths: %s" % e) 1651 wine_path = ';'.join(wine_paths) 1652 finally: 1653 os.remove(getShortPathScript) 1654 if len(wine_path) > 2048: 1655 raise MesonException( 1656 'WINEPATH size {} > 2048' 1657 ' this will cause random failure.'.format( 1658 len(wine_path))) 1659 1660 return wine_path.strip(';') 1661 1662 1663def run_once(func: T.Callable[..., _T]) -> T.Callable[..., _T]: 1664 ret = [] # type: T.List[_T] 1665 1666 @wraps(func) 1667 def wrapper(*args: T.Any, **kwargs: T.Any) -> _T: 1668 if ret: 1669 return ret[0] 1670 1671 val = func(*args, **kwargs) 1672 ret.append(val) 1673 return val 1674 1675 return wrapper 1676 1677 1678class OptionProxy(T.Generic[_T]): 1679 def __init__(self, value: _T): 1680 self.value = value 1681 1682 1683class OptionOverrideProxy: 1684 1685 '''Mimic an option list but transparently override selected option 1686 values. 1687 ''' 1688 1689 # TODO: the typing here could be made more explicit using a TypeDict from 1690 # python 3.8 or typing_extensions 1691 1692 def __init__(self, overrides: T.Dict[str, T.Any], *options: 'OptionDictType'): 1693 self.overrides = overrides 1694 self.options = options 1695 1696 def __getitem__(self, option_name: str) -> T.Any: 1697 for opts in self.options: 1698 if option_name in opts: 1699 return self._get_override(option_name, opts[option_name]) 1700 raise KeyError('Option not found', option_name) 1701 1702 def _get_override(self, option_name: str, base_opt: 'UserOption[T.Any]') -> T.Union[OptionProxy[T.Any], 'UserOption[T.Any]']: 1703 if option_name in self.overrides: 1704 return OptionProxy(base_opt.validate_value(self.overrides[option_name])) 1705 return base_opt 1706 1707 def copy(self) -> T.Dict[str, T.Any]: 1708 result = {} # type: T.Dict[str, T.Any] 1709 for opts in self.options: 1710 for option_name in opts: 1711 result[option_name] = self._get_override(option_name, opts[option_name]) 1712 return result 1713