| # SPDX-License-Identifier: Apache-2.0 |
| # Copyright 2012-2016 The Meson development team |
| |
| from __future__ import annotations |
| |
| from dataclasses import dataclass |
| import subprocess |
| import typing as T |
| from enum import Enum |
| |
| from . import mesonlib |
| from .mesonlib import EnvironmentException, HoldableObject |
| from . import mlog |
| from pathlib import Path |
| |
| |
| # These classes contains all the data pulled from configuration files (native |
| # and cross file currently), and also assists with the reading environment |
| # variables. |
| # |
| # At this time there isn't an ironclad difference between this and other sources |
| # of state like `coredata`. But one rough guide is much what is in `coredata` is |
| # the *output* of the configuration process: the final decisions after tests. |
| # This, on the other hand has *inputs*. The config files are parsed, but |
| # otherwise minimally transformed. When more complex fallbacks (environment |
| # detection) exist, they are defined elsewhere as functions that construct |
| # instances of these classes. |
| |
| |
| known_cpu_families = ( |
| 'aarch64', |
| 'alpha', |
| 'arc', |
| 'arm', |
| 'avr', |
| 'c2000', |
| 'c6000', |
| 'csky', |
| 'dspic', |
| 'e2k', |
| 'ft32', |
| 'ia64', |
| 'loongarch64', |
| 'm68k', |
| 'microblaze', |
| 'mips', |
| 'mips64', |
| 'msp430', |
| 'parisc', |
| 'pic24', |
| 'ppc', |
| 'ppc64', |
| 'riscv32', |
| 'riscv64', |
| 'rl78', |
| 'rx', |
| 's390', |
| 's390x', |
| 'sh4', |
| 'sparc', |
| 'sparc64', |
| 'sw_64', |
| 'wasm32', |
| 'wasm64', |
| 'x86', |
| 'x86_64', |
| ) |
| |
| # It would feel more natural to call this "64_BIT_CPU_FAMILIES", but |
| # python identifiers cannot start with numbers |
| CPU_FAMILIES_64_BIT = [ |
| 'aarch64', |
| 'alpha', |
| 'ia64', |
| 'loongarch64', |
| 'mips64', |
| 'ppc64', |
| 'riscv64', |
| 's390x', |
| 'sparc64', |
| 'sw_64', |
| 'wasm64', |
| 'x86_64', |
| ] |
| |
| # Map from language identifiers to environment variables. |
| ENV_VAR_COMPILER_MAP: T.Mapping[str, str] = { |
| # Compilers |
| 'c': 'CC', |
| 'cpp': 'CXX', |
| 'cs': 'CSC', |
| 'cython': 'CYTHON', |
| 'd': 'DC', |
| 'fortran': 'FC', |
| 'objc': 'OBJC', |
| 'objcpp': 'OBJCXX', |
| 'rust': 'RUSTC', |
| 'vala': 'VALAC', |
| 'nasm': 'NASM', |
| |
| # Linkers |
| 'c_ld': 'CC_LD', |
| 'cpp_ld': 'CXX_LD', |
| 'd_ld': 'DC_LD', |
| 'fortran_ld': 'FC_LD', |
| 'objc_ld': 'OBJC_LD', |
| 'objcpp_ld': 'OBJCXX_LD', |
| 'rust_ld': 'RUSTC_LD', |
| } |
| |
| # Map from utility names to environment variables. |
| ENV_VAR_TOOL_MAP: T.Mapping[str, str] = { |
| # Binutils |
| 'ar': 'AR', |
| 'as': 'AS', |
| 'ld': 'LD', |
| 'nm': 'NM', |
| 'objcopy': 'OBJCOPY', |
| 'objdump': 'OBJDUMP', |
| 'ranlib': 'RANLIB', |
| 'readelf': 'READELF', |
| 'size': 'SIZE', |
| 'strings': 'STRINGS', |
| 'strip': 'STRIP', |
| 'windres': 'WINDRES', |
| |
| # Other tools |
| 'cmake': 'CMAKE', |
| 'qmake': 'QMAKE', |
| 'pkg-config': 'PKG_CONFIG', |
| 'make': 'MAKE', |
| 'vapigen': 'VAPIGEN', |
| 'llvm-config': 'LLVM_CONFIG', |
| } |
| |
| ENV_VAR_PROG_MAP = {**ENV_VAR_COMPILER_MAP, **ENV_VAR_TOOL_MAP} |
| |
| # Deprecated environment variables mapped from the new variable to the old one |
| # Deprecated in 0.54.0 |
| DEPRECATED_ENV_PROG_MAP: T.Mapping[str, str] = { |
| 'd_ld': 'D_LD', |
| 'fortran_ld': 'F_LD', |
| 'rust_ld': 'RUST_LD', |
| 'objcpp_ld': 'OBJCPP_LD', |
| } |
| |
| class CMakeSkipCompilerTest(Enum): |
| ALWAYS = 'always' |
| NEVER = 'never' |
| DEP_ONLY = 'dep_only' |
| |
| class Properties: |
| def __init__( |
| self, |
| properties: T.Optional[T.Dict[str, T.Optional[T.Union[str, bool, int, T.List[str]]]]] = None, |
| ): |
| self.properties = properties or {} |
| |
| def has_stdlib(self, language: str) -> bool: |
| return language + '_stdlib' in self.properties |
| |
| # Some of get_stdlib, get_root, get_sys_root are wider than is actually |
| # true, but without heterogeneous dict annotations it's not practical to |
| # narrow them |
| def get_stdlib(self, language: str) -> T.Union[str, T.List[str]]: |
| stdlib = self.properties[language + '_stdlib'] |
| if isinstance(stdlib, str): |
| return stdlib |
| assert isinstance(stdlib, list) |
| for i in stdlib: |
| assert isinstance(i, str) |
| return stdlib |
| |
| def get_root(self) -> T.Optional[str]: |
| root = self.properties.get('root', None) |
| assert root is None or isinstance(root, str) |
| return root |
| |
| def get_sys_root(self) -> T.Optional[str]: |
| sys_root = self.properties.get('sys_root', None) |
| assert sys_root is None or isinstance(sys_root, str) |
| return sys_root |
| |
| def get_pkg_config_libdir(self) -> T.Optional[T.List[str]]: |
| p = self.properties.get('pkg_config_libdir', None) |
| if p is None: |
| return p |
| res = mesonlib.listify(p) |
| for i in res: |
| assert isinstance(i, str) |
| return res |
| |
| def get_cmake_defaults(self) -> bool: |
| if 'cmake_defaults' not in self.properties: |
| return True |
| res = self.properties['cmake_defaults'] |
| assert isinstance(res, bool) |
| return res |
| |
| def get_cmake_toolchain_file(self) -> T.Optional[Path]: |
| if 'cmake_toolchain_file' not in self.properties: |
| return None |
| raw = self.properties['cmake_toolchain_file'] |
| assert isinstance(raw, str) |
| cmake_toolchain_file = Path(raw) |
| if not cmake_toolchain_file.is_absolute(): |
| raise EnvironmentException(f'cmake_toolchain_file ({raw}) is not absolute') |
| return cmake_toolchain_file |
| |
| def get_cmake_skip_compiler_test(self) -> CMakeSkipCompilerTest: |
| if 'cmake_skip_compiler_test' not in self.properties: |
| return CMakeSkipCompilerTest.DEP_ONLY |
| raw = self.properties['cmake_skip_compiler_test'] |
| assert isinstance(raw, str) |
| try: |
| return CMakeSkipCompilerTest(raw) |
| except ValueError: |
| raise EnvironmentException( |
| '"{}" is not a valid value for cmake_skip_compiler_test. Supported values are {}' |
| .format(raw, [e.value for e in CMakeSkipCompilerTest])) |
| |
| def get_cmake_use_exe_wrapper(self) -> bool: |
| if 'cmake_use_exe_wrapper' not in self.properties: |
| return True |
| res = self.properties['cmake_use_exe_wrapper'] |
| assert isinstance(res, bool) |
| return res |
| |
| def get_java_home(self) -> T.Optional[Path]: |
| value = T.cast('T.Optional[str]', self.properties.get('java_home')) |
| return Path(value) if value else None |
| |
| def get_bindgen_clang_args(self) -> T.List[str]: |
| value = mesonlib.listify(self.properties.get('bindgen_clang_arguments', [])) |
| if not all(isinstance(v, str) for v in value): |
| raise EnvironmentException('bindgen_clang_arguments must be a string or an array of strings') |
| return T.cast('T.List[str]', value) |
| |
| def __eq__(self, other: object) -> bool: |
| if isinstance(other, type(self)): |
| return self.properties == other.properties |
| return NotImplemented |
| |
| # TODO consider removing so Properties is less freeform |
| def __getitem__(self, key: str) -> T.Optional[T.Union[str, bool, int, T.List[str]]]: |
| return self.properties[key] |
| |
| # TODO consider removing so Properties is less freeform |
| def __contains__(self, item: T.Union[str, bool, int, T.List[str]]) -> bool: |
| return item in self.properties |
| |
| # TODO consider removing, for same reasons as above |
| 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]]]: |
| return self.properties.get(key, default) |
| |
| @dataclass(unsafe_hash=True) |
| class MachineInfo(HoldableObject): |
| system: str |
| cpu_family: str |
| cpu: str |
| endian: str |
| kernel: T.Optional[str] |
| subsystem: T.Optional[str] |
| |
| def __post_init__(self) -> None: |
| self.is_64_bit: bool = self.cpu_family in CPU_FAMILIES_64_BIT |
| |
| def __repr__(self) -> str: |
| return f'<MachineInfo: {self.system} {self.cpu_family} ({self.cpu})>' |
| |
| @classmethod |
| def from_literal(cls, literal: T.Dict[str, str]) -> 'MachineInfo': |
| minimum_literal = {'cpu', 'cpu_family', 'endian', 'system'} |
| if set(literal) < minimum_literal: |
| raise EnvironmentException( |
| f'Machine info is currently {literal}\n' + |
| 'but is missing {}.'.format(minimum_literal - set(literal))) |
| |
| cpu_family = literal['cpu_family'] |
| if cpu_family not in known_cpu_families: |
| mlog.warning(f'Unknown CPU family {cpu_family}, please report this at https://github.com/mesonbuild/meson/issues/new') |
| |
| endian = literal['endian'] |
| if endian not in ('little', 'big'): |
| mlog.warning(f'Unknown endian {endian}') |
| |
| system = literal['system'] |
| kernel = literal.get('kernel', None) |
| subsystem = literal.get('subsystem', None) |
| |
| return cls(system, cpu_family, literal['cpu'], endian, kernel, subsystem) |
| |
| def is_windows(self) -> bool: |
| """ |
| Machine is windows? |
| """ |
| return self.system == 'windows' |
| |
| def is_cygwin(self) -> bool: |
| """ |
| Machine is cygwin? |
| """ |
| return self.system == 'cygwin' |
| |
| def is_linux(self) -> bool: |
| """ |
| Machine is linux? |
| """ |
| return self.system == 'linux' |
| |
| def is_darwin(self) -> bool: |
| """ |
| Machine is Darwin (iOS/tvOS/OS X)? |
| """ |
| return self.system in {'darwin', 'ios', 'tvos'} |
| |
| def is_android(self) -> bool: |
| """ |
| Machine is Android? |
| """ |
| return self.system == 'android' |
| |
| def is_haiku(self) -> bool: |
| """ |
| Machine is Haiku? |
| """ |
| return self.system == 'haiku' |
| |
| def is_netbsd(self) -> bool: |
| """ |
| Machine is NetBSD? |
| """ |
| return self.system == 'netbsd' |
| |
| def is_openbsd(self) -> bool: |
| """ |
| Machine is OpenBSD? |
| """ |
| return self.system == 'openbsd' |
| |
| def is_dragonflybsd(self) -> bool: |
| """Machine is DragonflyBSD?""" |
| return self.system == 'dragonfly' |
| |
| def is_freebsd(self) -> bool: |
| """Machine is FreeBSD?""" |
| return self.system == 'freebsd' |
| |
| def is_sunos(self) -> bool: |
| """Machine is illumos or Solaris?""" |
| return self.system == 'sunos' |
| |
| def is_hurd(self) -> bool: |
| """ |
| Machine is GNU/Hurd? |
| """ |
| return self.system == 'gnu' |
| |
| def is_aix(self) -> bool: |
| """ |
| Machine is aix? |
| """ |
| return self.system == 'aix' |
| |
| def is_irix(self) -> bool: |
| """Machine is IRIX?""" |
| return self.system.startswith('irix') |
| |
| # Various prefixes and suffixes for import libraries, shared libraries, |
| # static libraries, and executables. |
| # Versioning is added to these names in the backends as-needed. |
| def get_exe_suffix(self) -> str: |
| if self.is_windows() or self.is_cygwin(): |
| return 'exe' |
| else: |
| return '' |
| |
| def get_object_suffix(self) -> str: |
| if self.is_windows(): |
| return 'obj' |
| else: |
| return 'o' |
| |
| def libdir_layout_is_win(self) -> bool: |
| return self.is_windows() or self.is_cygwin() |
| |
| class BinaryTable: |
| |
| def __init__( |
| self, |
| binaries: T.Optional[T.Dict[str, T.Union[str, T.List[str]]]] = None, |
| ): |
| self.binaries: T.Dict[str, T.List[str]] = {} |
| if binaries: |
| for name, command in binaries.items(): |
| if not isinstance(command, (list, str)): |
| raise mesonlib.MesonException( |
| f'Invalid type {command!r} for entry {name!r} in cross file') |
| self.binaries[name] = mesonlib.listify(command) |
| if 'pkgconfig' in self.binaries: |
| if 'pkg-config' not in self.binaries: |
| mlog.deprecation('"pkgconfig" entry is deprecated and should be replaced by "pkg-config"', fatal=False) |
| self.binaries['pkg-config'] = self.binaries['pkgconfig'] |
| elif self.binaries['pkgconfig'] != self.binaries['pkg-config']: |
| raise mesonlib.MesonException('Mismatched pkgconfig and pkg-config binaries in the machine file.') |
| else: |
| # Both are defined with the same value, this is allowed |
| # for backward compatibility. |
| # FIXME: We should still print deprecation warning if the |
| # project targets Meson >= 1.3.0, but we have no way to know |
| # that here. |
| pass |
| del self.binaries['pkgconfig'] |
| |
| @staticmethod |
| def detect_ccache() -> T.List[str]: |
| try: |
| subprocess.check_call(['ccache', '--version'], stdout=subprocess.PIPE, stderr=subprocess.PIPE) |
| except (OSError, subprocess.CalledProcessError): |
| return [] |
| return ['ccache'] |
| |
| @staticmethod |
| def detect_sccache() -> T.List[str]: |
| try: |
| subprocess.check_call(['sccache', '--version'], stdout=subprocess.PIPE, stderr=subprocess.PIPE) |
| except (OSError, subprocess.CalledProcessError): |
| return [] |
| return ['sccache'] |
| |
| @staticmethod |
| def detect_compiler_cache() -> T.List[str]: |
| # Sccache is "newer" so it is assumed that people would prefer it by default. |
| cache = BinaryTable.detect_sccache() |
| if cache: |
| return cache |
| return BinaryTable.detect_ccache() |
| |
| @classmethod |
| def parse_entry(cls, entry: T.Union[str, T.List[str]]) -> T.Tuple[T.List[str], T.List[str]]: |
| compiler = mesonlib.stringlistify(entry) |
| # Ensure ccache exists and remove it if it doesn't |
| if compiler[0] == 'ccache': |
| compiler = compiler[1:] |
| ccache = cls.detect_ccache() |
| elif compiler[0] == 'sccache': |
| compiler = compiler[1:] |
| ccache = cls.detect_sccache() |
| else: |
| ccache = [] |
| # Return value has to be a list of compiler 'choices' |
| return compiler, ccache |
| |
| def lookup_entry(self, name: str) -> T.Optional[T.List[str]]: |
| """Lookup binary in cross/native file and fallback to environment. |
| |
| Returns command with args as list if found, Returns `None` if nothing is |
| found. |
| """ |
| command = self.binaries.get(name) |
| if not command: |
| return None |
| elif not command[0].strip(): |
| return None |
| return command |
| |
| class CMakeVariables: |
| def __init__(self, variables: T.Optional[T.Dict[str, T.Any]] = None) -> None: |
| variables = variables or {} |
| self.variables: T.Dict[str, T.List[str]] = {} |
| |
| for key, value in variables.items(): |
| value = mesonlib.listify(value) |
| for i in value: |
| if not isinstance(i, str): |
| raise EnvironmentException(f"Value '{i}' of CMake variable '{key}' defined in a machine file is a {type(i).__name__} and not a str") |
| self.variables[key] = value |
| |
| def get_variables(self) -> T.Dict[str, T.List[str]]: |
| return self.variables |