1# Copyright 2012-2016 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
15import subprocess
16import typing as T
17from enum import Enum
18
19from . import mesonlib
20from .mesonlib import EnvironmentException, HoldableObject
21from . import mlog
22from pathlib import Path
23
24
25# These classes contains all the data pulled from configuration files (native
26# and cross file currently), and also assists with the reading environment
27# variables.
28#
29# At this time there isn't an ironclad difference between this an other sources
30# of state like `coredata`. But one rough guide is much what is in `coredata` is
31# the *output* of the configuration process: the final decisions after tests.
32# This, on the other hand has *inputs*. The config files are parsed, but
33# otherwise minimally transformed. When more complex fallbacks (environment
34# detection) exist, they are defined elsewhere as functions that construct
35# instances of these classes.
36
37
38known_cpu_families = (
39    'aarch64',
40    'alpha',
41    'arc',
42    'arm',
43    'avr',
44    'c2000',
45    'csky',
46    'dspic',
47    'e2k',
48    'ft32',
49    'ia64',
50    'loongarch64',
51    'm68k',
52    'microblaze',
53    'mips',
54    'mips64',
55    'parisc',
56    'pic24',
57    'ppc',
58    'ppc64',
59    'riscv32',
60    'riscv64',
61    'rl78',
62    'rx',
63    's390',
64    's390x',
65    'sh4',
66    'sparc',
67    'sparc64',
68    'wasm32',
69    'wasm64',
70    'x86',
71    'x86_64',
72)
73
74# It would feel more natural to call this "64_BIT_CPU_FAMILIES", but
75# python identifiers cannot start with numbers
76CPU_FAMILIES_64_BIT = [
77    'aarch64',
78    'alpha',
79    'ia64',
80    'loongarch64',
81    'mips64',
82    'ppc64',
83    'riscv64',
84    's390x',
85    'sparc64',
86    'wasm64',
87    'x86_64',
88]
89
90# Map from language identifiers to environment variables.
91ENV_VAR_PROG_MAP: T.Mapping[str, str] = {
92    # Compilers
93    'c': 'CC',
94    'cpp': 'CXX',
95    'cs': 'CSC',
96    'd': 'DC',
97    'fortran': 'FC',
98    'objc': 'OBJC',
99    'objcpp': 'OBJCXX',
100    'rust': 'RUSTC',
101    'vala': 'VALAC',
102
103    # Linkers
104    'c_ld': 'CC_LD',
105    'cpp_ld': 'CXX_LD',
106    'd_ld': 'DC_LD',
107    'fortran_ld': 'FC_LD',
108    'objc_ld': 'OBJC_LD',
109    'objcpp_ld': 'OBJCXX_LD',
110    'rust_ld': 'RUSTC_LD',
111
112    # Binutils
113    'strip': 'STRIP',
114    'ar': 'AR',
115    'windres': 'WINDRES',
116
117    # Other tools
118    'cmake': 'CMAKE',
119    'qmake': 'QMAKE',
120    'pkgconfig': 'PKG_CONFIG',
121    'make': 'MAKE',
122}
123
124# Deprecated environment variables mapped from the new variable to the old one
125# Deprecated in 0.54.0
126DEPRECATED_ENV_PROG_MAP: T.Mapping[str, str] = {
127    'd_ld': 'D_LD',
128    'fortran_ld': 'F_LD',
129    'rust_ld': 'RUST_LD',
130    'objcpp_ld': 'OBJCPP_LD',
131}
132
133class CMakeSkipCompilerTest(Enum):
134    ALWAYS = 'always'
135    NEVER = 'never'
136    DEP_ONLY = 'dep_only'
137
138class Properties:
139    def __init__(
140            self,
141            properties: T.Optional[T.Dict[str, T.Optional[T.Union[str, bool, int, T.List[str]]]]] = None,
142    ):
143        self.properties = properties or {}  # type: T.Dict[str, T.Optional[T.Union[str, bool, int, T.List[str]]]]
144
145    def has_stdlib(self, language: str) -> bool:
146        return language + '_stdlib' in self.properties
147
148    # Some of get_stdlib, get_root, get_sys_root are wider than is actually
149    # true, but without heterogenious dict annotations it's not practical to
150    # narrow them
151    def get_stdlib(self, language: str) -> T.Union[str, T.List[str]]:
152        stdlib = self.properties[language + '_stdlib']
153        if isinstance(stdlib, str):
154            return stdlib
155        assert isinstance(stdlib, list)
156        for i in stdlib:
157            assert isinstance(i, str)
158        return stdlib
159
160    def get_root(self) -> T.Optional[str]:
161        root = self.properties.get('root', None)
162        assert root is None or isinstance(root, str)
163        return root
164
165    def get_sys_root(self) -> T.Optional[str]:
166        sys_root = self.properties.get('sys_root', None)
167        assert sys_root is None or isinstance(sys_root, str)
168        return sys_root
169
170    def get_pkg_config_libdir(self) -> T.Optional[T.List[str]]:
171        p = self.properties.get('pkg_config_libdir', None)
172        if p is None:
173            return p
174        res = mesonlib.listify(p)
175        for i in res:
176            assert isinstance(i, str)
177        return res
178
179    def get_cmake_defaults(self) -> bool:
180        if 'cmake_defaults' not in self.properties:
181            return True
182        res = self.properties['cmake_defaults']
183        assert isinstance(res, bool)
184        return res
185
186    def get_cmake_toolchain_file(self) -> T.Optional[Path]:
187        if 'cmake_toolchain_file' not in self.properties:
188            return None
189        raw = self.properties['cmake_toolchain_file']
190        assert isinstance(raw, str)
191        cmake_toolchain_file = Path(raw)
192        if not cmake_toolchain_file.is_absolute():
193            raise EnvironmentException(f'cmake_toolchain_file ({raw}) is not absolute')
194        return cmake_toolchain_file
195
196    def get_cmake_skip_compiler_test(self) -> CMakeSkipCompilerTest:
197        if 'cmake_skip_compiler_test' not in self.properties:
198            return CMakeSkipCompilerTest.DEP_ONLY
199        raw = self.properties['cmake_skip_compiler_test']
200        assert isinstance(raw, str)
201        try:
202            return CMakeSkipCompilerTest(raw)
203        except ValueError:
204            raise EnvironmentException(
205                '"{}" is not a valid value for cmake_skip_compiler_test. Supported values are {}'
206                .format(raw, [e.value for e in CMakeSkipCompilerTest]))
207
208    def get_cmake_use_exe_wrapper(self) -> bool:
209        if 'cmake_use_exe_wrapper' not in self.properties:
210            return True
211        res = self.properties['cmake_use_exe_wrapper']
212        assert isinstance(res, bool)
213        return res
214
215    def get_java_home(self) -> T.Optional[Path]:
216        value = T.cast(T.Optional[str], self.properties.get('java_home'))
217        return Path(value) if value else None
218
219    def __eq__(self, other: object) -> bool:
220        if isinstance(other, type(self)):
221            return self.properties == other.properties
222        return NotImplemented
223
224    # TODO consider removing so Properties is less freeform
225    def __getitem__(self, key: str) -> T.Optional[T.Union[str, bool, int, T.List[str]]]:
226        return self.properties[key]
227
228    # TODO consider removing so Properties is less freeform
229    def __contains__(self, item: T.Union[str, bool, int, T.List[str]]) -> bool:
230        return item in self.properties
231
232    # TODO consider removing, for same reasons as above
233    def get(self, key: str, default: T.Optional[T.Union[str, bool, int, T.List[str]]] = None) -> T.Optional[T.Union[str, bool, int, T.List[str]]]:
234        return self.properties.get(key, default)
235
236class MachineInfo(HoldableObject):
237    def __init__(self, system: str, cpu_family: str, cpu: str, endian: str):
238        self.system = system
239        self.cpu_family = cpu_family
240        self.cpu = cpu
241        self.endian = endian
242        self.is_64_bit = cpu_family in CPU_FAMILIES_64_BIT  # type: bool
243
244    def __eq__(self, other: object) -> bool:
245        if not isinstance(other, MachineInfo):
246            return NotImplemented
247        return \
248            self.system == other.system and \
249            self.cpu_family == other.cpu_family and \
250            self.cpu == other.cpu and \
251            self.endian == other.endian
252
253    def __ne__(self, other: object) -> bool:
254        if not isinstance(other, MachineInfo):
255            return NotImplemented
256        return not self.__eq__(other)
257
258    def __repr__(self) -> str:
259        return f'<MachineInfo: {self.system} {self.cpu_family} ({self.cpu})>'
260
261    @classmethod
262    def from_literal(cls, literal: T.Dict[str, str]) -> 'MachineInfo':
263        minimum_literal = {'cpu', 'cpu_family', 'endian', 'system'}
264        if set(literal) < minimum_literal:
265            raise EnvironmentException(
266                f'Machine info is currently {literal}\n' +
267                'but is missing {}.'.format(minimum_literal - set(literal)))
268
269        cpu_family = literal['cpu_family']
270        if cpu_family not in known_cpu_families:
271            mlog.warning(f'Unknown CPU family {cpu_family}, please report this at https://github.com/mesonbuild/meson/issues/new')
272
273        endian = literal['endian']
274        if endian not in ('little', 'big'):
275            mlog.warning(f'Unknown endian {endian}')
276
277        return cls(literal['system'], cpu_family, literal['cpu'], endian)
278
279    def is_windows(self) -> bool:
280        """
281        Machine is windows?
282        """
283        return self.system == 'windows'
284
285    def is_cygwin(self) -> bool:
286        """
287        Machine is cygwin?
288        """
289        return self.system == 'cygwin'
290
291    def is_linux(self) -> bool:
292        """
293        Machine is linux?
294        """
295        return self.system == 'linux'
296
297    def is_darwin(self) -> bool:
298        """
299        Machine is Darwin (iOS/tvOS/OS X)?
300        """
301        return self.system in {'darwin', 'ios', 'tvos'}
302
303    def is_android(self) -> bool:
304        """
305        Machine is Android?
306        """
307        return self.system == 'android'
308
309    def is_haiku(self) -> bool:
310        """
311        Machine is Haiku?
312        """
313        return self.system == 'haiku'
314
315    def is_netbsd(self) -> bool:
316        """
317        Machine is NetBSD?
318        """
319        return self.system == 'netbsd'
320
321    def is_openbsd(self) -> bool:
322        """
323        Machine is OpenBSD?
324        """
325        return self.system == 'openbsd'
326
327    def is_dragonflybsd(self) -> bool:
328        """Machine is DragonflyBSD?"""
329        return self.system == 'dragonfly'
330
331    def is_freebsd(self) -> bool:
332        """Machine is FreeBSD?"""
333        return self.system == 'freebsd'
334
335    def is_sunos(self) -> bool:
336        """Machine is illumos or Solaris?"""
337        return self.system == 'sunos'
338
339    def is_hurd(self) -> bool:
340        """
341        Machine is GNU/Hurd?
342        """
343        return self.system == 'gnu'
344
345    def is_irix(self) -> bool:
346        """Machine is IRIX?"""
347        return self.system.startswith('irix')
348
349    # Various prefixes and suffixes for import libraries, shared libraries,
350    # static libraries, and executables.
351    # Versioning is added to these names in the backends as-needed.
352    def get_exe_suffix(self) -> str:
353        if self.is_windows() or self.is_cygwin():
354            return 'exe'
355        else:
356            return ''
357
358    def get_object_suffix(self) -> str:
359        if self.is_windows():
360            return 'obj'
361        else:
362            return 'o'
363
364    def libdir_layout_is_win(self) -> bool:
365        return self.is_windows() or self.is_cygwin()
366
367class BinaryTable:
368
369    def __init__(
370            self,
371            binaries: T.Optional[T.Dict[str, T.Union[str, T.List[str]]]] = None,
372    ):
373        self.binaries: T.Dict[str, T.List[str]] = {}
374        if binaries:
375            for name, command in binaries.items():
376                if not isinstance(command, (list, str)):
377                    raise mesonlib.MesonException(
378                        f'Invalid type {command!r} for entry {name!r} in cross file')
379                self.binaries[name] = mesonlib.listify(command)
380
381    @staticmethod
382    def detect_ccache() -> T.List[str]:
383        try:
384            subprocess.check_call(['ccache', '--version'], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
385        except (OSError, subprocess.CalledProcessError):
386            return []
387        return ['ccache']
388
389    @classmethod
390    def parse_entry(cls, entry: T.Union[str, T.List[str]]) -> T.Tuple[T.List[str], T.List[str]]:
391        compiler = mesonlib.stringlistify(entry)
392        # Ensure ccache exists and remove it if it doesn't
393        if compiler[0] == 'ccache':
394            compiler = compiler[1:]
395            ccache = cls.detect_ccache()
396        else:
397            ccache = []
398        # Return value has to be a list of compiler 'choices'
399        return compiler, ccache
400
401    def lookup_entry(self, name: str) -> T.Optional[T.List[str]]:
402        """Lookup binary in cross/native file and fallback to environment.
403
404        Returns command with args as list if found, Returns `None` if nothing is
405        found.
406        """
407        command = self.binaries.get(name)
408        if not command:
409            return None
410        elif not command[0].strip():
411            return None
412        return command
413
414class CMakeVariables:
415    def __init__(self, variables: T.Optional[T.Dict[str, T.Any]] = None) -> None:
416        variables = variables or {}
417        self.variables = {}  # type: T.Dict[str, T.List[str]]
418
419        for key, value in variables.items():
420            value = mesonlib.listify(value)
421            for i in value:
422                assert isinstance(i, str)
423            self.variables[key] = value
424
425    def get_variables(self) -> T.Dict[str, T.List[str]]:
426        return self.variables
427