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