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 argparse 18import enum 19import sys 20import stat 21import time 22import abc 23import platform, subprocess, operator, os, shlex, shutil, re 24import collections 25from functools import lru_cache, wraps, total_ordering 26from itertools import tee, filterfalse 27from tempfile import TemporaryDirectory 28import typing as T 29import uuid 30import textwrap 31 32from mesonbuild import mlog 33 34if T.TYPE_CHECKING: 35 from .._typing import ImmutableListProtocol 36 from ..build import ConfigurationData 37 from ..coredata import KeyedOptionDictType, UserOption 38 from ..compilers.compilers import Compiler 39 40FileOrString = T.Union['File', str] 41 42_T = T.TypeVar('_T') 43_U = T.TypeVar('_U') 44 45__all__ = [ 46 'GIT', 47 'python_command', 48 'project_meson_versions', 49 'HoldableObject', 50 'SecondLevelHolder', 51 'File', 52 'FileMode', 53 'GitException', 54 'LibType', 55 'MachineChoice', 56 'MesonException', 57 'MesonBugException', 58 'EnvironmentException', 59 'FileOrString', 60 'GitException', 61 'OptionKey', 62 'dump_conf_header', 63 'OptionOverrideProxy', 64 'OptionProxy', 65 'OptionType', 66 'OrderedSet', 67 'PerMachine', 68 'PerMachineDefaultable', 69 'PerThreeMachine', 70 'PerThreeMachineDefaultable', 71 'ProgressBar', 72 'RealPathAction', 73 'TemporaryDirectoryWinProof', 74 'Version', 75 'check_direntry_issues', 76 'classify_unity_sources', 77 'current_vs_supports_modules', 78 'darwin_get_object_archs', 79 'default_libdir', 80 'default_libexecdir', 81 'default_prefix', 82 'detect_subprojects', 83 'detect_vcs', 84 'do_conf_file', 85 'do_conf_str', 86 'do_define', 87 'do_replacement', 88 'exe_exists', 89 'expand_arguments', 90 'extract_as_list', 91 'get_compiler_for_source', 92 'get_filenames_templates_dict', 93 'get_library_dirs', 94 'get_variable_regex', 95 'get_wine_shortpath', 96 'git', 97 'has_path_sep', 98 'is_aix', 99 'is_android', 100 'is_ascii_string', 101 'is_cygwin', 102 'is_debianlike', 103 'is_dragonflybsd', 104 'is_freebsd', 105 'is_haiku', 106 'is_hurd', 107 'is_irix', 108 'is_linux', 109 'is_netbsd', 110 'is_openbsd', 111 'is_osx', 112 'is_qnx', 113 'is_sunos', 114 'is_windows', 115 'is_wsl', 116 'iter_regexin_iter', 117 'join_args', 118 'listify', 119 'partition', 120 'path_is_in_root', 121 'Popen_safe', 122 'quiet_git', 123 'quote_arg', 124 'relative_to_if_possible', 125 'relpath', 126 'replace_if_different', 127 'run_once', 128 'get_meson_command', 129 'set_meson_command', 130 'split_args', 131 'stringlistify', 132 'substitute_values', 133 'substring_is_in_list', 134 'typeslistify', 135 'verbose_git', 136 'version_compare', 137 'version_compare_condition_with_min', 138 'version_compare_many', 139 'search_version', 140 'windows_proof_rm', 141 'windows_proof_rmtree', 142] 143 144 145# TODO: this is such a hack, this really should be either in coredata or in the 146# interpreter 147# {subproject: project_meson_version} 148project_meson_versions = collections.defaultdict(str) # type: T.DefaultDict[str, str] 149 150 151from glob import glob 152 153if os.path.basename(sys.executable) == 'meson.exe': 154 # In Windows and using the MSI installed executable. 155 python_command = [sys.executable, 'runpython'] 156else: 157 python_command = [sys.executable] 158_meson_command = None 159 160class MesonException(Exception): 161 '''Exceptions thrown by Meson''' 162 163 def __init__(self, *args: object, file: T.Optional[str] = None, 164 lineno: T.Optional[int] = None, colno: T.Optional[int] = None): 165 super().__init__(*args) 166 self.file = file 167 self.lineno = lineno 168 self.colno = colno 169 170 171class MesonBugException(MesonException): 172 '''Exceptions thrown when there is a clear Meson bug that should be reported''' 173 174 def __init__(self, msg: str, file: T.Optional[str] = None, 175 lineno: T.Optional[int] = None, colno: T.Optional[int] = None): 176 super().__init__(msg + '\n\n This is a Meson bug and should be reported!', 177 file=file, lineno=lineno, colno=colno) 178 179class EnvironmentException(MesonException): 180 '''Exceptions thrown while processing and creating the build environment''' 181 182class GitException(MesonException): 183 def __init__(self, msg: str, output: T.Optional[str] = None): 184 super().__init__(msg) 185 self.output = output.strip() if output else '' 186 187GIT = shutil.which('git') 188def git(cmd: T.List[str], workingdir: str, check: bool = False, **kwargs: T.Any) -> T.Tuple[subprocess.Popen, str, str]: 189 cmd = [GIT] + cmd 190 p, o, e = Popen_safe(cmd, cwd=workingdir, **kwargs) 191 if check and p.returncode != 0: 192 raise GitException('Git command failed: ' + str(cmd), e) 193 return p, o, e 194 195def quiet_git(cmd: T.List[str], workingdir: str, check: bool = False) -> T.Tuple[bool, str]: 196 if not GIT: 197 m = 'Git program not found.' 198 if check: 199 raise GitException(m) 200 return False, m 201 p, o, e = git(cmd, workingdir, check) 202 if p.returncode != 0: 203 return False, e 204 return True, o 205 206def verbose_git(cmd: T.List[str], workingdir: str, check: bool = False) -> bool: 207 if not GIT: 208 m = 'Git program not found.' 209 if check: 210 raise GitException(m) 211 return False 212 p, _, _ = git(cmd, workingdir, check, stdout=None, stderr=None) 213 return p.returncode == 0 214 215def set_meson_command(mainfile: str) -> None: 216 global python_command 217 global _meson_command 218 # On UNIX-like systems `meson` is a Python script 219 # On Windows `meson` and `meson.exe` are wrapper exes 220 if not mainfile.endswith('.py'): 221 _meson_command = [mainfile] 222 elif os.path.isabs(mainfile) and mainfile.endswith('mesonmain.py'): 223 # Can't actually run meson with an absolute path to mesonmain.py, it must be run as -m mesonbuild.mesonmain 224 _meson_command = python_command + ['-m', 'mesonbuild.mesonmain'] 225 else: 226 # Either run uninstalled, or full path to meson-script.py 227 _meson_command = python_command + [mainfile] 228 # We print this value for unit tests. 229 if 'MESON_COMMAND_TESTS' in os.environ: 230 mlog.log(f'meson_command is {_meson_command!r}') 231 232 233def get_meson_command() -> T.Optional[T.List[str]]: 234 return _meson_command 235 236 237def is_ascii_string(astring: T.Union[str, bytes]) -> bool: 238 try: 239 if isinstance(astring, str): 240 astring.encode('ascii') 241 elif isinstance(astring, bytes): 242 astring.decode('ascii') 243 except UnicodeDecodeError: 244 return False 245 return True 246 247 248def check_direntry_issues(direntry_array: T.Union[T.List[T.Union[str, bytes]], str, bytes]) -> None: 249 import locale 250 # Warn if the locale is not UTF-8. This can cause various unfixable issues 251 # such as os.stat not being able to decode filenames with unicode in them. 252 # There is no way to reset both the preferred encoding and the filesystem 253 # encoding, so we can just warn about it. 254 e = locale.getpreferredencoding() 255 if e.upper() != 'UTF-8' and not is_windows(): 256 if not isinstance(direntry_array, list): 257 direntry_array = [direntry_array] 258 for de in direntry_array: 259 if is_ascii_string(de): 260 continue 261 mlog.warning(textwrap.dedent(f''' 262 You are using {e!r} which is not a Unicode-compatible 263 locale but you are trying to access a file system entry called {de!r} which is 264 not pure ASCII. This may cause problems. 265 '''), file=sys.stderr) 266 267class HoldableObject(metaclass=abc.ABCMeta): 268 ''' Dummy base class for all objects that can be 269 held by an interpreter.baseobjects.ObjectHolder ''' 270 271class SecondLevelHolder(HoldableObject, metaclass=abc.ABCMeta): 272 ''' A second level object holder. The primary purpose 273 of such objects is to hold multiple objects with one 274 default option. ''' 275 276 @abc.abstractmethod 277 def get_default_object(self) -> HoldableObject: ... 278 279class FileMode: 280 # The first triad is for owner permissions, the second for group permissions, 281 # and the third for others (everyone else). 282 # For the 1st character: 283 # 'r' means can read 284 # '-' means not allowed 285 # For the 2nd character: 286 # 'w' means can write 287 # '-' means not allowed 288 # For the 3rd character: 289 # 'x' means can execute 290 # 's' means can execute and setuid/setgid is set (owner/group triads only) 291 # 'S' means cannot execute and setuid/setgid is set (owner/group triads only) 292 # 't' means can execute and sticky bit is set ("others" triads only) 293 # 'T' means cannot execute and sticky bit is set ("others" triads only) 294 # '-' means none of these are allowed 295 # 296 # The meanings of 'rwx' perms is not obvious for directories; see: 297 # https://www.hackinglinuxexposed.com/articles/20030424.html 298 # 299 # For information on this notation such as setuid/setgid/sticky bits, see: 300 # https://en.wikipedia.org/wiki/File_system_permissions#Symbolic_notation 301 symbolic_perms_regex = re.compile('[r-][w-][xsS-]' # Owner perms 302 '[r-][w-][xsS-]' # Group perms 303 '[r-][w-][xtT-]') # Others perms 304 305 def __init__(self, perms: T.Optional[str] = None, owner: T.Union[str, int, None] = None, 306 group: T.Union[str, int, None] = None): 307 self.perms_s = perms 308 self.perms = self.perms_s_to_bits(perms) 309 self.owner = owner 310 self.group = group 311 312 def __repr__(self) -> str: 313 ret = '<FileMode: {!r} owner={} group={}' 314 return ret.format(self.perms_s, self.owner, self.group) 315 316 @classmethod 317 def perms_s_to_bits(cls, perms_s: T.Optional[str]) -> int: 318 ''' 319 Does the opposite of stat.filemode(), converts strings of the form 320 'rwxr-xr-x' to st_mode enums which can be passed to os.chmod() 321 ''' 322 if perms_s is None: 323 # No perms specified, we will not touch the permissions 324 return -1 325 eg = 'rwxr-xr-x' 326 if not isinstance(perms_s, str): 327 raise MesonException(f'Install perms must be a string. For example, {eg!r}') 328 if len(perms_s) != 9 or not cls.symbolic_perms_regex.match(perms_s): 329 raise MesonException(f'File perms {perms_s!r} must be exactly 9 chars. For example, {eg!r}') 330 perms = 0 331 # Owner perms 332 if perms_s[0] == 'r': 333 perms |= stat.S_IRUSR 334 if perms_s[1] == 'w': 335 perms |= stat.S_IWUSR 336 if perms_s[2] == 'x': 337 perms |= stat.S_IXUSR 338 elif perms_s[2] == 'S': 339 perms |= stat.S_ISUID 340 elif perms_s[2] == 's': 341 perms |= stat.S_IXUSR 342 perms |= stat.S_ISUID 343 # Group perms 344 if perms_s[3] == 'r': 345 perms |= stat.S_IRGRP 346 if perms_s[4] == 'w': 347 perms |= stat.S_IWGRP 348 if perms_s[5] == 'x': 349 perms |= stat.S_IXGRP 350 elif perms_s[5] == 'S': 351 perms |= stat.S_ISGID 352 elif perms_s[5] == 's': 353 perms |= stat.S_IXGRP 354 perms |= stat.S_ISGID 355 # Others perms 356 if perms_s[6] == 'r': 357 perms |= stat.S_IROTH 358 if perms_s[7] == 'w': 359 perms |= stat.S_IWOTH 360 if perms_s[8] == 'x': 361 perms |= stat.S_IXOTH 362 elif perms_s[8] == 'T': 363 perms |= stat.S_ISVTX 364 elif perms_s[8] == 't': 365 perms |= stat.S_IXOTH 366 perms |= stat.S_ISVTX 367 return perms 368 369dot_C_dot_H_warning = """You are using .C or .H files in your project. This is deprecated. 370 Currently, Meson treats this as C++ code, but they 371 used to be treated as C code. 372 Note that the situation is a bit more complex if you are using the 373 Visual Studio compiler, as it treats .C files as C code, unless you add 374 the /TP compiler flag, but this is unreliable. 375 See https://github.com/mesonbuild/meson/pull/8747 for the discussions.""" 376class File(HoldableObject): 377 def __init__(self, is_built: bool, subdir: str, fname: str): 378 if fname.endswith(".C") or fname.endswith(".H"): 379 mlog.warning(dot_C_dot_H_warning, once=True) 380 self.is_built = is_built 381 self.subdir = subdir 382 self.fname = fname 383 self.hash = hash((is_built, subdir, fname)) 384 385 def __str__(self) -> str: 386 return self.relative_name() 387 388 def __repr__(self) -> str: 389 ret = '<File: {0}' 390 if not self.is_built: 391 ret += ' (not built)' 392 ret += '>' 393 return ret.format(self.relative_name()) 394 395 @staticmethod 396 @lru_cache(maxsize=None) 397 def from_source_file(source_root: str, subdir: str, fname: str) -> 'File': 398 if not os.path.isfile(os.path.join(source_root, subdir, fname)): 399 raise MesonException('File %s does not exist.' % fname) 400 return File(False, subdir, fname) 401 402 @staticmethod 403 def from_built_file(subdir: str, fname: str) -> 'File': 404 return File(True, subdir, fname) 405 406 @staticmethod 407 def from_absolute_file(fname: str) -> 'File': 408 return File(False, '', fname) 409 410 @lru_cache(maxsize=None) 411 def rel_to_builddir(self, build_to_src: str) -> str: 412 if self.is_built: 413 return self.relative_name() 414 else: 415 return os.path.join(build_to_src, self.subdir, self.fname) 416 417 @lru_cache(maxsize=None) 418 def absolute_path(self, srcdir: str, builddir: str) -> str: 419 absdir = srcdir 420 if self.is_built: 421 absdir = builddir 422 return os.path.join(absdir, self.relative_name()) 423 424 def endswith(self, ending: str) -> bool: 425 return self.fname.endswith(ending) 426 427 def split(self, s: str, maxsplit: int = -1) -> T.List[str]: 428 return self.fname.split(s, maxsplit=maxsplit) 429 430 def rsplit(self, s: str, maxsplit: int = -1) -> T.List[str]: 431 return self.fname.rsplit(s, maxsplit=maxsplit) 432 433 def __eq__(self, other: object) -> bool: 434 if not isinstance(other, File): 435 return NotImplemented 436 if self.hash != other.hash: 437 return False 438 return (self.fname, self.subdir, self.is_built) == (other.fname, other.subdir, other.is_built) 439 440 def __hash__(self) -> int: 441 return self.hash 442 443 @lru_cache(maxsize=None) 444 def relative_name(self) -> str: 445 return os.path.join(self.subdir, self.fname) 446 447 448def get_compiler_for_source(compilers: T.Iterable['Compiler'], src: 'FileOrString') -> 'Compiler': 449 """Given a set of compilers and a source, find the compiler for that source type.""" 450 for comp in compilers: 451 if comp.can_compile(src): 452 return comp 453 raise MesonException(f'No specified compiler can handle file {src!s}') 454 455 456def classify_unity_sources(compilers: T.Iterable['Compiler'], sources: T.Sequence['FileOrString']) -> T.Dict['Compiler', T.List['FileOrString']]: 457 compsrclist: T.Dict['Compiler', T.List['FileOrString']] = {} 458 for src in sources: 459 comp = get_compiler_for_source(compilers, src) 460 if comp not in compsrclist: 461 compsrclist[comp] = [src] 462 else: 463 compsrclist[comp].append(src) 464 return compsrclist 465 466 467class MachineChoice(enum.IntEnum): 468 469 """Enum class representing one of the two abstract machine names used in 470 most places: the build, and host, machines. 471 """ 472 473 BUILD = 0 474 HOST = 1 475 476 def get_lower_case_name(self) -> str: 477 return PerMachine('build', 'host')[self] 478 479 def get_prefix(self) -> str: 480 return PerMachine('build.', '')[self] 481 482 483class PerMachine(T.Generic[_T]): 484 def __init__(self, build: _T, host: _T) -> None: 485 self.build = build 486 self.host = host 487 488 def __getitem__(self, machine: MachineChoice) -> _T: 489 return { 490 MachineChoice.BUILD: self.build, 491 MachineChoice.HOST: self.host, 492 }[machine] 493 494 def __setitem__(self, machine: MachineChoice, val: _T) -> None: 495 setattr(self, machine.get_lower_case_name(), val) 496 497 def miss_defaulting(self) -> "PerMachineDefaultable[T.Optional[_T]]": 498 """Unset definition duplicated from their previous to None 499 500 This is the inverse of ''default_missing''. By removing defaulted 501 machines, we can elaborate the original and then redefault them and thus 502 avoid repeating the elaboration explicitly. 503 """ 504 unfreeze = PerMachineDefaultable() # type: PerMachineDefaultable[T.Optional[_T]] 505 unfreeze.build = self.build 506 unfreeze.host = self.host 507 if unfreeze.host == unfreeze.build: 508 unfreeze.host = None 509 return unfreeze 510 511 def __repr__(self) -> str: 512 return f'PerMachine({self.build!r}, {self.host!r})' 513 514 515class PerThreeMachine(PerMachine[_T]): 516 """Like `PerMachine` but includes `target` too. 517 518 It turns out just one thing do we need track the target machine. There's no 519 need to computer the `target` field so we don't bother overriding the 520 `__getitem__`/`__setitem__` methods. 521 """ 522 def __init__(self, build: _T, host: _T, target: _T) -> None: 523 super().__init__(build, host) 524 self.target = target 525 526 def miss_defaulting(self) -> "PerThreeMachineDefaultable[T.Optional[_T]]": 527 """Unset definition duplicated from their previous to None 528 529 This is the inverse of ''default_missing''. By removing defaulted 530 machines, we can elaborate the original and then redefault them and thus 531 avoid repeating the elaboration explicitly. 532 """ 533 unfreeze = PerThreeMachineDefaultable() # type: PerThreeMachineDefaultable[T.Optional[_T]] 534 unfreeze.build = self.build 535 unfreeze.host = self.host 536 unfreeze.target = self.target 537 if unfreeze.target == unfreeze.host: 538 unfreeze.target = None 539 if unfreeze.host == unfreeze.build: 540 unfreeze.host = None 541 return unfreeze 542 543 def matches_build_machine(self, machine: MachineChoice) -> bool: 544 return self.build == self[machine] 545 546 def __repr__(self) -> str: 547 return f'PerThreeMachine({self.build!r}, {self.host!r}, {self.target!r})' 548 549 550class PerMachineDefaultable(PerMachine[T.Optional[_T]]): 551 """Extends `PerMachine` with the ability to default from `None`s. 552 """ 553 def __init__(self, build: T.Optional[_T] = None, host: T.Optional[_T] = None) -> None: 554 super().__init__(build, host) 555 556 def default_missing(self) -> "PerMachine[_T]": 557 """Default host to build 558 559 This allows just specifying nothing in the native case, and just host in the 560 cross non-compiler case. 561 """ 562 freeze = PerMachine(self.build, self.host) 563 if freeze.host is None: 564 freeze.host = freeze.build 565 return freeze 566 567 def __repr__(self) -> str: 568 return f'PerMachineDefaultable({self.build!r}, {self.host!r})' 569 570 @classmethod 571 def default(cls, is_cross: bool, build: _T, host: _T) -> PerMachine[_T]: 572 """Easy way to get a defaulted value 573 574 This allows simplifying the case where you can control whether host and 575 build are separate or not with a boolean. If the is_cross value is set 576 to true then the optional host value will be used, otherwise the host 577 will be set to the build value. 578 """ 579 m = cls(build) 580 if is_cross: 581 m.host = host 582 return m.default_missing() 583 584 585 586class PerThreeMachineDefaultable(PerMachineDefaultable, PerThreeMachine[T.Optional[_T]]): 587 """Extends `PerThreeMachine` with the ability to default from `None`s. 588 """ 589 def __init__(self) -> None: 590 PerThreeMachine.__init__(self, None, None, None) 591 592 def default_missing(self) -> "PerThreeMachine[T.Optional[_T]]": 593 """Default host to build and target to host. 594 595 This allows just specifying nothing in the native case, just host in the 596 cross non-compiler case, and just target in the native-built 597 cross-compiler case. 598 """ 599 freeze = PerThreeMachine(self.build, self.host, self.target) 600 if freeze.host is None: 601 freeze.host = freeze.build 602 if freeze.target is None: 603 freeze.target = freeze.host 604 return freeze 605 606 def __repr__(self) -> str: 607 return f'PerThreeMachineDefaultable({self.build!r}, {self.host!r}, {self.target!r})' 608 609 610def is_sunos() -> bool: 611 return platform.system().lower() == 'sunos' 612 613 614def is_osx() -> bool: 615 return platform.system().lower() == 'darwin' 616 617 618def is_linux() -> bool: 619 return platform.system().lower() == 'linux' 620 621 622def is_android() -> bool: 623 return platform.system().lower() == 'android' 624 625 626def is_haiku() -> bool: 627 return platform.system().lower() == 'haiku' 628 629 630def is_openbsd() -> bool: 631 return platform.system().lower() == 'openbsd' 632 633 634def is_windows() -> bool: 635 platname = platform.system().lower() 636 return platname == 'windows' 637 638def is_wsl() -> bool: 639 return is_linux() and 'microsoft' in platform.release().lower() 640 641def is_cygwin() -> bool: 642 return sys.platform == 'cygwin' 643 644 645def is_debianlike() -> bool: 646 return os.path.isfile('/etc/debian_version') 647 648 649def is_dragonflybsd() -> bool: 650 return platform.system().lower() == 'dragonfly' 651 652 653def is_netbsd() -> bool: 654 return platform.system().lower() == 'netbsd' 655 656 657def is_freebsd() -> bool: 658 return platform.system().lower() == 'freebsd' 659 660def is_irix() -> bool: 661 return platform.system().startswith('irix') 662 663def is_hurd() -> bool: 664 return platform.system().lower() == 'gnu' 665 666def is_qnx() -> bool: 667 return platform.system().lower() == 'qnx' 668 669def is_aix() -> bool: 670 return platform.system().lower() == 'aix' 671 672def exe_exists(arglist: T.List[str]) -> bool: 673 try: 674 if subprocess.run(arglist, timeout=10).returncode == 0: 675 return True 676 except (FileNotFoundError, subprocess.TimeoutExpired): 677 pass 678 return False 679 680 681@lru_cache(maxsize=None) 682def darwin_get_object_archs(objpath: str) -> 'ImmutableListProtocol[str]': 683 ''' 684 For a specific object (executable, static library, dylib, etc), run `lipo` 685 to fetch the list of archs supported by it. Supports both thin objects and 686 'fat' objects. 687 ''' 688 _, stdo, stderr = Popen_safe(['lipo', '-info', objpath]) 689 if not stdo: 690 mlog.debug(f'lipo {objpath}: {stderr}') 691 return None 692 stdo = stdo.rsplit(': ', 1)[1] 693 # Convert from lipo-style archs to meson-style CPUs 694 stdo = stdo.replace('i386', 'x86') 695 stdo = stdo.replace('arm64', 'aarch64') 696 # Add generic name for armv7 and armv7s 697 if 'armv7' in stdo: 698 stdo += ' arm' 699 return stdo.split() 700 701 702def detect_vcs(source_dir: T.Union[str, Path]) -> T.Optional[T.Dict[str, str]]: 703 vcs_systems = [ 704 dict(name = 'git', cmd = 'git', repo_dir = '.git', get_rev = 'git describe --dirty=+', rev_regex = '(.*)', dep = '.git/logs/HEAD'), 705 dict(name = 'mercurial', cmd = 'hg', repo_dir = '.hg', get_rev = 'hg id -i', rev_regex = '(.*)', dep = '.hg/dirstate'), 706 dict(name = 'subversion', cmd = 'svn', repo_dir = '.svn', get_rev = 'svn info', rev_regex = 'Revision: (.*)', dep = '.svn/wc.db'), 707 dict(name = 'bazaar', cmd = 'bzr', repo_dir = '.bzr', get_rev = 'bzr revno', rev_regex = '(.*)', dep = '.bzr'), 708 ] 709 if isinstance(source_dir, str): 710 source_dir = Path(source_dir) 711 712 parent_paths_and_self = collections.deque(source_dir.parents) 713 # Prepend the source directory to the front so we can check it; 714 # source_dir.parents doesn't include source_dir 715 parent_paths_and_self.appendleft(source_dir) 716 for curdir in parent_paths_and_self: 717 for vcs in vcs_systems: 718 if Path.is_dir(curdir.joinpath(vcs['repo_dir'])) and shutil.which(vcs['cmd']): 719 vcs['wc_dir'] = str(curdir) 720 return vcs 721 return None 722 723def current_vs_supports_modules() -> bool: 724 vsver = os.environ.get('VSCMD_VER', '') 725 nums = vsver.split('.', 2) 726 major = int(nums[0]) 727 if major >= 17: 728 return True 729 if major == 16 and int(nums[1]) >= 10: 730 return True 731 return vsver.startswith('16.9.0') and '-pre.' in vsver 732 733# a helper class which implements the same version ordering as RPM 734class Version: 735 def __init__(self, s: str) -> None: 736 self._s = s 737 738 # split into numeric, alphabetic and non-alphanumeric sequences 739 sequences1 = re.finditer(r'(\d+|[a-zA-Z]+|[^a-zA-Z\d]+)', s) 740 741 # non-alphanumeric separators are discarded 742 sequences2 = [m for m in sequences1 if not re.match(r'[^a-zA-Z\d]+', m.group(1))] 743 744 # numeric sequences are converted from strings to ints 745 sequences3 = [int(m.group(1)) if m.group(1).isdigit() else m.group(1) for m in sequences2] 746 747 self._v = sequences3 748 749 def __str__(self) -> str: 750 return '{} (V={})'.format(self._s, str(self._v)) 751 752 def __repr__(self) -> str: 753 return f'<Version: {self._s}>' 754 755 def __lt__(self, other: object) -> bool: 756 if isinstance(other, Version): 757 return self.__cmp(other, operator.lt) 758 return NotImplemented 759 760 def __gt__(self, other: object) -> bool: 761 if isinstance(other, Version): 762 return self.__cmp(other, operator.gt) 763 return NotImplemented 764 765 def __le__(self, other: object) -> bool: 766 if isinstance(other, Version): 767 return self.__cmp(other, operator.le) 768 return NotImplemented 769 770 def __ge__(self, other: object) -> bool: 771 if isinstance(other, Version): 772 return self.__cmp(other, operator.ge) 773 return NotImplemented 774 775 def __eq__(self, other: object) -> bool: 776 if isinstance(other, Version): 777 return self._v == other._v 778 return NotImplemented 779 780 def __ne__(self, other: object) -> bool: 781 if isinstance(other, Version): 782 return self._v != other._v 783 return NotImplemented 784 785 def __cmp(self, other: 'Version', comparator: T.Callable[[T.Any, T.Any], bool]) -> bool: 786 # compare each sequence in order 787 for ours, theirs in zip(self._v, other._v): 788 # sort a non-digit sequence before a digit sequence 789 ours_is_int = isinstance(ours, int) 790 theirs_is_int = isinstance(theirs, int) 791 if ours_is_int != theirs_is_int: 792 return comparator(ours_is_int, theirs_is_int) 793 794 if ours != theirs: 795 return comparator(ours, theirs) 796 797 # if equal length, all components have matched, so equal 798 # otherwise, the version with a suffix remaining is greater 799 return comparator(len(self._v), len(other._v)) 800 801 802def _version_extract_cmpop(vstr2: str) -> T.Tuple[T.Callable[[T.Any, T.Any], bool], str]: 803 if vstr2.startswith('>='): 804 cmpop = operator.ge 805 vstr2 = vstr2[2:] 806 elif vstr2.startswith('<='): 807 cmpop = operator.le 808 vstr2 = vstr2[2:] 809 elif vstr2.startswith('!='): 810 cmpop = operator.ne 811 vstr2 = vstr2[2:] 812 elif vstr2.startswith('=='): 813 cmpop = operator.eq 814 vstr2 = vstr2[2:] 815 elif vstr2.startswith('='): 816 cmpop = operator.eq 817 vstr2 = vstr2[1:] 818 elif vstr2.startswith('>'): 819 cmpop = operator.gt 820 vstr2 = vstr2[1:] 821 elif vstr2.startswith('<'): 822 cmpop = operator.lt 823 vstr2 = vstr2[1:] 824 else: 825 cmpop = operator.eq 826 827 return (cmpop, vstr2) 828 829 830def version_compare(vstr1: str, vstr2: str) -> bool: 831 (cmpop, vstr2) = _version_extract_cmpop(vstr2) 832 return cmpop(Version(vstr1), Version(vstr2)) 833 834 835def version_compare_many(vstr1: str, conditions: T.Union[str, T.Iterable[str]]) -> T.Tuple[bool, T.List[str], T.List[str]]: 836 if isinstance(conditions, str): 837 conditions = [conditions] 838 found = [] 839 not_found = [] 840 for req in conditions: 841 if not version_compare(vstr1, req): 842 not_found.append(req) 843 else: 844 found.append(req) 845 return not_found == [], not_found, found 846 847 848# determine if the minimum version satisfying the condition |condition| exceeds 849# the minimum version for a feature |minimum| 850def version_compare_condition_with_min(condition: str, minimum: str) -> bool: 851 if condition.startswith('>='): 852 cmpop = operator.le 853 condition = condition[2:] 854 elif condition.startswith('<='): 855 return False 856 elif condition.startswith('!='): 857 return False 858 elif condition.startswith('=='): 859 cmpop = operator.le 860 condition = condition[2:] 861 elif condition.startswith('='): 862 cmpop = operator.le 863 condition = condition[1:] 864 elif condition.startswith('>'): 865 cmpop = operator.lt 866 condition = condition[1:] 867 elif condition.startswith('<'): 868 return False 869 else: 870 cmpop = operator.le 871 872 # Declaring a project(meson_version: '>=0.46') and then using features in 873 # 0.46.0 is valid, because (knowing the meson versioning scheme) '0.46.0' is 874 # the lowest version which satisfies the constraint '>=0.46'. 875 # 876 # But this will fail here, because the minimum version required by the 877 # version constraint ('0.46') is strictly less (in our version comparison) 878 # than the minimum version needed for the feature ('0.46.0'). 879 # 880 # Map versions in the constraint of the form '0.46' to '0.46.0', to embed 881 # this knowledge of the meson versioning scheme. 882 condition = condition.strip() 883 if re.match(r'^\d+.\d+$', condition): 884 condition += '.0' 885 886 return T.cast(bool, cmpop(Version(minimum), Version(condition))) 887 888def search_version(text: str) -> str: 889 # Usually of the type 4.1.4 but compiler output may contain 890 # stuff like this: 891 # (Sourcery CodeBench Lite 2014.05-29) 4.8.3 20140320 (prerelease) 892 # Limiting major version number to two digits seems to work 893 # thus far. When we get to GCC 100, this will break, but 894 # if we are still relevant when that happens, it can be 895 # considered an achievement in itself. 896 # 897 # This regex is reaching magic levels. If it ever needs 898 # to be updated, do not complexify but convert to something 899 # saner instead. 900 # We'll demystify it a bit with a verbose definition. 901 version_regex = re.compile(r""" 902 (?<! # Zero-width negative lookbehind assertion 903 ( 904 \d # One digit 905 | \. # Or one period 906 ) # One occurrence 907 ) 908 # Following pattern must not follow a digit or period 909 ( 910 \d{1,2} # One or two digits 911 ( 912 \.\d+ # Period and one or more digits 913 )+ # One or more occurrences 914 ( 915 -[a-zA-Z0-9]+ # Hyphen and one or more alphanumeric 916 )? # Zero or one occurrence 917 ) # One occurrence 918 """, re.VERBOSE) 919 match = version_regex.search(text) 920 if match: 921 return match.group(0) 922 923 # try a simpler regex that has like "blah 2020.01.100 foo" or "blah 2020.01 foo" 924 version_regex = re.compile(r"(\d{1,4}\.\d{1,4}\.?\d{0,4})") 925 match = version_regex.search(text) 926 if match: 927 return match.group(0) 928 929 return 'unknown version' 930 931 932def default_libdir() -> str: 933 if is_debianlike(): 934 try: 935 pc = subprocess.Popen(['dpkg-architecture', '-qDEB_HOST_MULTIARCH'], 936 stdout=subprocess.PIPE, 937 stderr=subprocess.DEVNULL) 938 (stdo, _) = pc.communicate() 939 if pc.returncode == 0: 940 archpath = stdo.decode().strip() 941 return 'lib/' + archpath 942 except Exception: 943 pass 944 if is_freebsd() or is_irix(): 945 return 'lib' 946 if os.path.isdir('/usr/lib64') and not os.path.islink('/usr/lib64'): 947 return 'lib64' 948 return 'lib' 949 950 951def default_libexecdir() -> str: 952 # There is no way to auto-detect this, so it must be set at build time 953 return 'libexec' 954 955 956def default_prefix() -> str: 957 return 'c:/' if is_windows() else '/usr/local' 958 959 960def get_library_dirs() -> T.List[str]: 961 if is_windows(): 962 return ['C:/mingw/lib'] # TODO: get programmatically 963 if is_osx(): 964 return ['/usr/lib'] # TODO: get programmatically 965 # The following is probably Debian/Ubuntu specific. 966 # /usr/local/lib is first because it contains stuff 967 # installed by the sysadmin and is probably more up-to-date 968 # than /usr/lib. If you feel that this search order is 969 # problematic, please raise the issue on the mailing list. 970 unixdirs = ['/usr/local/lib', '/usr/lib', '/lib'] 971 972 if is_freebsd(): 973 return unixdirs 974 # FIXME: this needs to be further genericized for aarch64 etc. 975 machine = platform.machine() 976 if machine in ('i386', 'i486', 'i586', 'i686'): 977 plat = 'i386' 978 elif machine.startswith('arm'): 979 plat = 'arm' 980 else: 981 plat = '' 982 983 # Solaris puts 32-bit libraries in the main /lib & /usr/lib directories 984 # and 64-bit libraries in platform specific subdirectories. 985 if is_sunos(): 986 if machine == 'i86pc': 987 plat = 'amd64' 988 elif machine.startswith('sun4'): 989 plat = 'sparcv9' 990 991 usr_platdir = Path('/usr/lib/') / plat 992 if usr_platdir.is_dir(): 993 unixdirs += [str(x) for x in (usr_platdir).iterdir() if x.is_dir()] 994 if os.path.exists('/usr/lib64'): 995 unixdirs.append('/usr/lib64') 996 997 lib_platdir = Path('/lib/') / plat 998 if lib_platdir.is_dir(): 999 unixdirs += [str(x) for x in (lib_platdir).iterdir() if x.is_dir()] 1000 if os.path.exists('/lib64'): 1001 unixdirs.append('/lib64') 1002 1003 return unixdirs 1004 1005 1006def has_path_sep(name: str, sep: str = '/\\') -> bool: 1007 'Checks if any of the specified @sep path separators are in @name' 1008 for each in sep: 1009 if each in name: 1010 return True 1011 return False 1012 1013 1014if is_windows(): 1015 # shlex.split is not suitable for splitting command line on Window (https://bugs.python.org/issue1724822); 1016 # shlex.quote is similarly problematic. Below are "proper" implementations of these functions according to 1017 # https://docs.microsoft.com/en-us/cpp/c-language/parsing-c-command-line-arguments and 1018 # https://blogs.msdn.microsoft.com/twistylittlepassagesallalike/2011/04/23/everyone-quotes-command-line-arguments-the-wrong-way/ 1019 1020 _whitespace = ' \t\n\r' 1021 _find_unsafe_char = re.compile(fr'[{_whitespace}"]').search 1022 1023 def quote_arg(arg: str) -> str: 1024 if arg and not _find_unsafe_char(arg): 1025 return arg 1026 1027 result = '"' 1028 num_backslashes = 0 1029 for c in arg: 1030 if c == '\\': 1031 num_backslashes += 1 1032 else: 1033 if c == '"': 1034 # Escape all backslashes and the following double quotation mark 1035 num_backslashes = num_backslashes * 2 + 1 1036 1037 result += num_backslashes * '\\' + c 1038 num_backslashes = 0 1039 1040 # Escape all backslashes, but let the terminating double quotation 1041 # mark we add below be interpreted as a metacharacter 1042 result += (num_backslashes * 2) * '\\' + '"' 1043 return result 1044 1045 def split_args(cmd: str) -> T.List[str]: 1046 result = [] 1047 arg = '' 1048 num_backslashes = 0 1049 num_quotes = 0 1050 in_quotes = False 1051 for c in cmd: 1052 if c == '\\': 1053 num_backslashes += 1 1054 else: 1055 if c == '"' and not num_backslashes % 2: 1056 # unescaped quote, eat it 1057 arg += (num_backslashes // 2) * '\\' 1058 num_quotes += 1 1059 in_quotes = not in_quotes 1060 elif c in _whitespace and not in_quotes: 1061 if arg or num_quotes: 1062 # reached the end of the argument 1063 result.append(arg) 1064 arg = '' 1065 num_quotes = 0 1066 else: 1067 if c == '"': 1068 # escaped quote 1069 num_backslashes = (num_backslashes - 1) // 2 1070 1071 arg += num_backslashes * '\\' + c 1072 1073 num_backslashes = 0 1074 1075 if arg or num_quotes: 1076 result.append(arg) 1077 1078 return result 1079else: 1080 def quote_arg(arg: str) -> str: 1081 return shlex.quote(arg) 1082 1083 def split_args(cmd: str) -> T.List[str]: 1084 return shlex.split(cmd) 1085 1086 1087def join_args(args: T.Iterable[str]) -> str: 1088 return ' '.join([quote_arg(x) for x in args]) 1089 1090 1091def do_replacement(regex: T.Pattern[str], line: str, variable_format: str, 1092 confdata: 'ConfigurationData') -> T.Tuple[str, T.Set[str]]: 1093 missing_variables = set() # type: T.Set[str] 1094 if variable_format == 'cmake': 1095 start_tag = '${' 1096 backslash_tag = '\\${' 1097 else: 1098 assert variable_format in ['meson', 'cmake@'] 1099 start_tag = '@' 1100 backslash_tag = '\\@' 1101 1102 def variable_replace(match: T.Match[str]) -> str: 1103 # Pairs of escape characters before '@' or '\@' 1104 if match.group(0).endswith('\\'): 1105 num_escapes = match.end(0) - match.start(0) 1106 return '\\' * (num_escapes // 2) 1107 # Single escape character and '@' 1108 elif match.group(0) == backslash_tag: 1109 return start_tag 1110 # Template variable to be replaced 1111 else: 1112 varname = match.group(1) 1113 var_str = '' 1114 if varname in confdata: 1115 (var, desc) = confdata.get(varname) 1116 if isinstance(var, str): 1117 var_str = var 1118 elif isinstance(var, int): 1119 var_str = str(var) 1120 else: 1121 msg = f'Tried to replace variable {varname!r} value with ' \ 1122 f'something other than a string or int: {var!r}' 1123 raise MesonException(msg) 1124 else: 1125 missing_variables.add(varname) 1126 return var_str 1127 return re.sub(regex, variable_replace, line), missing_variables 1128 1129def do_define(regex: T.Pattern[str], line: str, confdata: 'ConfigurationData', variable_format: str) -> str: 1130 def get_cmake_define(line: str, confdata: 'ConfigurationData') -> str: 1131 arr = line.split() 1132 define_value=[] 1133 for token in arr[2:]: 1134 try: 1135 (v, desc) = confdata.get(token) 1136 define_value += [str(v)] 1137 except KeyError: 1138 define_value += [token] 1139 return ' '.join(define_value) 1140 1141 arr = line.split() 1142 if variable_format == 'meson' and len(arr) != 2: 1143 raise MesonException('#mesondefine does not contain exactly two tokens: %s' % line.strip()) 1144 1145 varname = arr[1] 1146 try: 1147 (v, desc) = confdata.get(varname) 1148 except KeyError: 1149 return '/* #undef %s */\n' % varname 1150 if isinstance(v, bool): 1151 if v: 1152 return '#define %s\n' % varname 1153 else: 1154 return '#undef %s\n' % varname 1155 elif isinstance(v, int): 1156 return '#define %s %d\n' % (varname, v) 1157 elif isinstance(v, str): 1158 if variable_format == 'meson': 1159 result = v 1160 else: 1161 result = get_cmake_define(line, confdata) 1162 result = f'#define {varname} {result}\n' 1163 (result, missing_variable) = do_replacement(regex, result, variable_format, confdata) 1164 return result 1165 else: 1166 raise MesonException('#mesondefine argument "%s" is of unknown type.' % varname) 1167 1168def get_variable_regex(variable_format: str = 'meson') -> T.Pattern[str]: 1169 # Only allow (a-z, A-Z, 0-9, _, -) as valid characters for a define 1170 # Also allow escaping '@' with '\@' 1171 if variable_format in ['meson', 'cmake@']: 1172 regex = re.compile(r'(?:\\\\)+(?=\\?@)|\\@|@([-a-zA-Z0-9_]+)@') 1173 elif variable_format == 'cmake': 1174 regex = re.compile(r'(?:\\\\)+(?=\\?\$)|\\\${|\${([-a-zA-Z0-9_]+)}') 1175 else: 1176 raise MesonException(f'Format "{variable_format}" not handled') 1177 return regex 1178 1179def do_conf_str (src: str, data: list, confdata: 'ConfigurationData', variable_format: str, 1180 encoding: str = 'utf-8') -> T.Tuple[T.List[str],T.Set[str], bool]: 1181 def line_is_valid(line : str, variable_format: str) -> bool: 1182 if variable_format == 'meson': 1183 if '#cmakedefine' in line: 1184 return False 1185 else: #cmake format 1186 if '#mesondefine' in line: 1187 return False 1188 return True 1189 1190 regex = get_variable_regex(variable_format) 1191 1192 search_token = '#mesondefine' 1193 if variable_format != 'meson': 1194 search_token = '#cmakedefine' 1195 1196 result = [] 1197 missing_variables = set() 1198 # Detect when the configuration data is empty and no tokens were found 1199 # during substitution so we can warn the user to use the `copy:` kwarg. 1200 confdata_useless = not confdata.keys() 1201 for line in data: 1202 if line.startswith(search_token): 1203 confdata_useless = False 1204 line = do_define(regex, line, confdata, variable_format) 1205 else: 1206 if not line_is_valid(line,variable_format): 1207 raise MesonException(f'Format error in {src}: saw "{line.strip()}" when format set to "{variable_format}"') 1208 line, missing = do_replacement(regex, line, variable_format, confdata) 1209 missing_variables.update(missing) 1210 if missing: 1211 confdata_useless = False 1212 result.append(line) 1213 1214 return result, missing_variables, confdata_useless 1215 1216def do_conf_file(src: str, dst: str, confdata: 'ConfigurationData', variable_format: str, 1217 encoding: str = 'utf-8') -> T.Tuple[T.Set[str], bool]: 1218 try: 1219 with open(src, encoding=encoding, newline='') as f: 1220 data = f.readlines() 1221 except Exception as e: 1222 raise MesonException(f'Could not read input file {src}: {e!s}') 1223 1224 (result, missing_variables, confdata_useless) = do_conf_str(src, data, confdata, variable_format, encoding) 1225 dst_tmp = dst + '~' 1226 try: 1227 with open(dst_tmp, 'w', encoding=encoding, newline='') as f: 1228 f.writelines(result) 1229 except Exception as e: 1230 raise MesonException(f'Could not write output file {dst}: {e!s}') 1231 shutil.copymode(src, dst_tmp) 1232 replace_if_different(dst, dst_tmp) 1233 return missing_variables, confdata_useless 1234 1235CONF_C_PRELUDE = '''/* 1236 * Autogenerated by the Meson build system. 1237 * Do not edit, your changes will be lost. 1238 */ 1239 1240#pragma once 1241 1242''' 1243 1244CONF_NASM_PRELUDE = '''; Autogenerated by the Meson build system. 1245; Do not edit, your changes will be lost. 1246 1247''' 1248 1249def dump_conf_header(ofilename: str, cdata: 'ConfigurationData', output_format: str) -> None: 1250 if output_format == 'c': 1251 prelude = CONF_C_PRELUDE 1252 prefix = '#' 1253 elif output_format == 'nasm': 1254 prelude = CONF_NASM_PRELUDE 1255 prefix = '%' 1256 else: 1257 raise MesonBugException(f'Undefined output_format: "{output_format}"') 1258 1259 ofilename_tmp = ofilename + '~' 1260 with open(ofilename_tmp, 'w', encoding='utf-8') as ofile: 1261 ofile.write(prelude) 1262 for k in sorted(cdata.keys()): 1263 (v, desc) = cdata.get(k) 1264 if desc: 1265 if output_format == 'c': 1266 ofile.write('/* %s */\n' % desc) 1267 elif output_format == 'nasm': 1268 for line in desc.split('\n'): 1269 ofile.write('; %s\n' % line) 1270 if isinstance(v, bool): 1271 if v: 1272 ofile.write(f'{prefix}define {k}\n\n') 1273 else: 1274 ofile.write(f'{prefix}undef {k}\n\n') 1275 elif isinstance(v, (int, str)): 1276 ofile.write(f'{prefix}define {k} {v}\n\n') 1277 else: 1278 raise MesonException('Unknown data type in configuration file entry: ' + k) 1279 replace_if_different(ofilename, ofilename_tmp) 1280 1281 1282def replace_if_different(dst: str, dst_tmp: str) -> None: 1283 # If contents are identical, don't touch the file to prevent 1284 # unnecessary rebuilds. 1285 different = True 1286 try: 1287 with open(dst, 'rb') as f1, open(dst_tmp, 'rb') as f2: 1288 if f1.read() == f2.read(): 1289 different = False 1290 except FileNotFoundError: 1291 pass 1292 if different: 1293 os.replace(dst_tmp, dst) 1294 else: 1295 os.unlink(dst_tmp) 1296 1297 1298 1299def listify(item: T.Any, flatten: bool = True) -> T.List[T.Any]: 1300 ''' 1301 Returns a list with all args embedded in a list if they are not a list. 1302 This function preserves order. 1303 @flatten: Convert lists of lists to a flat list 1304 ''' 1305 if not isinstance(item, list): 1306 return [item] 1307 result = [] # type: T.List[T.Any] 1308 for i in item: 1309 if flatten and isinstance(i, list): 1310 result += listify(i, flatten=True) 1311 else: 1312 result.append(i) 1313 return result 1314 1315 1316def extract_as_list(dict_object: T.Dict[_T, _U], key: _T, pop: bool = False) -> T.List[_U]: 1317 ''' 1318 Extracts all values from given dict_object and listifies them. 1319 ''' 1320 fetch = dict_object.get 1321 if pop: 1322 fetch = dict_object.pop 1323 # If there's only one key, we don't return a list with one element 1324 return listify(fetch(key, []), flatten=True) 1325 1326 1327def typeslistify(item: 'T.Union[_T, T.Sequence[_T]]', 1328 types: 'T.Union[T.Type[_T], T.Tuple[T.Type[_T]]]') -> T.List[_T]: 1329 ''' 1330 Ensure that type(@item) is one of @types or a 1331 list of items all of which are of type @types 1332 ''' 1333 if isinstance(item, types): 1334 item = T.cast(T.List[_T], [item]) 1335 if not isinstance(item, list): 1336 raise MesonException('Item must be a list or one of {!r}, not {!r}'.format(types, type(item))) 1337 for i in item: 1338 if i is not None and not isinstance(i, types): 1339 raise MesonException('List item must be one of {!r}, not {!r}'.format(types, type(i))) 1340 return item 1341 1342 1343def stringlistify(item: T.Union[T.Any, T.Sequence[T.Any]]) -> T.List[str]: 1344 return typeslistify(item, str) 1345 1346 1347def expand_arguments(args: T.Iterable[str]) -> T.Optional[T.List[str]]: 1348 expended_args = [] # type: T.List[str] 1349 for arg in args: 1350 if not arg.startswith('@'): 1351 expended_args.append(arg) 1352 continue 1353 1354 args_file = arg[1:] 1355 try: 1356 with open(args_file, encoding='utf-8') as f: 1357 extended_args = f.read().split() 1358 expended_args += extended_args 1359 except Exception as e: 1360 mlog.error('Expanding command line arguments:', args_file, 'not found') 1361 mlog.exception(e) 1362 return None 1363 return expended_args 1364 1365 1366def partition(pred: T.Callable[[_T], object], iterable: T.Iterable[_T]) -> T.Tuple[T.Iterator[_T], T.Iterator[_T]]: 1367 """Use a predicate to partition entries into false entries and true 1368 entries. 1369 1370 >>> x, y = partition(is_odd, range(10)) 1371 >>> (list(x), list(y)) 1372 ([0, 2, 4, 6, 8], [1, 3, 5, 7, 9]) 1373 """ 1374 t1, t2 = tee(iterable) 1375 return filterfalse(pred, t1), filter(pred, t2) 1376 1377 1378def Popen_safe(args: T.List[str], write: T.Optional[str] = None, 1379 stdout: T.Union[T.TextIO, T.BinaryIO, int] = subprocess.PIPE, 1380 stderr: T.Union[T.TextIO, T.BinaryIO, int] = subprocess.PIPE, 1381 **kwargs: T.Any) -> T.Tuple['subprocess.Popen[str]', str, str]: 1382 import locale 1383 encoding = locale.getpreferredencoding() 1384 # Redirect stdin to DEVNULL otherwise the command run by us here might mess 1385 # up the console and ANSI colors will stop working on Windows. 1386 if 'stdin' not in kwargs: 1387 kwargs['stdin'] = subprocess.DEVNULL 1388 if not sys.stdout.encoding or encoding.upper() != 'UTF-8': 1389 p, o, e = Popen_safe_legacy(args, write=write, stdout=stdout, stderr=stderr, **kwargs) 1390 else: 1391 p = subprocess.Popen(args, universal_newlines=True, close_fds=False, 1392 stdout=stdout, stderr=stderr, **kwargs) 1393 o, e = p.communicate(write) 1394 # Sometimes the command that we run will call another command which will be 1395 # without the above stdin workaround, so set the console mode again just in 1396 # case. 1397 mlog.setup_console() 1398 return p, o, e 1399 1400 1401def Popen_safe_legacy(args: T.List[str], write: T.Optional[str] = None, 1402 stdout: T.Union[T.TextIO, T.BinaryIO, int] = subprocess.PIPE, 1403 stderr: T.Union[T.TextIO, T.BinaryIO, int] = subprocess.PIPE, 1404 **kwargs: T.Any) -> T.Tuple['subprocess.Popen[str]', str, str]: 1405 p = subprocess.Popen(args, universal_newlines=False, close_fds=False, 1406 stdout=stdout, stderr=stderr, **kwargs) 1407 input_ = None # type: T.Optional[bytes] 1408 if write is not None: 1409 input_ = write.encode('utf-8') 1410 o, e = p.communicate(input_) 1411 if o is not None: 1412 if sys.stdout.encoding: 1413 o = o.decode(encoding=sys.stdout.encoding, errors='replace').replace('\r\n', '\n') 1414 else: 1415 o = o.decode(errors='replace').replace('\r\n', '\n') 1416 if e is not None: 1417 if sys.stderr.encoding: 1418 e = e.decode(encoding=sys.stderr.encoding, errors='replace').replace('\r\n', '\n') 1419 else: 1420 e = e.decode(errors='replace').replace('\r\n', '\n') 1421 return p, o, e 1422 1423 1424def iter_regexin_iter(regexiter: T.Iterable[str], initer: T.Iterable[str]) -> T.Optional[str]: 1425 ''' 1426 Takes each regular expression in @regexiter and tries to search for it in 1427 every item in @initer. If there is a match, returns that match. 1428 Else returns False. 1429 ''' 1430 for regex in regexiter: 1431 for ii in initer: 1432 if not isinstance(ii, str): 1433 continue 1434 match = re.search(regex, ii) 1435 if match: 1436 return match.group() 1437 return None 1438 1439 1440def _substitute_values_check_errors(command: T.List[str], values: T.Dict[str, T.Union[str, T.List[str]]]) -> None: 1441 # Error checking 1442 inregex = ['@INPUT([0-9]+)?@', '@PLAINNAME@', '@BASENAME@'] # type: T.List[str] 1443 outregex = ['@OUTPUT([0-9]+)?@', '@OUTDIR@'] # type: T.List[str] 1444 if '@INPUT@' not in values: 1445 # Error out if any input-derived templates are present in the command 1446 match = iter_regexin_iter(inregex, command) 1447 if match: 1448 raise MesonException(f'Command cannot have {match!r}, since no input files were specified') 1449 else: 1450 if len(values['@INPUT@']) > 1: 1451 # Error out if @PLAINNAME@ or @BASENAME@ is present in the command 1452 match = iter_regexin_iter(inregex[1:], command) 1453 if match: 1454 raise MesonException(f'Command cannot have {match!r} when there is ' 1455 'more than one input file') 1456 # Error out if an invalid @INPUTnn@ template was specified 1457 for each in command: 1458 if not isinstance(each, str): 1459 continue 1460 match2 = re.search(inregex[0], each) 1461 if match2 and match2.group() not in values: 1462 m = 'Command cannot have {!r} since there are only {!r} inputs' 1463 raise MesonException(m.format(match2.group(), len(values['@INPUT@']))) 1464 if '@OUTPUT@' not in values: 1465 # Error out if any output-derived templates are present in the command 1466 match = iter_regexin_iter(outregex, command) 1467 if match: 1468 m = 'Command cannot have {!r} since there are no outputs' 1469 raise MesonException(m.format(match)) 1470 else: 1471 # Error out if an invalid @OUTPUTnn@ template was specified 1472 for each in command: 1473 if not isinstance(each, str): 1474 continue 1475 match2 = re.search(outregex[0], each) 1476 if match2 and match2.group() not in values: 1477 m = 'Command cannot have {!r} since there are only {!r} outputs' 1478 raise MesonException(m.format(match2.group(), len(values['@OUTPUT@']))) 1479 1480 1481def substitute_values(command: T.List[str], values: T.Dict[str, T.Union[str, T.List[str]]]) -> T.List[str]: 1482 ''' 1483 Substitute the template strings in the @values dict into the list of 1484 strings @command and return a new list. For a full list of the templates, 1485 see get_filenames_templates_dict() 1486 1487 If multiple inputs/outputs are given in the @values dictionary, we 1488 substitute @INPUT@ and @OUTPUT@ only if they are the entire string, not 1489 just a part of it, and in that case we substitute *all* of them. 1490 1491 The typing of this function is difficult, as only @OUTPUT@ and @INPUT@ can 1492 be lists, everything else is a string. However, TypeDict cannot represent 1493 this, as you can have optional keys, but not extra keys. We end up just 1494 having to us asserts to convince type checkers that this is okay. 1495 1496 https://github.com/python/mypy/issues/4617 1497 ''' 1498 1499 def replace(m: T.Match[str]) -> str: 1500 v = values[m.group(0)] 1501 assert isinstance(v, str), 'for mypy' 1502 return v 1503 1504 # Error checking 1505 _substitute_values_check_errors(command, values) 1506 1507 # Substitution 1508 outcmd = [] # type: T.List[str] 1509 rx_keys = [re.escape(key) for key in values if key not in ('@INPUT@', '@OUTPUT@')] 1510 value_rx = re.compile('|'.join(rx_keys)) if rx_keys else None 1511 for vv in command: 1512 more: T.Optional[str] = None 1513 if not isinstance(vv, str): 1514 outcmd.append(vv) 1515 elif '@INPUT@' in vv: 1516 inputs = values['@INPUT@'] 1517 if vv == '@INPUT@': 1518 outcmd += inputs 1519 elif len(inputs) == 1: 1520 outcmd.append(vv.replace('@INPUT@', inputs[0])) 1521 else: 1522 raise MesonException("Command has '@INPUT@' as part of a " 1523 "string and more than one input file") 1524 elif '@OUTPUT@' in vv: 1525 outputs = values['@OUTPUT@'] 1526 if vv == '@OUTPUT@': 1527 outcmd += outputs 1528 elif len(outputs) == 1: 1529 outcmd.append(vv.replace('@OUTPUT@', outputs[0])) 1530 else: 1531 raise MesonException("Command has '@OUTPUT@' as part of a " 1532 "string and more than one output file") 1533 1534 # Append values that are exactly a template string. 1535 # This is faster than a string replace. 1536 elif vv in values: 1537 o = values[vv] 1538 assert isinstance(o, str), 'for mypy' 1539 more = o 1540 # Substitute everything else with replacement 1541 elif value_rx: 1542 more = value_rx.sub(replace, vv) 1543 else: 1544 more = vv 1545 1546 if more is not None: 1547 outcmd.append(more) 1548 1549 return outcmd 1550 1551 1552def get_filenames_templates_dict(inputs: T.List[str], outputs: T.List[str]) -> T.Dict[str, T.Union[str, T.List[str]]]: 1553 ''' 1554 Create a dictionary with template strings as keys and values as values for 1555 the following templates: 1556 1557 @INPUT@ - the full path to one or more input files, from @inputs 1558 @OUTPUT@ - the full path to one or more output files, from @outputs 1559 @OUTDIR@ - the full path to the directory containing the output files 1560 1561 If there is only one input file, the following keys are also created: 1562 1563 @PLAINNAME@ - the filename of the input file 1564 @BASENAME@ - the filename of the input file with the extension removed 1565 1566 If there is more than one input file, the following keys are also created: 1567 1568 @INPUT0@, @INPUT1@, ... one for each input file 1569 1570 If there is more than one output file, the following keys are also created: 1571 1572 @OUTPUT0@, @OUTPUT1@, ... one for each output file 1573 ''' 1574 values = {} # type: T.Dict[str, T.Union[str, T.List[str]]] 1575 # Gather values derived from the input 1576 if inputs: 1577 # We want to substitute all the inputs. 1578 values['@INPUT@'] = inputs 1579 for (ii, vv) in enumerate(inputs): 1580 # Write out @INPUT0@, @INPUT1@, ... 1581 values[f'@INPUT{ii}@'] = vv 1582 if len(inputs) == 1: 1583 # Just one value, substitute @PLAINNAME@ and @BASENAME@ 1584 values['@PLAINNAME@'] = plain = os.path.basename(inputs[0]) 1585 values['@BASENAME@'] = os.path.splitext(plain)[0] 1586 if outputs: 1587 # Gather values derived from the outputs, similar to above. 1588 values['@OUTPUT@'] = outputs 1589 for (ii, vv) in enumerate(outputs): 1590 values[f'@OUTPUT{ii}@'] = vv 1591 # Outdir should be the same for all outputs 1592 values['@OUTDIR@'] = os.path.dirname(outputs[0]) 1593 # Many external programs fail on empty arguments. 1594 if values['@OUTDIR@'] == '': 1595 values['@OUTDIR@'] = '.' 1596 return values 1597 1598 1599def _make_tree_writable(topdir: str) -> None: 1600 # Ensure all files and directories under topdir are writable 1601 # (and readable) by owner. 1602 for d, _, files in os.walk(topdir): 1603 os.chmod(d, os.stat(d).st_mode | stat.S_IWRITE | stat.S_IREAD) 1604 for fname in files: 1605 fpath = os.path.join(d, fname) 1606 if os.path.isfile(fpath): 1607 os.chmod(fpath, os.stat(fpath).st_mode | stat.S_IWRITE | stat.S_IREAD) 1608 1609 1610def windows_proof_rmtree(f: str) -> None: 1611 # On Windows if anyone is holding a file open you can't 1612 # delete it. As an example an anti virus scanner might 1613 # be scanning files you are trying to delete. The only 1614 # way to fix this is to try again and again. 1615 delays = [0.1, 0.1, 0.2, 0.2, 0.2, 0.5, 0.5, 1, 1, 1, 1, 2] 1616 writable = False 1617 for d in delays: 1618 try: 1619 # Start by making the tree writable. 1620 if not writable: 1621 _make_tree_writable(f) 1622 writable = True 1623 except PermissionError: 1624 time.sleep(d) 1625 continue 1626 try: 1627 shutil.rmtree(f) 1628 return 1629 except FileNotFoundError: 1630 return 1631 except OSError: 1632 time.sleep(d) 1633 # Try one last time and throw if it fails. 1634 shutil.rmtree(f) 1635 1636 1637def windows_proof_rm(fpath: str) -> None: 1638 """Like windows_proof_rmtree, but for a single file.""" 1639 if os.path.isfile(fpath): 1640 os.chmod(fpath, os.stat(fpath).st_mode | stat.S_IWRITE | stat.S_IREAD) 1641 delays = [0.1, 0.1, 0.2, 0.2, 0.2, 0.5, 0.5, 1, 1, 1, 1, 2] 1642 for d in delays: 1643 try: 1644 os.unlink(fpath) 1645 return 1646 except FileNotFoundError: 1647 return 1648 except OSError: 1649 time.sleep(d) 1650 os.unlink(fpath) 1651 1652 1653class TemporaryDirectoryWinProof(TemporaryDirectory): 1654 """ 1655 Like TemporaryDirectory, but cleans things up using 1656 windows_proof_rmtree() 1657 """ 1658 1659 def __exit__(self, exc: T.Any, value: T.Any, tb: T.Any) -> None: 1660 try: 1661 super().__exit__(exc, value, tb) 1662 except OSError: 1663 windows_proof_rmtree(self.name) 1664 1665 def cleanup(self) -> None: 1666 try: 1667 super().cleanup() 1668 except OSError: 1669 windows_proof_rmtree(self.name) 1670 1671 1672def detect_subprojects(spdir_name: str, current_dir: str = '', 1673 result: T.Optional[T.Dict[str, T.List[str]]] = None) -> T.Optional[T.Dict[str, T.List[str]]]: 1674 if result is None: 1675 result = {} 1676 spdir = os.path.join(current_dir, spdir_name) 1677 if not os.path.exists(spdir): 1678 return result 1679 for trial in glob(os.path.join(spdir, '*')): 1680 basename = os.path.basename(trial) 1681 if trial == 'packagecache': 1682 continue 1683 append_this = True 1684 if os.path.isdir(trial): 1685 detect_subprojects(spdir_name, trial, result) 1686 elif trial.endswith('.wrap') and os.path.isfile(trial): 1687 basename = os.path.splitext(basename)[0] 1688 else: 1689 append_this = False 1690 if append_this: 1691 if basename in result: 1692 result[basename].append(trial) 1693 else: 1694 result[basename] = [trial] 1695 return result 1696 1697 1698def substring_is_in_list(substr: str, strlist: T.List[str]) -> bool: 1699 for s in strlist: 1700 if substr in s: 1701 return True 1702 return False 1703 1704 1705class OrderedSet(T.MutableSet[_T]): 1706 """A set that preserves the order in which items are added, by first 1707 insertion. 1708 """ 1709 def __init__(self, iterable: T.Optional[T.Iterable[_T]] = None): 1710 # typing.OrderedDict is new in 3.7.2, so we can't use that, but we can 1711 # use MutableMapping, which is fine in this case. 1712 self.__container = collections.OrderedDict() # type: T.MutableMapping[_T, None] 1713 if iterable: 1714 self.update(iterable) 1715 1716 def __contains__(self, value: object) -> bool: 1717 return value in self.__container 1718 1719 def __iter__(self) -> T.Iterator[_T]: 1720 return iter(self.__container.keys()) 1721 1722 def __len__(self) -> int: 1723 return len(self.__container) 1724 1725 def __repr__(self) -> str: 1726 # Don't print 'OrderedSet("")' for an empty set. 1727 if self.__container: 1728 return 'OrderedSet("{}")'.format( 1729 '", "'.join(repr(e) for e in self.__container.keys())) 1730 return 'OrderedSet()' 1731 1732 def __reversed__(self) -> T.Iterator[_T]: 1733 # Mypy is complaining that sets cant be reversed, which is true for 1734 # unordered sets, but this is an ordered, set so reverse() makes sense. 1735 return reversed(self.__container.keys()) # type: ignore 1736 1737 def add(self, value: _T) -> None: 1738 self.__container[value] = None 1739 1740 def discard(self, value: _T) -> None: 1741 if value in self.__container: 1742 del self.__container[value] 1743 1744 def move_to_end(self, value: _T, last: bool = True) -> None: 1745 # Mypy does not know about move_to_end, because it is not part of MutableMapping 1746 self.__container.move_to_end(value, last) # type: ignore 1747 1748 def pop(self, last: bool = True) -> _T: 1749 # Mypy does not know about the last argument, because it is not part of MutableMapping 1750 item, _ = self.__container.popitem(last) # type: ignore 1751 return item 1752 1753 def update(self, iterable: T.Iterable[_T]) -> None: 1754 for item in iterable: 1755 self.__container[item] = None 1756 1757 def difference(self, set_: T.Union[T.Set[_T], 'OrderedSet[_T]']) -> 'OrderedSet[_T]': 1758 return type(self)(e for e in self if e not in set_) 1759 1760def relpath(path: str, start: str) -> str: 1761 # On Windows a relative path can't be evaluated for paths on two different 1762 # drives (i.e. c:\foo and f:\bar). The only thing left to do is to use the 1763 # original absolute path. 1764 try: 1765 return os.path.relpath(path, start) 1766 except (TypeError, ValueError): 1767 return path 1768 1769def path_is_in_root(path: Path, root: Path, resolve: bool = False) -> bool: 1770 # Check whether a path is within the root directory root 1771 try: 1772 if resolve: 1773 path.resolve().relative_to(root.resolve()) 1774 else: 1775 path.relative_to(root) 1776 except ValueError: 1777 return False 1778 return True 1779 1780def relative_to_if_possible(path: Path, root: Path, resolve: bool = False) -> Path: 1781 try: 1782 if resolve: 1783 return path.resolve().relative_to(root.resolve()) 1784 else: 1785 return path.relative_to(root) 1786 except ValueError: 1787 return path 1788 1789class LibType(enum.IntEnum): 1790 1791 """Enumeration for library types.""" 1792 1793 SHARED = 0 1794 STATIC = 1 1795 PREFER_SHARED = 2 1796 PREFER_STATIC = 3 1797 1798 1799class ProgressBarFallback: # lgtm [py/iter-returns-non-self] 1800 ''' 1801 Fallback progress bar implementation when tqdm is not found 1802 1803 Since this class is not an actual iterator, but only provides a minimal 1804 fallback, it is safe to ignore the 'Iterator does not return self from 1805 __iter__ method' warning. 1806 ''' 1807 def __init__(self, iterable: T.Optional[T.Iterable[str]] = None, total: T.Optional[int] = None, 1808 bar_type: T.Optional[str] = None, desc: T.Optional[str] = None): 1809 if iterable is not None: 1810 self.iterable = iter(iterable) 1811 return 1812 self.total = total 1813 self.done = 0 1814 self.printed_dots = 0 1815 if self.total and bar_type == 'download': 1816 print('Download size:', self.total) 1817 if desc: 1818 print(f'{desc}: ', end='') 1819 1820 # Pretend to be an iterator when called as one and don't print any 1821 # progress 1822 def __iter__(self) -> T.Iterator[str]: 1823 return self.iterable 1824 1825 def __next__(self) -> str: 1826 return next(self.iterable) 1827 1828 def print_dot(self) -> None: 1829 print('.', end='') 1830 sys.stdout.flush() 1831 self.printed_dots += 1 1832 1833 def update(self, progress: int) -> None: 1834 self.done += progress 1835 if not self.total: 1836 # Just print one dot per call if we don't have a total length 1837 self.print_dot() 1838 return 1839 ratio = int(self.done / self.total * 10) 1840 while self.printed_dots < ratio: 1841 self.print_dot() 1842 1843 def close(self) -> None: 1844 print('') 1845 1846try: 1847 from tqdm import tqdm 1848except ImportError: 1849 # ideally we would use a typing.Protocol here, but it's part of typing_extensions until 3.8 1850 ProgressBar = ProgressBarFallback # type: T.Union[T.Type[ProgressBarFallback], T.Type[ProgressBarTqdm]] 1851else: 1852 class ProgressBarTqdm(tqdm): 1853 def __init__(self, *args: T.Any, bar_type: T.Optional[str] = None, **kwargs: T.Any) -> None: 1854 if bar_type == 'download': 1855 kwargs.update({'unit': 'bytes', 'leave': True}) 1856 else: 1857 kwargs.update({'leave': False}) 1858 kwargs['ncols'] = 100 1859 super().__init__(*args, **kwargs) 1860 1861 ProgressBar = ProgressBarTqdm 1862 1863 1864class RealPathAction(argparse.Action): 1865 def __init__(self, option_strings: T.List[str], dest: str, default: str = '.', **kwargs: T.Any): 1866 default = os.path.abspath(os.path.realpath(default)) 1867 super().__init__(option_strings, dest, nargs=None, default=default, **kwargs) 1868 1869 def __call__(self, parser: argparse.ArgumentParser, namespace: argparse.Namespace, 1870 values: T.Union[str, T.Sequence[T.Any], None], option_string: str = None) -> None: 1871 assert isinstance(values, str) 1872 setattr(namespace, self.dest, os.path.abspath(os.path.realpath(values))) 1873 1874 1875def get_wine_shortpath(winecmd: T.List[str], wine_paths: T.Sequence[str]) -> str: 1876 """Get A short version of @wine_paths to avoid reaching WINEPATH number 1877 of char limit. 1878 """ 1879 1880 wine_paths = list(OrderedSet(wine_paths)) 1881 1882 getShortPathScript = '%s.bat' % str(uuid.uuid4()).lower()[:5] 1883 with open(getShortPathScript, mode='w', encoding='utf-8') as f: 1884 f.write("@ECHO OFF\nfor %%x in (%*) do (\n echo|set /p=;%~sx\n)\n") 1885 f.flush() 1886 try: 1887 with open(os.devnull, 'w', encoding='utf-8') as stderr: 1888 wine_path = subprocess.check_output( 1889 winecmd + 1890 ['cmd', '/C', getShortPathScript] + wine_paths, 1891 stderr=stderr).decode('utf-8') 1892 except subprocess.CalledProcessError as e: 1893 print("Could not get short paths: %s" % e) 1894 wine_path = ';'.join(wine_paths) 1895 finally: 1896 os.remove(getShortPathScript) 1897 if len(wine_path) > 2048: 1898 raise MesonException( 1899 'WINEPATH size {} > 2048' 1900 ' this will cause random failure.'.format( 1901 len(wine_path))) 1902 1903 return wine_path.strip(';') 1904 1905 1906def run_once(func: T.Callable[..., _T]) -> T.Callable[..., _T]: 1907 ret = [] # type: T.List[_T] 1908 1909 @wraps(func) 1910 def wrapper(*args: T.Any, **kwargs: T.Any) -> _T: 1911 if ret: 1912 return ret[0] 1913 1914 val = func(*args, **kwargs) 1915 ret.append(val) 1916 return val 1917 1918 return wrapper 1919 1920 1921class OptionProxy(T.Generic[_T]): 1922 def __init__(self, value: _T, choices: T.Optional[T.List[str]] = None): 1923 self.value = value 1924 self.choices = choices 1925 1926 def set_value(self, v: _T) -> None: 1927 # XXX: should this be an error 1928 self.value = v 1929 1930 1931class OptionOverrideProxy(collections.abc.MutableMapping): 1932 1933 '''Mimic an option list but transparently override selected option 1934 values. 1935 ''' 1936 1937 # TODO: the typing here could be made more explicit using a TypeDict from 1938 # python 3.8 or typing_extensions 1939 1940 def __init__(self, overrides: T.Dict['OptionKey', T.Any], *options: 'KeyedOptionDictType'): 1941 self.overrides = overrides.copy() 1942 self.options: T.Dict['OptionKey', UserOption] = {} 1943 for o in options: 1944 self.options.update(o) 1945 1946 def __getitem__(self, key: 'OptionKey') -> T.Union['UserOption', OptionProxy]: 1947 if key in self.options: 1948 opt = self.options[key] 1949 if key in self.overrides: 1950 return OptionProxy(opt.validate_value(self.overrides[key]), getattr(opt, 'choices', None)) 1951 return opt 1952 raise KeyError('Option not found', key) 1953 1954 def __setitem__(self, key: 'OptionKey', value: T.Union['UserOption', OptionProxy]) -> None: 1955 self.overrides[key] = value.value 1956 1957 def __delitem__(self, key: 'OptionKey') -> None: 1958 del self.overrides[key] 1959 1960 def __iter__(self) -> T.Iterator['OptionKey']: 1961 return iter(self.options) 1962 1963 def __len__(self) -> int: 1964 return len(self.options) 1965 1966 def copy(self) -> 'OptionOverrideProxy': 1967 return OptionOverrideProxy(self.overrides.copy(), self.options.copy()) 1968 1969 1970class OptionType(enum.IntEnum): 1971 1972 """Enum used to specify what kind of argument a thing is.""" 1973 1974 BUILTIN = 0 1975 BACKEND = 1 1976 BASE = 2 1977 COMPILER = 3 1978 PROJECT = 4 1979 1980# This is copied from coredata. There is no way to share this, because this 1981# is used in the OptionKey constructor, and the coredata lists are 1982# OptionKeys... 1983_BUILTIN_NAMES = { 1984 'prefix', 1985 'bindir', 1986 'datadir', 1987 'includedir', 1988 'infodir', 1989 'libdir', 1990 'libexecdir', 1991 'localedir', 1992 'localstatedir', 1993 'mandir', 1994 'sbindir', 1995 'sharedstatedir', 1996 'sysconfdir', 1997 'auto_features', 1998 'backend', 1999 'buildtype', 2000 'debug', 2001 'default_library', 2002 'errorlogs', 2003 'install_umask', 2004 'layout', 2005 'optimization', 2006 'stdsplit', 2007 'strip', 2008 'unity', 2009 'unity_size', 2010 'warning_level', 2011 'werror', 2012 'wrap_mode', 2013 'force_fallback_for', 2014 'pkg_config_path', 2015 'cmake_prefix_path', 2016} 2017 2018 2019def _classify_argument(key: 'OptionKey') -> OptionType: 2020 """Classify arguments into groups so we know which dict to assign them to.""" 2021 2022 if key.name.startswith('b_'): 2023 return OptionType.BASE 2024 elif key.lang is not None: 2025 return OptionType.COMPILER 2026 elif key.name in _BUILTIN_NAMES: 2027 return OptionType.BUILTIN 2028 elif key.name.startswith('backend_'): 2029 assert key.machine is MachineChoice.HOST, str(key) 2030 return OptionType.BACKEND 2031 else: 2032 assert key.machine is MachineChoice.HOST, str(key) 2033 return OptionType.PROJECT 2034 2035 2036@total_ordering 2037class OptionKey: 2038 2039 """Represents an option key in the various option dictionaries. 2040 2041 This provides a flexible, powerful way to map option names from their 2042 external form (things like subproject:build.option) to something that 2043 internally easier to reason about and produce. 2044 """ 2045 2046 __slots__ = ['name', 'subproject', 'machine', 'lang', '_hash', 'type'] 2047 2048 name: str 2049 subproject: str 2050 machine: MachineChoice 2051 lang: T.Optional[str] 2052 _hash: int 2053 type: OptionType 2054 2055 def __init__(self, name: str, subproject: str = '', 2056 machine: MachineChoice = MachineChoice.HOST, 2057 lang: T.Optional[str] = None, _type: T.Optional[OptionType] = None): 2058 # the _type option to the constructor is kinda private. We want to be 2059 # able tos ave the state and avoid the lookup function when 2060 # pickling/unpickling, but we need to be able to calculate it when 2061 # constructing a new OptionKey 2062 object.__setattr__(self, 'name', name) 2063 object.__setattr__(self, 'subproject', subproject) 2064 object.__setattr__(self, 'machine', machine) 2065 object.__setattr__(self, 'lang', lang) 2066 object.__setattr__(self, '_hash', hash((name, subproject, machine, lang))) 2067 if _type is None: 2068 _type = _classify_argument(self) 2069 object.__setattr__(self, 'type', _type) 2070 2071 def __setattr__(self, key: str, value: T.Any) -> None: 2072 raise AttributeError('OptionKey instances do not support mutation.') 2073 2074 def __getstate__(self) -> T.Dict[str, T.Any]: 2075 return { 2076 'name': self.name, 2077 'subproject': self.subproject, 2078 'machine': self.machine, 2079 'lang': self.lang, 2080 '_type': self.type, 2081 } 2082 2083 def __setstate__(self, state: T.Dict[str, T.Any]) -> None: 2084 """De-serialize the state of a pickle. 2085 2086 This is very clever. __init__ is not a constructor, it's an 2087 initializer, therefore it's safe to call more than once. We create a 2088 state in the custom __getstate__ method, which is valid to pass 2089 splatted to the initializer. 2090 """ 2091 # Mypy doesn't like this, because it's so clever. 2092 self.__init__(**state) # type: ignore 2093 2094 def __hash__(self) -> int: 2095 return self._hash 2096 2097 def __eq__(self, other: object) -> bool: 2098 if isinstance(other, OptionKey): 2099 return ( 2100 self.name == other.name and 2101 self.subproject == other.subproject and 2102 self.machine is other.machine and 2103 self.lang == other.lang) 2104 return NotImplemented 2105 2106 def __lt__(self, other: object) -> bool: 2107 if isinstance(other, OptionKey): 2108 self_tuple = (self.subproject, self.type, self.lang, self.machine, self.name) 2109 other_tuple = (other.subproject, other.type, other.lang, other.machine, other.name) 2110 return self_tuple < other_tuple 2111 return NotImplemented 2112 2113 def __str__(self) -> str: 2114 out = self.name 2115 if self.lang: 2116 out = f'{self.lang}_{out}' 2117 if self.machine is MachineChoice.BUILD: 2118 out = f'build.{out}' 2119 if self.subproject: 2120 out = f'{self.subproject}:{out}' 2121 return out 2122 2123 def __repr__(self) -> str: 2124 return f'OptionKey({repr(self.name)}, {repr(self.subproject)}, {repr(self.machine)}, {repr(self.lang)})' 2125 2126 @classmethod 2127 def from_string(cls, raw: str) -> 'OptionKey': 2128 """Parse the raw command line format into a three part tuple. 2129 2130 This takes strings like `mysubproject:build.myoption` and Creates an 2131 OptionKey out of them. 2132 """ 2133 try: 2134 subproject, raw2 = raw.split(':') 2135 except ValueError: 2136 subproject, raw2 = '', raw 2137 2138 if raw2.startswith('build.'): 2139 raw3 = raw2.split('.', 1)[1] 2140 for_machine = MachineChoice.BUILD 2141 else: 2142 raw3 = raw2 2143 for_machine = MachineChoice.HOST 2144 2145 from ..compilers import all_languages 2146 if any(raw3.startswith(f'{l}_') for l in all_languages): 2147 lang, opt = raw3.split('_', 1) 2148 else: 2149 lang, opt = None, raw3 2150 assert ':' not in opt 2151 assert 'build.' not in opt 2152 2153 return cls(opt, subproject, for_machine, lang) 2154 2155 def evolve(self, name: T.Optional[str] = None, subproject: T.Optional[str] = None, 2156 machine: T.Optional[MachineChoice] = None, lang: T.Optional[str] = '') -> 'OptionKey': 2157 """Create a new copy of this key, but with alterted members. 2158 2159 For example: 2160 >>> a = OptionKey('foo', '', MachineChoice.Host) 2161 >>> b = OptionKey('foo', 'bar', MachineChoice.Host) 2162 >>> b == a.evolve(subproject='bar') 2163 True 2164 """ 2165 # We have to be a little clever with lang here, because lang is valid 2166 # as None, for non-compiler options 2167 return OptionKey( 2168 name if name is not None else self.name, 2169 subproject if subproject is not None else self.subproject, 2170 machine if machine is not None else self.machine, 2171 lang if lang != '' else self.lang, 2172 ) 2173 2174 def as_root(self) -> 'OptionKey': 2175 """Convenience method for key.evolve(subproject='').""" 2176 return self.evolve(subproject='') 2177 2178 def as_build(self) -> 'OptionKey': 2179 """Convenience method for key.evolve(machine=MachinceChoice.BUILD).""" 2180 return self.evolve(machine=MachineChoice.BUILD) 2181 2182 def as_host(self) -> 'OptionKey': 2183 """Convenience method for key.evolve(machine=MachinceChoice.HOST).""" 2184 return self.evolve(machine=MachineChoice.HOST) 2185 2186 def is_backend(self) -> bool: 2187 """Convenience method to check if this is a backend option.""" 2188 return self.type is OptionType.BACKEND 2189 2190 def is_builtin(self) -> bool: 2191 """Convenience method to check if this is a builtin option.""" 2192 return self.type is OptionType.BUILTIN 2193 2194 def is_compiler(self) -> bool: 2195 """Convenience method to check if this is a builtin option.""" 2196 return self.type is OptionType.COMPILER 2197 2198 def is_project(self) -> bool: 2199 """Convenience method to check if this is a project option.""" 2200 return self.type is OptionType.PROJECT 2201 2202 def is_base(self) -> bool: 2203 """Convenience method to check if this is a base option.""" 2204 return self.type is OptionType.BASE 2205