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