diff options
Diffstat (limited to 'mesonbuild/environment.py')
-rw-r--r-- | mesonbuild/environment.py | 858 |
1 files changed, 858 insertions, 0 deletions
diff --git a/mesonbuild/environment.py b/mesonbuild/environment.py new file mode 100644 index 0000000..a06d35e --- /dev/null +++ b/mesonbuild/environment.py @@ -0,0 +1,858 @@ +# Copyright 2012-2020 The Meson development team + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from __future__ import annotations + +import itertools +import os, platform, re, sys, shutil +import typing as T +import collections + +from . import coredata +from . import mesonlib +from .mesonlib import ( + MesonException, MachineChoice, Popen_safe, PerMachine, + PerMachineDefaultable, PerThreeMachineDefaultable, split_args, quote_arg, OptionKey, + search_version, MesonBugException +) +from . import mlog +from .programs import ExternalProgram + +from .envconfig import ( + BinaryTable, MachineInfo, Properties, known_cpu_families, CMakeVariables, +) +from . import compilers +from .compilers import ( + Compiler, + is_assembly, + is_header, + is_library, + is_llvm_ir, + is_object, + is_source, +) + +from functools import lru_cache +from mesonbuild import envconfig + +if T.TYPE_CHECKING: + import argparse + from configparser import ConfigParser + + from .wrap.wrap import Resolver + + CompilersDict = T.Dict[str, Compiler] + + +build_filename = 'meson.build' + + +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 + + +def detect_gcovr(min_version='3.3', log=False): + gcovr_exe = 'gcovr' + try: + p, found = Popen_safe([gcovr_exe, '--version'])[0:2] + except (FileNotFoundError, PermissionError): + # Doesn't exist in PATH or isn't executable + return None, None + found = search_version(found) + if p.returncode == 0 and mesonlib.version_compare(found, '>=' + min_version): + if log: + mlog.log('Found gcovr-{} at {}'.format(found, quote_arg(shutil.which(gcovr_exe)))) + return gcovr_exe, found + return None, None + +def detect_llvm_cov(): + tools = get_llvm_tool_names('llvm-cov') + for tool in tools: + if mesonlib.exe_exists([tool, '--version']): + return tool + return None + +def find_coverage_tools() -> T.Tuple[T.Optional[str], T.Optional[str], T.Optional[str], T.Optional[str], T.Optional[str]]: + gcovr_exe, gcovr_version = detect_gcovr() + + llvm_cov_exe = detect_llvm_cov() + + lcov_exe = 'lcov' + genhtml_exe = 'genhtml' + + if not mesonlib.exe_exists([lcov_exe, '--version']): + lcov_exe = None + if not mesonlib.exe_exists([genhtml_exe, '--version']): + genhtml_exe = None + + return gcovr_exe, gcovr_version, lcov_exe, genhtml_exe, llvm_cov_exe + +def detect_ninja(version: str = '1.8.2', log: bool = False) -> T.List[str]: + r = detect_ninja_command_and_version(version, log) + return r[0] if r else None + +def detect_ninja_command_and_version(version: str = '1.8.2', log: bool = False) -> T.Tuple[T.List[str], str]: + env_ninja = os.environ.get('NINJA', None) + for n in [env_ninja] if env_ninja else ['ninja', 'ninja-build', 'samu']: + prog = ExternalProgram(n, silent=True) + if not prog.found(): + continue + try: + p, found = Popen_safe(prog.command + ['--version'])[0:2] + except (FileNotFoundError, PermissionError): + # Doesn't exist in PATH or isn't executable + continue + found = found.strip() + # Perhaps we should add a way for the caller to know the failure mode + # (not found or too old) + if p.returncode == 0 and mesonlib.version_compare(found, '>=' + version): + if log: + name = os.path.basename(n) + if name.endswith('-' + found): + name = name[0:-1 - len(found)] + if name == 'ninja-build': + name = 'ninja' + if name == 'samu': + name = 'samurai' + mlog.log('Found {}-{} at {}'.format(name, found, + ' '.join([quote_arg(x) for x in prog.command]))) + return (prog.command, found) + +def get_llvm_tool_names(tool: str) -> T.List[str]: + # Ordered list of possible suffixes of LLVM executables to try. Start with + # base, then try newest back to oldest (3.5 is arbitrary), and finally the + # devel version. Please note that the development snapshot in Debian does + # not have a distinct name. Do not move it to the beginning of the list + # unless it becomes a stable release. + suffixes = [ + '', # base (no suffix) + '-14', '14', + '-13', '13', + '-12', '12', + '-11', '11', + '-10', '10', + '-9', '90', + '-8', '80', + '-7', '70', + '-6.0', '60', + '-5.0', '50', + '-4.0', '40', + '-3.9', '39', + '-3.8', '38', + '-3.7', '37', + '-3.6', '36', + '-3.5', '35', + '-15', # Debian development snapshot + '-devel', # FreeBSD development snapshot + ] + names = [] + for suffix in suffixes: + names.append(tool + suffix) + return names + +def detect_scanbuild() -> T.List[str]: + """ Look for scan-build binary on build platform + + First, if a SCANBUILD env variable has been provided, give it precedence + on all platforms. + + For most platforms, scan-build is found is the PATH contains a binary + named "scan-build". However, some distribution's package manager (FreeBSD) + don't. For those, loop through a list of candidates to see if one is + available. + + Return: a single-element list of the found scan-build binary ready to be + passed to Popen() + """ + exelist = [] + if 'SCANBUILD' in os.environ: + exelist = split_args(os.environ['SCANBUILD']) + + else: + tools = get_llvm_tool_names('scan-build') + for tool in tools: + if shutil.which(tool) is not None: + exelist = [shutil.which(tool)] + break + + if exelist: + tool = exelist[0] + if os.path.isfile(tool) and os.access(tool, os.X_OK): + return [tool] + return [] + +def detect_clangformat() -> T.List[str]: + """ Look for clang-format binary on build platform + + Do the same thing as detect_scanbuild to find clang-format except it + currently does not check the environment variable. + + Return: a single-element list of the found clang-format binary ready to be + passed to Popen() + """ + tools = get_llvm_tool_names('clang-format') + for tool in tools: + path = shutil.which(tool) + if path is not None: + return [path] + return [] + +def detect_windows_arch(compilers: CompilersDict) -> str: + """ + Detecting the 'native' architecture of Windows is not a trivial task. We + cannot trust that the architecture that Python is built for is the 'native' + one because you can run 32-bit apps on 64-bit Windows using WOW64 and + people sometimes install 32-bit Python on 64-bit Windows. + + We also can't rely on the architecture of the OS itself, since it's + perfectly normal to compile and run 32-bit applications on Windows as if + they were native applications. It's a terrible experience to require the + user to supply a cross-info file to compile 32-bit applications on 64-bit + Windows. Thankfully, the only way to compile things with Visual Studio on + Windows is by entering the 'msvc toolchain' environment, which can be + easily detected. + + In the end, the sanest method is as follows: + 1. Check environment variables that are set by Windows and WOW64 to find out + if this is x86 (possibly in WOW64), if so use that as our 'native' + architecture. + 2. If the compiler toolchain target architecture is x86, use that as our + 'native' architecture. + 3. Otherwise, use the actual Windows architecture + + """ + os_arch = mesonlib.windows_detect_native_arch() + if os_arch == 'x86': + return os_arch + # If we're on 64-bit Windows, 32-bit apps can be compiled without + # cross-compilation. So if we're doing that, just set the native arch as + # 32-bit and pretend like we're running under WOW64. Else, return the + # actual Windows architecture that we deduced above. + for compiler in compilers.values(): + if compiler.id == 'msvc' and (compiler.target in {'x86', '80x86'}): + return 'x86' + if compiler.id == 'clang-cl' and compiler.target == 'x86': + return 'x86' + if compiler.id == 'gcc' and compiler.has_builtin_define('__i386__'): + return 'x86' + return os_arch + +def any_compiler_has_define(compilers: CompilersDict, define): + for c in compilers.values(): + try: + if c.has_builtin_define(define): + return True + except mesonlib.MesonException: + # Ignore compilers that do not support has_builtin_define. + pass + return False + +def detect_cpu_family(compilers: CompilersDict) -> str: + """ + Python is inconsistent in its platform module. + It returns different values for the same cpu. + For x86 it might return 'x86', 'i686' or somesuch. + Do some canonicalization. + """ + if mesonlib.is_windows(): + trial = detect_windows_arch(compilers) + elif mesonlib.is_freebsd() or mesonlib.is_netbsd() or mesonlib.is_openbsd() or mesonlib.is_qnx() or mesonlib.is_aix(): + trial = platform.processor().lower() + else: + trial = platform.machine().lower() + if trial.startswith('i') and trial.endswith('86'): + trial = 'x86' + elif trial == 'bepc': + trial = 'x86' + elif trial == 'arm64': + trial = 'aarch64' + elif trial.startswith('aarch64'): + # This can be `aarch64_be` + trial = 'aarch64' + elif trial.startswith('arm') or trial.startswith('earm'): + trial = 'arm' + elif trial.startswith(('powerpc64', 'ppc64')): + trial = 'ppc64' + elif trial.startswith(('powerpc', 'ppc')) or trial in {'macppc', 'power macintosh'}: + trial = 'ppc' + elif trial in {'amd64', 'x64', 'i86pc'}: + trial = 'x86_64' + elif trial in {'sun4u', 'sun4v'}: + trial = 'sparc64' + elif trial.startswith('mips'): + if '64' not in trial: + trial = 'mips' + else: + trial = 'mips64' + elif trial in {'ip30', 'ip35'}: + trial = 'mips64' + + # On Linux (and maybe others) there can be any mixture of 32/64 bit code in + # the kernel, Python, system, 32-bit chroot on 64-bit host, etc. The only + # reliable way to know is to check the compiler defines. + if trial == 'x86_64': + if any_compiler_has_define(compilers, '__i386__'): + trial = 'x86' + elif trial == 'aarch64': + if any_compiler_has_define(compilers, '__arm__'): + trial = 'arm' + # Add more quirks here as bugs are reported. Keep in sync with detect_cpu() + # below. + elif trial == 'parisc64': + # ATM there is no 64 bit userland for PA-RISC. Thus always + # report it as 32 bit for simplicity. + trial = 'parisc' + elif trial == 'ppc': + # AIX always returns powerpc, check here for 64-bit + if any_compiler_has_define(compilers, '__64BIT__'): + trial = 'ppc64' + + if trial not in known_cpu_families: + mlog.warning(f'Unknown CPU family {trial!r}, please report this at ' + 'https://github.com/mesonbuild/meson/issues/new with the ' + 'output of `uname -a` and `cat /proc/cpuinfo`') + + return trial + +def detect_cpu(compilers: CompilersDict) -> str: + if mesonlib.is_windows(): + trial = detect_windows_arch(compilers) + elif mesonlib.is_freebsd() or mesonlib.is_netbsd() or mesonlib.is_openbsd() or mesonlib.is_aix(): + trial = platform.processor().lower() + else: + trial = platform.machine().lower() + + if trial in {'amd64', 'x64', 'i86pc'}: + trial = 'x86_64' + if trial == 'x86_64': + # Same check as above for cpu_family + if any_compiler_has_define(compilers, '__i386__'): + trial = 'i686' # All 64 bit cpus have at least this level of x86 support. + elif trial.startswith('aarch64') or trial.startswith('arm64'): + # Same check as above for cpu_family + if any_compiler_has_define(compilers, '__arm__'): + trial = 'arm' + else: + # for aarch64_be + trial = 'aarch64' + elif trial.startswith('earm'): + trial = 'arm' + elif trial == 'e2k': + # Make more precise CPU detection for Elbrus platform. + trial = platform.processor().lower() + elif trial.startswith('mips'): + if '64' not in trial: + trial = 'mips' + else: + trial = 'mips64' + elif trial == 'ppc': + # AIX always returns powerpc, check here for 64-bit + if any_compiler_has_define(compilers, '__64BIT__'): + trial = 'ppc64' + + # Add more quirks here as bugs are reported. Keep in sync with + # detect_cpu_family() above. + return trial + +def detect_system() -> str: + if sys.platform == 'cygwin': + return 'cygwin' + return platform.system().lower() + +def detect_msys2_arch() -> T.Optional[str]: + return os.environ.get('MSYSTEM_CARCH', None) + +def detect_machine_info(compilers: T.Optional[CompilersDict] = None) -> MachineInfo: + """Detect the machine we're running on + + If compilers are not provided, we cannot know as much. None out those + fields to avoid accidentally depending on partial knowledge. The + underlying ''detect_*'' method can be called to explicitly use the + partial information. + """ + return MachineInfo( + detect_system(), + detect_cpu_family(compilers) if compilers is not None else None, + detect_cpu(compilers) if compilers is not None else None, + sys.byteorder) + +# TODO make this compare two `MachineInfo`s purely. How important is the +# `detect_cpu_family({})` distinction? It is the one impediment to that. +def machine_info_can_run(machine_info: MachineInfo): + """Whether we can run binaries for this machine on the current machine. + + Can almost always run 32-bit binaries on 64-bit natively if the host + and build systems are the same. We don't pass any compilers to + detect_cpu_family() here because we always want to know the OS + architecture, not what the compiler environment tells us. + """ + if machine_info.system != detect_system(): + return False + true_build_cpu_family = detect_cpu_family({}) + return \ + (machine_info.cpu_family == true_build_cpu_family) or \ + ((true_build_cpu_family == 'x86_64') and (machine_info.cpu_family == 'x86')) or \ + ((true_build_cpu_family == 'aarch64') and (machine_info.cpu_family == 'arm')) + +class Environment: + private_dir = 'meson-private' + log_dir = 'meson-logs' + info_dir = 'meson-info' + + def __init__(self, source_dir: T.Optional[str], build_dir: T.Optional[str], options: 'argparse.Namespace') -> None: + self.source_dir = source_dir + self.build_dir = build_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.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.load(self.get_build_dir()) # type: coredata.CoreData + self.first_invocation = False + except FileNotFoundError: + self.create_new_coredata(options) + except coredata.MesonVersionMismatchException as e: + # This is routine, but tell the user the update happened + mlog.log('Regenerating configuration from scratch:', str(e)) + coredata.read_cmd_line_file(self.build_dir, options) + self.create_new_coredata(options) + except MesonException as e: + # If we stored previous command line options, we can recover from + # a broken/outdated coredata. + if os.path.isfile(coredata.get_cmd_line_file(self.build_dir)): + mlog.warning('Regenerating configuration from scratch.') + mlog.log('Reason:', mlog.red(str(e))) + coredata.read_cmd_line_file(self.build_dir, options) + self.create_new_coredata(options) + else: + raise e + else: + # Just create a fresh coredata in this case + self.scratch_dir = '' + self.create_new_coredata(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() # type: PerMachineDefaultable[BinaryTable] + + # Misc other properties about each machine. + properties = PerMachineDefaultable() # type: PerMachineDefaultable[Properties] + + # CMake toolchain variables + cmakevars = PerMachineDefaultable() # type: PerMachineDefaultable[CMakeVariables] + + ## 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: T.MutableMapping[OptionKey, T.Union[str, T.List[str]]] = collections.OrderedDict() + + ## Read in native file(s) to override build machine configuration + + if self.coredata.config_files is not None: + config = coredata.parse_machine_files(self.coredata.config_files) + 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 = coredata.parse_machine_files(self.coredata.cross_files) + 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.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() + + # Command line options override those from cross/native files + self.options.update(options.cmd_line_options) + + # 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') + + 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 _load_machine_file_options(self, config: 'ConfigParser', 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 k, v in paths.items(): + self.options[OptionKey.from_string(k).evolve(machine=machine)] = 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 k, v in properties.properties.copy().items(): + if k in deprecated_properties: + mlog.deprecation(f'{k} in the [properties] section of the machine file is deprecated, use the [built-in options] section.') + self.options[OptionKey.from_string(k).evolve(machine=machine)] = v + del properties.properties[k] + + for section, values in config.items(): + if ':' in section: + subproject, section = section.split(':') + else: + subproject = '' + if section == 'built-in options': + for k, v in values.items(): + key = OptionKey.from_string(k) + # 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 0.60', once=True) + if key.subproject: + raise MesonException('Do not set subproject options in [built-in options] section, use [subproject:built-in options] instead.') + self.options[key.evolve(subproject=subproject, machine=machine)] = 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 k, v in values.items(): + # Project options are always for the host machine + key = OptionKey.from_string(k) + if key.subproject: + raise MesonException('Do not set subproject options in [built-in options] section, use [subproject:built-in options] instead.') + self.options[key.evolve(subproject=subproject)] = v + + 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()] + + [ + ('PKG_CONFIG_PATH', 'pkg_config_path'), + ('CMAKE_PREFIX_PATH', 'cmake_prefix_path'), + ('LDFLAGS', 'ldflags'), + ('CPPFLAGS', 'cppflags'), + ] + ) + + 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': + key = OptionKey('link_args', machine=for_machine, lang='c') # needs a language to initialize properly + for lang in compilers.compilers.LANGUAGES_USING_LDFLAGS: + key = key.evolve(lang=lang) + env_opts[key].extend(p_list) + elif keyname == 'cppflags': + key = OptionKey('env_args', machine=for_machine, lang='c') + for lang in compilers.compilers.LANGUAGES_USING_CPPFLAGS: + key = key.evolve(lang=lang) + env_opts[key].extend(p_list) + else: + key = OptionKey.from_string(keyname).evolve(machine=for_machine) + if evar in compilers.compilers.CFLAGS_MAPPING.values(): + # 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 subtely different behavior. `$CFLAGS` will be + # added to the linker command line if the compiler + # acts as a linker driver, `-Dc_args` will not. + # + # We still use the original key as the base here, as + # we want to inherit the machine and the compiler + # language + key = key.evolve('env_args') + env_opts[key].extend(p_list) + + # Only store options that are not already in self.options, + # otherwise we'd override the machine files + for k, v in env_opts.items(): + if k not in self.options: + self.options[k] = v + + 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: + 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: 'argparse.Namespace') -> 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 + self.coredata = coredata.CoreData(options, self.scratch_dir, mesonlib.get_meson_command()) + self.first_invocation = True + + 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 is_header(self, fname: 'mesonlib.FileOrString') -> bool: + return is_header(fname) + + def is_source(self, fname: 'mesonlib.FileOrString') -> bool: + return is_source(fname) + + def is_assembly(self, fname: 'mesonlib.FileOrString') -> bool: + return is_assembly(fname) + + def is_llvm_ir(self, fname: 'mesonlib.FileOrString') -> bool: + return is_llvm_ir(fname) + + def is_object(self, fname: 'mesonlib.FileOrString') -> bool: + return is_object(fname) + + @lru_cache(maxsize=None) + def is_library(self, fname): + return is_library(fname) + + 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 self.coredata.get_option(OptionKey('prefix')) + + def get_libdir(self) -> str: + return self.coredata.get_option(OptionKey('libdir')) + + def get_libexecdir(self) -> str: + return self.coredata.get_option(OptionKey('libexecdir')) + + def get_bindir(self) -> str: + return self.coredata.get_option(OptionKey('bindir')) + + def get_includedir(self) -> str: + return self.coredata.get_option(OptionKey('includedir')) + + def get_mandir(self) -> str: + return self.coredata.get_option(OptionKey('mandir')) + + def get_datadir(self) -> str: + return self.coredata.get_option(OptionKey('datadir')) + + def get_compiler_system_dirs(self, for_machine: MachineChoice): + 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') + out = out.split('\n')[index].lstrip('libraries: =').split(':') + return [os.path.normpath(p) for p in out] + + def need_exe_wrapper(self, for_machine: MachineChoice = MachineChoice.HOST): + value = self.properties[for_machine].get('needs_exe_wrapper', None) + if value is not None: + return value + 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 |