# SPDX-License-Identifier: Apache-2.0 # Copyright 2012-2020 The Meson development team # Copyright © 2023-2025 Intel Corporation from __future__ import annotations import itertools import os, re import typing as T import collections from . import cmdline from . import coredata from . import mesonlib from . import machinefile from . import options from .mesonlib import ( MesonException, MachineChoice, Popen_safe, PerMachine, PerMachineDefaultable, PerThreeMachineDefaultable, split_args, MesonBugException ) from .options import OptionKey from . import mlog from .programs import ExternalProgram from .envconfig import ( BinaryTable, MachineInfo, Properties, CMakeVariables, detect_machine_info, machine_info_can_run ) from . import compilers from mesonbuild import envconfig if T.TYPE_CHECKING: from .compilers import Compiler from .options import OptionDict, ElementaryOptionValues from .wrap.wrap import Resolver NON_LANG_ENV_OPTIONS = [ ('PKG_CONFIG_PATH', 'pkg_config_path'), ('CMAKE_PREFIX_PATH', 'cmake_prefix_path'), ('LDFLAGS', 'ldflags'), ('CPPFLAGS', 'cppflags'), ] build_filename = 'meson.build' def _as_str(val: object) -> str: assert isinstance(val, str), 'for mypy' return val def _get_env_var(for_machine: MachineChoice, is_cross: bool, var_name: str) -> T.Optional[str]: """ Returns the exact env var and the value. """ candidates = PerMachine( # The prefixed build version takes priority, but if we are native # compiling we fall back on the unprefixed host version. This # allows native builds to never need to worry about the 'BUILD_*' # ones. ([var_name + '_FOR_BUILD'] if is_cross else [var_name]), # Always just the unprefixed host versions [var_name] )[for_machine] for var in candidates: value = os.environ.get(var) if value is not None: break else: formatted = ', '.join([f'{var!r}' for var in candidates]) mlog.debug(f'None of {formatted} are defined in the environment, not changing global flags.') return None mlog.debug(f'Using {var!r} from environment with value: {value!r}') return value class Environment: private_dir = 'meson-private' log_dir = 'meson-logs' info_dir = 'meson-info' def __init__(self, source_dir: str, build_dir: T.Optional[str], cmd_options: cmdline.SharedCMDOptions) -> None: self.source_dir = source_dir # Do not try to create build directories when build_dir is none. # This reduced mode is used by the --buildoptions introspector if build_dir is not None: self.build_dir = build_dir self.scratch_dir = os.path.join(build_dir, Environment.private_dir) self.log_dir = os.path.join(build_dir, Environment.log_dir) self.info_dir = os.path.join(build_dir, Environment.info_dir) os.makedirs(self.scratch_dir, exist_ok=True) os.makedirs(self.log_dir, exist_ok=True) os.makedirs(self.info_dir, exist_ok=True) try: self.coredata: coredata.CoreData = coredata.load(self.get_build_dir(), suggest_reconfigure=False) self.first_invocation = False except FileNotFoundError: self.create_new_coredata(cmd_options) except coredata.MesonVersionMismatchException as e: # This is routine, but tell the user the update happened mlog.log('Regenerating configuration from scratch:', str(e)) cmdline.read_cmd_line_file(self.build_dir, cmd_options) self.create_new_coredata(cmd_options) except MesonException as e: # If we stored previous command line options, we can recover from # a broken/outdated coredata. if os.path.isfile(cmdline.get_cmd_line_file(self.build_dir)): mlog.warning('Regenerating configuration from scratch.', fatal=False) mlog.log('Reason:', mlog.red(str(e))) cmdline.read_cmd_line_file(self.build_dir, cmd_options) self.create_new_coredata(cmd_options) else: raise MesonException(f'{str(e)} Try regenerating using "meson setup --wipe".') else: # Just create a fresh coredata in this case self.build_dir = '' self.scratch_dir = '' self.create_new_coredata(cmd_options) ## locally bind some unfrozen configuration # Stores machine infos, the only *three* machine one because we have a # target machine info on for the user (Meson never cares about the # target machine.) machines: PerThreeMachineDefaultable[MachineInfo] = PerThreeMachineDefaultable() # Similar to coredata.compilers, but lower level in that there is no # meta data, only names/paths. binaries: PerMachineDefaultable[BinaryTable] = PerMachineDefaultable() # Misc other properties about each machine. properties: PerMachineDefaultable[Properties] = PerMachineDefaultable() # CMake toolchain variables cmakevars: PerMachineDefaultable[CMakeVariables] = PerMachineDefaultable() ## Setup build machine defaults # Will be fully initialized later using compilers later. machines.build = detect_machine_info() # Just uses hard-coded defaults and environment variables. Might be # overwritten by a native file. binaries.build = BinaryTable() properties.build = Properties() # Options with the key parsed into an OptionKey type. # # Note that order matters because of 'buildtype', if it is after # 'optimization' and 'debug' keys, it override them. self.options: OptionDict = collections.OrderedDict() # Environment variables with the name converted into an OptionKey type. # These have subtly different behavior compared to machine files, so do # not store them in self.options. See _set_default_options_from_env. self.env_opts: OptionDict = {} self.machinestore = machinefile.MachineFileStore(self.coredata.config_files, self.coredata.cross_files, self.source_dir) ## Read in native file(s) to override build machine configuration if self.coredata.config_files is not None: config = machinefile.parse_machine_files(self.coredata.config_files, self.source_dir) binaries.build = BinaryTable(config.get('binaries', {})) properties.build = Properties(config.get('properties', {})) cmakevars.build = CMakeVariables(config.get('cmake', {})) self._load_machine_file_options( config, properties.build, MachineChoice.BUILD if self.coredata.cross_files else MachineChoice.HOST) ## Read in cross file(s) to override host machine configuration if self.coredata.cross_files: config = machinefile.parse_machine_files(self.coredata.cross_files, self.source_dir) properties.host = Properties(config.get('properties', {})) binaries.host = BinaryTable(config.get('binaries', {})) cmakevars.host = CMakeVariables(config.get('cmake', {})) if 'host_machine' in config: machines.host = MachineInfo.from_literal(config['host_machine']) if 'target_machine' in config: machines.target = MachineInfo.from_literal(config['target_machine']) # Keep only per machine options from the native file. The cross # file takes precedence over all other options. for key, value in list(self.options.items()): if self.coredata.optstore.is_per_machine_option(key): self.options[key.as_build()] = value self._load_machine_file_options(config, properties.host, MachineChoice.HOST) ## "freeze" now initialized configuration, and "save" to the class. self.machines = machines.default_missing() self.binaries = binaries.default_missing() self.properties = properties.default_missing() self.cmakevars = cmakevars.default_missing() # Take default value from env if not set in cross/native files or command line. self._set_default_options_from_env() self._set_default_binaries_from_env() self._set_default_properties_from_env() # Warn if the user is using two different ways of setting build-type # options that override each other bt = OptionKey('buildtype') db = OptionKey('debug') op = OptionKey('optimization') if bt in self.options and (db in self.options or op in self.options): mlog.warning('Recommend using either -Dbuildtype or -Doptimization + -Ddebug. ' 'Using both is redundant since they override each other. ' 'See: https://mesonbuild.com/Builtin-options.html#build-type-options', fatal=False) # Filter out build machine options that are not valid per-project. # We allow this in the file because it makes the machine files more # useful (ie, the same file can be used for host == build configuration # a host != build configuration) self.options = {k: v for k, v in self.options.items() if k.machine is MachineChoice.HOST or self.coredata.optstore.is_per_machine_option(k)} exe_wrapper = self.lookup_binary_entry(MachineChoice.HOST, 'exe_wrapper') if exe_wrapper is not None: self.exe_wrapper = ExternalProgram.from_bin_list(self, MachineChoice.HOST, 'exe_wrapper') else: self.exe_wrapper = None self.default_cmake = ['cmake'] self.default_pkgconfig = ['pkg-config'] self.wrap_resolver: T.Optional['Resolver'] = None def mfilestr2key(self, machine_file_string: str, section: T.Optional[str], section_subproject: T.Optional[str], machine: MachineChoice) -> OptionKey: key = OptionKey.from_string(machine_file_string) if key.subproject: suggestion = section if section == 'project options' else 'built-in options' raise MesonException(f'Do not set subproject options in [{section}] section, use [subproject:{suggestion}] instead.') if section_subproject: key = key.evolve(subproject=section_subproject) if machine == MachineChoice.BUILD: if key.machine == MachineChoice.BUILD: mlog.deprecation('Setting build machine options in the native file does not need the "build." prefix', once=True) return key.evolve(machine=machine) return key def _load_machine_file_options(self, config: T.Mapping[str, T.Mapping[str, ElementaryOptionValues]], properties: Properties, machine: MachineChoice) -> None: """Read the contents of a Machine file and put it in the options store.""" # Look for any options in the deprecated paths section, warn about # those, then assign them. They will be overwritten by the ones in the # "built-in options" section if they're in both sections. paths = config.get('paths') if paths: mlog.deprecation('The [paths] section is deprecated, use the [built-in options] section instead.') for strk, v in paths.items(): k = self.mfilestr2key(strk, 'paths', None, machine) self.options[k] = v # Next look for compiler options in the "properties" section, this is # also deprecated, and these will also be overwritten by the "built-in # options" section. We need to remove these from this section, as well. deprecated_properties: T.Set[str] = set() for lang in compilers.all_languages: deprecated_properties.add(lang + '_args') deprecated_properties.add(lang + '_link_args') for strk, v in properties.properties.copy().items(): if strk in deprecated_properties: mlog.deprecation(f'{strk} in the [properties] section of the machine file is deprecated, use the [built-in options] section.') k = self.mfilestr2key(strk, 'properties', None, machine) self.options[k] = v del properties.properties[strk] for section, values in config.items(): if ':' in section: section_subproject, section = section.split(':', 1) else: section_subproject = '' if section == 'built-in options': for strk, v in values.items(): key = self.mfilestr2key(strk, section, section_subproject, machine) # If we're in the cross file, and there is a `build.foo` warn about that. Later we'll remove it. if machine is MachineChoice.HOST and key.machine is not machine: mlog.deprecation('Setting build machine options in cross files, please use a native file instead, this will be removed in meson 2.0', once=True) self.options[key] = v elif section == 'project options' and machine is MachineChoice.HOST: # Project options are only for the host machine, we don't want # to read these from the native file for strk, v in values.items(): # Project options are always for the host machine key = self.mfilestr2key(strk, section, section_subproject, machine) self.options[key] = v elif ':' in section: correct_subproject, correct_section = section.split(':')[-2:] raise MesonException( 'Subproject options should always be set as ' '`[subproject:section]`, even if the options are from a ' 'nested subproject. ' f'Replace `[{section_subproject}:{section}]` with `[{correct_subproject}:{correct_section}]`') def _set_default_options_from_env(self) -> None: opts: T.List[T.Tuple[str, str]] = ( [(v, f'{k}_args') for k, v in compilers.compilers.CFLAGS_MAPPING.items()] + NON_LANG_ENV_OPTIONS ) env_opts: T.DefaultDict[OptionKey, T.List[str]] = collections.defaultdict(list) for (evar, keyname), for_machine in itertools.product(opts, MachineChoice): p_env = _get_env_var(for_machine, self.is_cross_build(), evar) if p_env is not None: # these may contain duplicates, which must be removed, else # a duplicates-in-array-option warning arises. if keyname == 'cmake_prefix_path': if self.machines[for_machine].is_windows(): # Cannot split on ':' on Windows because its in the drive letter _p_env = p_env.split(os.pathsep) else: # https://github.com/mesonbuild/meson/issues/7294 _p_env = re.split(r':|;', p_env) p_list = list(mesonlib.OrderedSet(_p_env)) elif keyname == 'pkg_config_path': p_list = list(mesonlib.OrderedSet(p_env.split(os.pathsep))) else: p_list = split_args(p_env) p_list = [e for e in p_list if e] # filter out any empty elements # Take env vars only on first invocation, if the env changes when # reconfiguring it gets ignored. # FIXME: We should remember if we took the value from env to warn # if it changes on future invocations. if self.first_invocation: if keyname == 'ldflags': for lang in compilers.compilers.LANGUAGES_USING_LDFLAGS: key = OptionKey(name=f'{lang}_link_args', machine=for_machine) env_opts[key].extend(p_list) elif keyname == 'cppflags': for lang in compilers.compilers.LANGUAGES_USING_CPPFLAGS: key = OptionKey(f'{lang}_args', machine=for_machine) env_opts[key].extend(p_list) else: key = OptionKey.from_string(keyname).evolve(machine=for_machine) env_opts[key].extend(p_list) # If this is an environment variable, we have to # store it separately until the compiler is # instantiated, as we don't know whether the # compiler will want to use these arguments at link # time and compile time (instead of just at compile # time) until we're instantiating that `Compiler` # object. This is required so that passing # `-Dc_args=` on the command line and `$CFLAGS` # have subtly different behavior. `$CFLAGS` will be # added to the linker command line if the compiler # acts as a linker driver, `-Dc_args` will not. for (_, keyname), for_machine in itertools.product(NON_LANG_ENV_OPTIONS, MachineChoice): key = OptionKey.from_string(keyname).evolve(machine=for_machine) # Only store options that are not already in self.options, # otherwise we'd override the machine files if key in env_opts and key not in self.options: self.options[key] = env_opts[key] del env_opts[key] self.env_opts.update(env_opts) def _set_default_binaries_from_env(self) -> None: """Set default binaries from the environment. For example, pkg-config can be set via PKG_CONFIG, or in the machine file. We want to set the default to the env variable. """ opts = itertools.chain(envconfig.DEPRECATED_ENV_PROG_MAP.items(), envconfig.ENV_VAR_PROG_MAP.items()) for (name, evar), for_machine in itertools.product(opts, MachineChoice): p_env = _get_env_var(for_machine, self.is_cross_build(), evar) if p_env is not None: if os.path.exists(p_env): self.binaries[for_machine].binaries.setdefault(name, [p_env]) else: self.binaries[for_machine].binaries.setdefault(name, mesonlib.split_args(p_env)) def _set_default_properties_from_env(self) -> None: """Properties which can also be set from the environment.""" # name, evar, split opts: T.List[T.Tuple[str, T.List[str], bool]] = [ ('boost_includedir', ['BOOST_INCLUDEDIR'], False), ('boost_librarydir', ['BOOST_LIBRARYDIR'], False), ('boost_root', ['BOOST_ROOT', 'BOOSTROOT'], True), ('java_home', ['JAVA_HOME'], False), ] for (name, evars, split), for_machine in itertools.product(opts, MachineChoice): for evar in evars: p_env = _get_env_var(for_machine, self.is_cross_build(), evar) if p_env is not None: if split: self.properties[for_machine].properties.setdefault(name, p_env.split(os.pathsep)) else: self.properties[for_machine].properties.setdefault(name, p_env) break def create_new_coredata(self, options: cmdline.SharedCMDOptions) -> None: # WARNING: Don't use any values from coredata in __init__. It gets # re-initialized with project options by the interpreter during # build file parsing. # meson_command is used by the regenchecker script, which runs meson meson_command = mesonlib.get_meson_command() if meson_command is None: meson_command = [] else: meson_command = meson_command.copy() self.coredata = coredata.CoreData(options, self.scratch_dir, meson_command) self.first_invocation = True def init_backend_options(self, backend_name: str) -> None: # Only init backend options on first invocation otherwise it would # override values previously set from command line. if not self.first_invocation: return self.coredata.init_backend_options(backend_name) for k, v in self.options.items(): if self.coredata.optstore.is_backend_option(k): self.coredata.optstore.set_option(k, v) def is_cross_build(self, when_building_for: MachineChoice = MachineChoice.HOST) -> bool: return self.coredata.is_cross_build(when_building_for) def dump_coredata(self) -> str: return coredata.save(self.coredata, self.get_build_dir()) def get_log_dir(self) -> str: return self.log_dir def get_coredata(self) -> coredata.CoreData: return self.coredata @staticmethod def get_build_command(unbuffered: bool = False) -> T.List[str]: cmd = mesonlib.get_meson_command() if cmd is None: raise MesonBugException('No command?') cmd = cmd.copy() if unbuffered and 'python' in os.path.basename(cmd[0]): cmd.insert(1, '-u') return cmd def lookup_binary_entry(self, for_machine: MachineChoice, name: str) -> T.Optional[T.List[str]]: return self.binaries[for_machine].lookup_entry(name) def get_scratch_dir(self) -> str: return self.scratch_dir def get_source_dir(self) -> str: return self.source_dir def get_build_dir(self) -> str: return self.build_dir def get_import_lib_dir(self) -> str: "Install dir for the import library (library used for linking)" return self.get_libdir() def get_shared_module_dir(self) -> str: "Install dir for shared modules that are loaded at runtime" return self.get_libdir() def get_shared_lib_dir(self) -> str: "Install dir for the shared library" m = self.machines.host # Windows has no RPATH or similar, so DLLs must be next to EXEs. if m.is_windows() or m.is_cygwin(): return self.get_bindir() return self.get_libdir() def get_jar_dir(self) -> str: """Install dir for JAR files""" return f"{self.get_datadir()}/java" def get_static_lib_dir(self) -> str: "Install dir for the static library" return self.get_libdir() def get_prefix(self) -> str: return _as_str(self.coredata.optstore.get_value_for(OptionKey('prefix'))) def get_libdir(self) -> str: return _as_str(self.coredata.optstore.get_value_for(OptionKey('libdir'))) def get_libexecdir(self) -> str: return _as_str(self.coredata.optstore.get_value_for(OptionKey('libexecdir'))) def get_bindir(self) -> str: return _as_str(self.coredata.optstore.get_value_for(OptionKey('bindir'))) def get_includedir(self) -> str: return _as_str(self.coredata.optstore.get_value_for(OptionKey('includedir'))) def get_mandir(self) -> str: return _as_str(self.coredata.optstore.get_value_for(OptionKey('mandir'))) def get_datadir(self) -> str: return _as_str(self.coredata.optstore.get_value_for(OptionKey('datadir'))) def get_compiler_system_lib_dirs(self, for_machine: MachineChoice) -> T.List[str]: for comp in self.coredata.compilers[for_machine].values(): if comp.id == 'clang': index = 1 break elif comp.id == 'gcc': index = 2 break else: # This option is only supported by gcc and clang. If we don't get a # GCC or Clang compiler return and empty list. return [] p, out, _ = Popen_safe(comp.get_exelist() + ['-print-search-dirs']) if p.returncode != 0: raise mesonlib.MesonException('Could not calculate system search dirs') split = out.split('\n')[index].lstrip('libraries: =').split(':') return [os.path.normpath(p) for p in split] def get_compiler_system_include_dirs(self, for_machine: MachineChoice) -> T.List[str]: for comp in self.coredata.compilers[for_machine].values(): if comp.id == 'clang': break elif comp.id == 'gcc': break else: # This option is only supported by gcc and clang. If we don't get a # GCC or Clang compiler return and empty list. return [] return comp.get_default_include_dirs() def need_exe_wrapper(self, for_machine: MachineChoice = MachineChoice.HOST) -> bool: value = self.properties[for_machine].get('needs_exe_wrapper', None) if value is not None: assert isinstance(value, bool), 'for mypy' return value if not self.is_cross_build(): return False return not machine_info_can_run(self.machines[for_machine]) def get_exe_wrapper(self) -> T.Optional[ExternalProgram]: if not self.need_exe_wrapper(): return None return self.exe_wrapper def has_exe_wrapper(self) -> bool: return self.exe_wrapper is not None and self.exe_wrapper.found() def get_env_for_paths(self, library_paths: T.Set[str], extra_paths: T.Set[str]) -> mesonlib.EnvironmentVariables: env = mesonlib.EnvironmentVariables() need_wine = not self.machines.build.is_windows() and self.machines.host.is_windows() if need_wine: # Executable paths should be in both PATH and WINEPATH. # - Having them in PATH makes bash completion find it, # and make running "foo.exe" find it when wine-binfmt is installed. # - Having them in WINEPATH makes "wine foo.exe" find it. library_paths.update(extra_paths) if library_paths: if need_wine: env.prepend('WINEPATH', list(library_paths), separator=';') elif self.machines.host.is_windows() or self.machines.host.is_cygwin(): extra_paths.update(library_paths) elif self.machines.host.is_darwin(): env.prepend('DYLD_LIBRARY_PATH', list(library_paths)) else: env.prepend('LD_LIBRARY_PATH', list(library_paths)) if extra_paths: env.prepend('PATH', list(extra_paths)) return env def add_lang_args(self, lang: str, comp: T.Type['Compiler'], for_machine: MachineChoice) -> None: """Add global language arguments that are needed before compiler/linker detection.""" description = f'Extra arguments passed to the {lang}' argkey = OptionKey(f'{lang}_args', machine=for_machine) largkey = OptionKey(f'{lang}_link_args', machine=for_machine) comp_args_from_envvar = False comp_options = self.coredata.optstore.get_pending_value(argkey) if comp_options is None: comp_args_from_envvar = True comp_options = self.env_opts.get(argkey, []) link_options = self.coredata.optstore.get_pending_value(largkey) if link_options is None: link_options = self.env_opts.get(largkey, []) assert isinstance(comp_options, (str, list)), 'for mypy' assert isinstance(link_options, (str, list)), 'for mypy' cargs = options.UserStringArrayOption( argkey.name, description + ' compiler', comp_options, split_args=True, allow_dups=True) largs = options.UserStringArrayOption( largkey.name, description + ' linker', link_options, split_args=True, allow_dups=True) self.coredata.optstore.add_compiler_option(lang, argkey, cargs) self.coredata.optstore.add_compiler_option(lang, largkey, largs) if comp.INVOKES_LINKER and comp_args_from_envvar: # If the compiler acts as a linker driver, and we're using the # environment variable flags for both the compiler and linker # arguments, then put the compiler flags in the linker flags as well. # This is how autotools works, and the env vars feature is for # autotools compatibility. largs.extend_value(comp_options)