blob: 86bad9be23ee662077d6e8be302a12946ae7d834 [file] [log] [blame]
# 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