# SPDX-License-Identifier: Apache-2.0 # Copyright 2013-2025 The Meson development team # Copyright © 2023-2025 Intel Corporation from __future__ import annotations import copy from . import mlog, options import pickle, os, uuid import sys from functools import lru_cache from collections import OrderedDict import textwrap from .mesonlib import ( MesonException, MachineChoice, PerMachine, PerMachineDefaultable, pickle_load ) from .options import OptionKey import enum import typing as T if T.TYPE_CHECKING: from . import dependencies from .compilers.compilers import Compiler, CompileResult, RunResult, CompileCheckMode from .dependencies.detect import TV_DepID from .mesonlib import FileOrString from .cmake.traceparser import CMakeCacheEntry from .interpreterbase import SubProject from .options import ElementaryOptionValues, MutableKeyedOptionDictType from .build import BuildTarget from .cmdline import SharedCMDOptions OptionDictType = T.Dict[str, options.AnyOptionType] CompilerCheckCacheKey = T.Tuple[T.Tuple[str, ...], str, FileOrString, T.Tuple[str, ...], CompileCheckMode] # code, args RunCheckCacheKey = T.Tuple[str, T.Tuple[str, ...]] # Check major_versions_differ() if changing versioning scheme. # # Pip requires that RCs are named like this: '0.1.0.rc1' # But the corresponding Git tag needs to be '0.1.0rc1' version = '1.10.99' # The next stable version when we are in dev. This is used to allow projects to # require meson version >=1.2.0 when using 1.1.99. FeatureNew won't warn when # using a feature introduced in 1.2.0 when using Meson 1.1.99. stable_version = version if stable_version.endswith('.99'): stable_version_array = stable_version.split('.') stable_version_array[-1] = '0' stable_version_array[-2] = str(int(stable_version_array[-2]) + 1) stable_version = '.'.join(stable_version_array) def get_genvs_default_buildtype_list() -> list[str]: # just debug, debugoptimized, and release for now # but this should probably be configurable through some extra option, alongside --genvslite. return options.buildtypelist[1:-2] class MesonVersionMismatchException(MesonException): '''Build directory generated with Meson version is incompatible with current version''' def __init__(self, old_version: str, current_version: str, extra_msg: str = '') -> None: super().__init__(f'Build directory has been generated with Meson version {old_version}, ' f'which is incompatible with the current version {current_version}.' + extra_msg) self.old_version = old_version self.current_version = current_version class DependencyCacheType(enum.Enum): OTHER = 0 PKG_CONFIG = 1 CMAKE = 2 @classmethod def from_type(cls, dep: 'dependencies.Dependency') -> 'DependencyCacheType': # As more types gain search overrides they'll need to be added here if dep.type_name == 'pkgconfig': return cls.PKG_CONFIG if dep.type_name == 'cmake': return cls.CMAKE return cls.OTHER class DependencySubCache: def __init__(self, type_: DependencyCacheType): self.types = [type_] self.__cache: T.Dict[T.Tuple[str, ...], 'dependencies.Dependency'] = {} def __getitem__(self, key: T.Tuple[str, ...]) -> 'dependencies.Dependency': return self.__cache[key] def __setitem__(self, key: T.Tuple[str, ...], value: 'dependencies.Dependency') -> None: self.__cache[key] = value def __contains__(self, key: T.Tuple[str, ...]) -> bool: return key in self.__cache def values(self) -> T.Iterable['dependencies.Dependency']: return self.__cache.values() class DependencyCache: """Class that stores a cache of dependencies. This class is meant to encapsulate the fact that we need multiple keys to successfully lookup by providing a simple get/put interface. """ def __init__(self, builtins: options.OptionStore, for_machine: MachineChoice): self.__cache: T.MutableMapping[TV_DepID, DependencySubCache] = OrderedDict() self.__builtins = builtins self.__pkg_conf_key = options.OptionKey('pkg_config_path', machine=for_machine) self.__cmake_key = options.OptionKey('cmake_prefix_path', machine=for_machine) def __calculate_subkey(self, type_: DependencyCacheType) -> T.Tuple[str, ...]: data: T.Dict[DependencyCacheType, T.List[str]] = { DependencyCacheType.PKG_CONFIG: T.cast('T.List[str]', self.__builtins.get_value_for(self.__pkg_conf_key)), DependencyCacheType.CMAKE: T.cast('T.List[str]', self.__builtins.get_value_for(self.__cmake_key)), DependencyCacheType.OTHER: [], } assert type_ in data, 'Someone forgot to update subkey calculations for a new type' return tuple(data[type_]) def __iter__(self) -> T.Iterator['TV_DepID']: return self.keys() def put(self, key: 'TV_DepID', dep: 'dependencies.Dependency') -> None: t = DependencyCacheType.from_type(dep) if key not in self.__cache: self.__cache[key] = DependencySubCache(t) subkey = self.__calculate_subkey(t) self.__cache[key][subkey] = dep def get(self, key: 'TV_DepID') -> T.Optional['dependencies.Dependency']: """Get a value from the cache. If there is no cache entry then None will be returned. """ try: val = self.__cache[key] except KeyError: return None for t in val.types: subkey = self.__calculate_subkey(t) try: return val[subkey] except KeyError: pass return None def values(self) -> T.Iterator['dependencies.Dependency']: for c in self.__cache.values(): yield from c.values() def keys(self) -> T.Iterator['TV_DepID']: return iter(self.__cache.keys()) def items(self) -> T.Iterator[T.Tuple['TV_DepID', T.List['dependencies.Dependency']]]: for k, v in self.__cache.items(): vs: T.List[dependencies.Dependency] = [] for t in v.types: subkey = self.__calculate_subkey(t) if subkey in v: vs.append(v[subkey]) yield k, vs def clear(self) -> None: self.__cache.clear() class CMakeStateCache: """Class that stores internal CMake compiler states. This cache is used to reduce the startup overhead of CMake by caching all internal CMake compiler variables. """ def __init__(self) -> None: self.__cache: T.Dict[str, T.Dict[str, T.List[str]]] = {} self.cmake_cache: T.Dict[str, 'CMakeCacheEntry'] = {} def __iter__(self) -> T.Iterator[T.Tuple[str, T.Dict[str, T.List[str]]]]: return iter(self.__cache.items()) def items(self) -> T.Iterator[T.Tuple[str, T.Dict[str, T.List[str]]]]: return iter(self.__cache.items()) def update(self, language: str, variables: T.Dict[str, T.List[str]]) -> None: if language not in self.__cache: self.__cache[language] = {} self.__cache[language].update(variables) @property def languages(self) -> T.Set[str]: return set(self.__cache.keys()) # Can't bind this near the class method it seems, sadly. _V = T.TypeVar('_V') # This class contains all data that must persist over multiple # invocations of Meson. It is roughly the same thing as # cmakecache. class CoreData: def __init__(self, cmd_options: SharedCMDOptions, scratch_dir: str, meson_command: T.List[str]): self.lang_guids = { 'default': '8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942', 'c': '8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942', 'cpp': '8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942', 'masm': '8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942', 'test': '3AC096D0-A1C2-E12C-1390-A8335801FDAB', 'directory': '2150E333-8FDC-42A3-9474-1A3956D46DE8', } self.test_guid = str(uuid.uuid4()).upper() self.regen_guid = str(uuid.uuid4()).upper() self.install_guid = str(uuid.uuid4()).upper() self.meson_command = meson_command self.target_guids: T.Dict[str, str] = {} self.version = version self.cross_files = self.__load_config_files(cmd_options, scratch_dir, 'cross') self.compilers: PerMachine[T.Dict[str, Compiler]] = PerMachine(OrderedDict(), OrderedDict()) self.optstore = options.OptionStore(self.is_cross_build()) # Stores the (name, hash) of the options file, The name will be either # "meson_options.txt" or "meson.options". # This is used by mconf to reload the option file if it's changed. self.options_files: T.Dict[SubProject, T.Optional[T.Tuple[str, str]]] = {} # Set of subprojects that have already been initialized once, this is # required to be stored and reloaded with the coredata, as we don't # want to overwrite options for such subprojects. self.initialized_subprojects: T.Set[str] = set() # For host == build configurations these caches should be the same. self.deps: PerMachine[DependencyCache] = PerMachineDefaultable.default( self.is_cross_build(), DependencyCache(self.optstore, MachineChoice.BUILD), DependencyCache(self.optstore, MachineChoice.HOST)) self.compiler_check_cache: T.Dict['CompilerCheckCacheKey', 'CompileResult'] = OrderedDict() self.run_check_cache: T.Dict['RunCheckCacheKey', 'RunResult'] = OrderedDict() # CMake cache self.cmake_cache: PerMachine[CMakeStateCache] = PerMachine(CMakeStateCache(), CMakeStateCache()) # Only to print a warning if it changes between Meson invocations. self.config_files = self.__load_config_files(cmd_options, scratch_dir, 'native') self.builtin_options_libdir_cross_fixup() self.optstore.init_builtins() @staticmethod def __load_config_files(cmd_options: SharedCMDOptions, scratch_dir: str, ftype: str) -> T.List[str]: # Need to try and make the passed filenames absolute because when the # files are parsed later we'll have chdir()d. if ftype == 'cross': filenames = cmd_options.cross_file else: filenames = cmd_options.native_file if not filenames: return [] found_invalid: T.List[str] = [] missing: T.List[str] = [] real: T.List[str] = [] for i, f in enumerate(filenames): f = os.path.expanduser(os.path.expandvars(f)) if os.path.exists(f): if os.path.isfile(f): real.append(os.path.abspath(f)) continue elif os.path.isdir(f): found_invalid.append(os.path.abspath(f)) else: # in this case we've been passed some kind of pipe, copy # the contents of that file into the meson private (scratch) # directory so that it can be re-read when wiping/reconfiguring fcopy = os.path.join(scratch_dir, f'{uuid.uuid4()}.{ftype}.ini') with open(f, encoding='utf-8') as rf: with open(fcopy, 'w', encoding='utf-8') as wf: wf.write(rf.read()) real.append(fcopy) # Also replace the command line argument, as the pipe # probably won't exist on reconfigure filenames[i] = fcopy continue if sys.platform != 'win32': paths = [ os.environ.get('XDG_DATA_HOME', os.path.expanduser('~/.local/share')), ] + os.environ.get('XDG_DATA_DIRS', '/usr/local/share:/usr/share').split(':') for path in paths: path_to_try = os.path.join(path, 'meson', ftype, f) if os.path.isfile(path_to_try): real.append(path_to_try) break else: missing.append(f) else: missing.append(f) if missing: if found_invalid: mlog.log('Found invalid candidates for', ftype, 'file:', *found_invalid) mlog.log('Could not find any valid candidate for', ftype, 'files:', *missing) raise MesonException(f'Cannot find specified {ftype} file: {f}') return real def builtin_options_libdir_cross_fixup(self) -> None: # By default set libdir to "lib" when cross compiling since # getting the "system default" is always wrong on multiarch # platforms as it gets a value like lib/x86_64-linux-gnu. if self.cross_files: options.BUILTIN_OPTIONS[OptionKey('libdir')].default = 'lib' def init_backend_options(self, backend_name: str) -> None: if backend_name == 'ninja': self.optstore.add_system_option('backend_max_links', options.UserIntegerOption( 'backend_max_links', 'Maximum number of linker processes to run or 0 for no ' 'limit', 0, min_value=0)) elif backend_name.startswith('vs'): self.optstore.add_system_option('backend_startup_project', options.UserStringOption( 'backend_startup_project', 'Default project to execute in Visual Studio', '')) def get_option_for_target(self, target: 'BuildTarget', key: T.Union[str, OptionKey]) -> ElementaryOptionValues: if isinstance(key, str): assert ':' not in key newkey = OptionKey(key, target.subproject) else: newkey = key if newkey.subproject != target.subproject: # FIXME: this should be an error. The caller needs to ensure that # key and target have the same subproject for consistency. # Now just do this to get things going. newkey = newkey.evolve(subproject=target.subproject) if self.is_cross_build(): newkey = newkey.evolve(machine=target.for_machine) option_object, value = self.optstore.get_option_and_value_for(newkey) override = target.get_override(newkey.name) if override is not None: return option_object.validate_value(override) return value def set_from_configure_command(self, options: SharedCMDOptions) -> bool: return self.optstore.set_from_configure_command(options.cmd_line_options) def clear_cache(self) -> None: self.deps.host.clear() self.deps.build.clear() self.compiler_check_cache.clear() self.run_check_cache.clear() def get_nondefault_buildtype_args(self) -> T.List[T.Union[T.Tuple[str, str, str], T.Tuple[str, bool, bool]]]: result: T.List[T.Union[T.Tuple[str, str, str], T.Tuple[str, bool, bool]]] = [] value = self.optstore.get_value_for('buildtype') if value == 'plain': opt = 'plain' debug = False elif value == 'debug': opt = '0' debug = True elif value == 'debugoptimized': opt = '2' debug = True elif value == 'release': opt = '3' debug = False elif value == 'minsize': opt = 's' debug = True else: assert value == 'custom' return [] actual_opt = self.optstore.get_value_for('optimization') actual_debug = self.optstore.get_value_for('debug') assert isinstance(actual_opt, str) # for mypy assert isinstance(actual_debug, bool) # for mypy if actual_opt != opt: result.append(('optimization', actual_opt, opt)) if actual_debug != debug: result.append(('debug', actual_debug, debug)) return result def get_external_args(self, for_machine: MachineChoice, lang: str) -> T.List[str]: # mypy cannot analyze type of OptionKey key = OptionKey(f'{lang}_args', machine=for_machine) return T.cast('T.List[str]', self.optstore.get_value_for(key)) @lru_cache(maxsize=None) def get_external_link_args(self, for_machine: MachineChoice, lang: str) -> T.List[str]: # mypy cannot analyze type of OptionKey linkkey = OptionKey(f'{lang}_link_args', machine=for_machine) return T.cast('T.List[str]', self.optstore.get_value_for(linkkey)) def is_cross_build(self, when_building_for: MachineChoice = MachineChoice.HOST) -> bool: if when_building_for == MachineChoice.BUILD: return False return len(self.cross_files) > 0 def add_compiler_options(self, c_options: MutableKeyedOptionDictType, lang: str, for_machine: MachineChoice, subproject: str) -> None: for k, o in c_options.items(): assert k.subproject is None and k.machine is for_machine if subproject: k = k.evolve(subproject=subproject) if lang == 'objc' and k.name == 'c_std': # For objective C, always fall back to c_std. self.optstore.add_compiler_option('c', k, o) elif lang == 'objcpp' and k.name == 'cpp_std': self.optstore.add_compiler_option('cpp', k, o) else: self.optstore.add_compiler_option(lang, k, o) def process_compiler_options(self, lang: str, comp: Compiler, subproject: str) -> None: self.add_compiler_options(comp.get_options(), lang, comp.for_machine, subproject) for key in [OptionKey(f'{lang}_args'), OptionKey(f'{lang}_link_args')]: if self.is_cross_build(): key = key.evolve(machine=comp.for_machine) # the global option is already there, but any augment is still # sitting in pending_options has to be taken into account assert key in self.optstore if subproject: skey = key.evolve(subproject=subproject) self.optstore.add_compiler_option(lang, skey, self.optstore.get_value_object(key)) for key in comp.base_options: if subproject: skey = key.evolve(subproject=subproject) else: skey = key if skey not in self.optstore: self.optstore.add_system_option(skey, copy.deepcopy(options.COMPILER_BASE_OPTIONS[key])) comp.init_from_options() self.emit_base_options_warnings() def emit_base_options_warnings(self) -> None: bcodekey = OptionKey('b_bitcode') if bcodekey in self.optstore and self.optstore.get_value_for(bcodekey): msg = textwrap.dedent('''Base option 'b_bitcode' is enabled, which is incompatible with many linker options. Incompatible options such as \'b_asneeded\' have been disabled.' Please see https://mesonbuild.com/Builtin-options.html#Notes_about_Apple_Bitcode_support for more details.''') mlog.warning(msg, once=True, fatal=False) def major_versions_differ(v1: str, v2: str) -> bool: v1_major, v1_minor = v1.rsplit('.', 1) v2_major, v2_minor = v2.rsplit('.', 1) # Major version differ, or one is development version but not the other. return v1_major != v2_major or ('99' in {v1_minor, v2_minor} and v1_minor != v2_minor) def load(build_dir: str, suggest_reconfigure: bool = True) -> CoreData: filename = os.path.join(build_dir, 'meson-private', 'coredata.dat') return pickle_load(filename, 'Coredata', CoreData, suggest_reconfigure) def save(obj: CoreData, build_dir: str) -> str: filename = os.path.join(build_dir, 'meson-private', 'coredata.dat') prev_filename = filename + '.prev' tempfilename = filename + '~' if major_versions_differ(obj.version, version): raise MesonException('Fatal version mismatch corruption.') if os.path.exists(filename): import shutil shutil.copyfile(filename, prev_filename) with open(tempfilename, 'wb') as f: pickle.dump(obj, f) f.flush() os.fsync(f.fileno()) os.replace(tempfilename, filename) return filename FORBIDDEN_TARGET_NAMES = frozenset({ 'clean', 'clean-ctlist', 'clean-gcno', 'clean-gcda', 'coverage', 'coverage-text', 'coverage-xml', 'coverage-html', 'phony', 'PHONY', 'all', 'test', 'benchmark', 'install', 'uninstall', 'build.ninja', 'scan-build', 'reconfigure', 'dist', 'distcheck', })