blob: 346abcc3abb14e0ec694394c8d9fd0f5bc23dc2f [file] [log] [blame]
# SPDX-License-Identifier: Apache-2.0
# Copyright 2013-2024 Contributors to the The Meson project
# Copyright © 2019-2025 Intel Corporation
from __future__ import annotations
from collections import OrderedDict
from itertools import chain
import argparse
import copy
import dataclasses
import itertools
import os
import pathlib
import typing as T
from .mesonlib import (
HoldableObject,
default_prefix,
default_datadir,
default_includedir,
default_infodir,
default_libdir,
default_libexecdir,
default_localedir,
default_mandir,
default_sbindir,
default_sysconfdir,
MesonException,
MesonBugException,
listify_array_value,
MachineChoice,
)
from . import mlog
if T.TYPE_CHECKING:
from typing_extensions import Literal, Final, TypeAlias, TypedDict
from .interpreterbase import SubProject
DeprecatedType: TypeAlias = T.Union[bool, str, T.Dict[str, str], T.List[str]]
AnyOptionType: TypeAlias = T.Union[
'UserBooleanOption', 'UserComboOption', 'UserFeatureOption',
'UserIntegerOption', 'UserStdOption', 'UserStringArrayOption',
'UserStringOption', 'UserUmaskOption']
ElementaryOptionValues: TypeAlias = T.Union[str, int, bool, T.List[str]]
MutableKeyedOptionDictType: TypeAlias = T.Dict['OptionKey', AnyOptionType]
_OptionKeyTuple: TypeAlias = T.Tuple[T.Optional[str], MachineChoice, str]
class ArgparseKWs(TypedDict, total=False):
action: str
dest: str
default: str
choices: T.List
DEFAULT_YIELDING = False
# Can't bind this near the class method it seems, sadly.
_T = T.TypeVar('_T')
backendlist = ['ninja', 'vs', 'vs2010', 'vs2012', 'vs2013', 'vs2015', 'vs2017', 'vs2019', 'vs2022', 'xcode', 'none']
genvslitelist = ['vs2022']
buildtypelist = ['plain', 'debug', 'debugoptimized', 'release', 'minsize', 'custom']
# This is copied from coredata. There is no way to share this, because this
# is used in the OptionKey constructor, and the coredata lists are
# OptionKeys...
_BUILTIN_NAMES = {
'prefix',
'bindir',
'datadir',
'includedir',
'infodir',
'libdir',
'licensedir',
'libexecdir',
'localedir',
'localstatedir',
'mandir',
'sbindir',
'sharedstatedir',
'sysconfdir',
'auto_features',
'backend',
'buildtype',
'debug',
'default_library',
'default_both_libraries',
'errorlogs',
'genvslite',
'install_umask',
'layout',
'optimization',
'prefer_static',
'stdsplit',
'strip',
'unity',
'unity_size',
'warning_level',
'werror',
'wrap_mode',
'force_fallback_for',
'pkg_config_path',
'cmake_prefix_path',
'vsenv',
}
_BAD_VALUE = 'Qwert Zuiopü'
_optionkey_cache: T.Dict[_OptionKeyTuple, OptionKey] = {}
class OptionKey:
"""Represents an option key in the various option dictionaries.
This provides a flexible, powerful way to map option names from their
external form (things like subproject:build.option) to something that
internally easier to reason about and produce.
"""
__slots__ = ('name', 'subproject', 'machine', '_hash')
name: str
subproject: T.Optional[str] # None is global, empty string means top level project
machine: MachineChoice
_hash: int
def __new__(cls,
name: str = '',
subproject: T.Optional[str] = None,
machine: MachineChoice = MachineChoice.HOST) -> OptionKey:
"""The use of the __new__ method allows to add a transparent cache
to the OptionKey object creation, without breaking its API.
"""
if not name:
return super().__new__(cls) # for unpickling, do not cache now
tuple_: _OptionKeyTuple = (subproject, machine, name)
try:
return _optionkey_cache[tuple_]
except KeyError:
instance = super().__new__(cls)
instance._init(name, subproject, machine)
_optionkey_cache[tuple_] = instance
return instance
def _init(self, name: str, subproject: T.Optional[str], machine: MachineChoice) -> None:
# We don't use the __init__ method, because it would be called after __new__
# while we need __new__ to initialise the object before populating the cache.
if not isinstance(machine, MachineChoice):
raise MesonException(f'Internal error, bad machine type: {machine}')
if not isinstance(name, str):
raise MesonBugException(f'Key name is not a string: {name}')
assert ':' not in name
object.__setattr__(self, 'name', name)
object.__setattr__(self, 'subproject', subproject)
object.__setattr__(self, 'machine', machine)
object.__setattr__(self, '_hash', hash((name, subproject, machine)))
def __setattr__(self, key: str, value: T.Any) -> None:
raise AttributeError('OptionKey instances do not support mutation.')
def __getstate__(self) -> T.Dict[str, T.Any]:
return {
'name': self.name,
'subproject': self.subproject,
'machine': self.machine,
}
def __setstate__(self, state: T.Dict[str, T.Any]) -> None:
# Here, the object is created using __new__()
self._init(**state)
_optionkey_cache[self._to_tuple()] = self
def __hash__(self) -> int:
return self._hash
def _to_tuple(self) -> _OptionKeyTuple:
return (self.subproject, self.machine, self.name)
def __eq__(self, other: object) -> bool:
if isinstance(other, OptionKey):
return self._to_tuple() == other._to_tuple()
return NotImplemented
def __ne__(self, other: object) -> bool:
if isinstance(other, OptionKey):
return self._to_tuple() != other._to_tuple()
return NotImplemented
def __lt__(self, other: object) -> bool:
if isinstance(other, OptionKey):
if self.subproject is None:
return other.subproject is not None
elif other.subproject is None:
return False
return self._to_tuple() < other._to_tuple()
return NotImplemented
def __le__(self, other: object) -> bool:
if isinstance(other, OptionKey):
if self.subproject is None and other.subproject is not None:
return True
elif self.subproject is not None and other.subproject is None:
return False
return self._to_tuple() <= other._to_tuple()
return NotImplemented
def __gt__(self, other: object) -> bool:
if isinstance(other, OptionKey):
if other.subproject is None:
return self.subproject is not None
elif self.subproject is None:
return False
return self._to_tuple() > other._to_tuple()
return NotImplemented
def __ge__(self, other: object) -> bool:
if isinstance(other, OptionKey):
if self.subproject is None and other.subproject is not None:
return False
elif self.subproject is not None and other.subproject is None:
return True
return self._to_tuple() >= other._to_tuple()
return NotImplemented
def __str__(self) -> str:
out = self.name
if self.machine is MachineChoice.BUILD:
out = f'build.{out}'
if self.subproject is not None:
out = f'{self.subproject}:{out}'
return out
def __repr__(self) -> str:
return f'OptionKey({self.name!r}, {self.subproject!r}, {self.machine!r})'
@classmethod
def from_string(cls, raw: str) -> 'OptionKey':
"""Parse the raw command line format into a three part tuple.
This takes strings like `mysubproject:build.myoption` and Creates an
OptionKey out of them.
"""
assert isinstance(raw, str)
try:
subproject, raw2 = raw.split(':')
except ValueError:
subproject, raw2 = None, raw
for_machine = MachineChoice.HOST
try:
prefix, raw3 = raw2.split('.')
if prefix == 'build':
for_machine = MachineChoice.BUILD
else:
raw3 = raw2
except ValueError:
raw3 = raw2
opt = raw3
assert ':' not in opt
assert opt.count('.') < 2
return cls(opt, subproject, for_machine)
def evolve(self,
name: T.Optional[str] = None,
subproject: T.Optional[str] = _BAD_VALUE,
machine: T.Optional[MachineChoice] = None) -> 'OptionKey':
"""Create a new copy of this key, but with altered members.
For example:
>>> a = OptionKey('foo', '', MachineChoice.Host)
>>> b = OptionKey('foo', 'bar', MachineChoice.Host)
>>> b == a.evolve(subproject='bar')
True
"""
# We have to be a little clever with lang here, because lang is valid
# as None, for non-compiler options
return OptionKey(name if name is not None else self.name,
subproject if subproject != _BAD_VALUE else self.subproject, # None is a valid value so it can'the default value in method declaration.
machine if machine is not None else self.machine)
def as_root(self) -> OptionKey:
"""Convenience method for key.evolve(subproject='')."""
return self.evolve(subproject='')
def as_build(self) -> OptionKey:
"""Convenience method for key.evolve(machine=MachineChoice.BUILD)."""
return self.evolve(machine=MachineChoice.BUILD)
def as_host(self) -> OptionKey:
"""Convenience method for key.evolve(machine=MachineChoice.HOST)."""
return self.evolve(machine=MachineChoice.HOST)
def has_module_prefix(self) -> bool:
return '.' in self.name
def get_module_prefix(self) -> T.Optional[str]:
if self.has_module_prefix():
return self.name.split('.', 1)[0]
return None
def is_for_build(self) -> bool:
return self.machine is MachineChoice.BUILD
if T.TYPE_CHECKING:
OptionDict: TypeAlias = T.Dict[OptionKey, ElementaryOptionValues]
@dataclasses.dataclass
class UserOption(T.Generic[_T], HoldableObject):
name: str
description: str
value_: dataclasses.InitVar[_T]
yielding: bool = DEFAULT_YIELDING
deprecated: DeprecatedType = False
readonly: bool = dataclasses.field(default=False)
parent: T.Optional[UserOption] = None
def __post_init__(self, value_: _T) -> None:
self.value = self.validate_value(value_)
# Final isn't technically allowed in a __post_init__ method
self.default: Final[_T] = self.value # type: ignore[misc]
def listify(self, value: ElementaryOptionValues) -> T.List[str]:
if isinstance(value, list):
return value
if isinstance(value, bool):
return ['true'] if value else ['false']
if isinstance(value, int):
return [str(value)]
return [value]
def printable_value(self) -> ElementaryOptionValues:
assert isinstance(self.value, (str, int, bool, list))
return self.value
def printable_choices(self) -> T.Optional[T.List[str]]:
return None
# Check that the input is a valid value and return the
# "cleaned" or "native" version. For example the Boolean
# option could take the string "true" and return True.
def validate_value(self, value: object) -> _T:
raise RuntimeError('Derived option class did not override validate_value.')
def set_value(self, newvalue: object) -> bool:
oldvalue = self.value
self.value = self.validate_value(newvalue)
return self.value != oldvalue
@dataclasses.dataclass
class EnumeratedUserOption(UserOption[_T]):
"""A generic UserOption that has enumerated values."""
choices: T.List[_T] = dataclasses.field(default_factory=list)
def printable_choices(self) -> T.Optional[T.List[str]]:
return [str(c) for c in self.choices]
class UserStringOption(UserOption[str]):
def validate_value(self, value: object) -> str:
if not isinstance(value, str):
raise MesonException(f'The value of option "{self.name}" is "{value}", which is not a string.')
return value
@dataclasses.dataclass
class UserBooleanOption(EnumeratedUserOption[bool]):
choices: T.List[bool] = dataclasses.field(default_factory=lambda: [True, False])
def __bool__(self) -> bool:
return self.value
def validate_value(self, value: object) -> bool:
if isinstance(value, bool):
return value
if not isinstance(value, str):
raise MesonException(f'Option "{self.name}" value {value} cannot be converted to a boolean')
if value.lower() == 'true':
return True
if value.lower() == 'false':
return False
raise MesonException(f'Option "{self.name}" value {value} is not boolean (true or false).')
class _UserIntegerBase(UserOption[_T]):
min_value: T.Optional[int]
max_value: T.Optional[int]
if T.TYPE_CHECKING:
def toint(self, v: str) -> int: ...
def __post_init__(self, value_: _T) -> None:
super().__post_init__(value_)
choices: T.List[str] = []
if self.min_value is not None:
choices.append(f'>= {self.min_value!s}')
if self.max_value is not None:
choices.append(f'<= {self.max_value!s}')
self.__choices: str = ', '.join(choices)
def printable_choices(self) -> T.Optional[T.List[str]]:
return [self.__choices]
def validate_value(self, value: object) -> _T:
if isinstance(value, str):
value = T.cast('_T', self.toint(value))
if not isinstance(value, int):
raise MesonException(f'Value {value!r} for option "{self.name}" is not an integer.')
if self.min_value is not None and value < self.min_value:
raise MesonException(f'Value {value} for option "{self.name}" is less than minimum value {self.min_value}.')
if self.max_value is not None and value > self.max_value:
raise MesonException(f'Value {value} for option "{self.name}" is more than maximum value {self.max_value}.')
return T.cast('_T', value)
@dataclasses.dataclass
class UserIntegerOption(_UserIntegerBase[int]):
min_value: T.Optional[int] = None
max_value: T.Optional[int] = None
def toint(self, valuestring: str) -> int:
try:
return int(valuestring)
except ValueError:
raise MesonException(f'Value string "{valuestring}" for option "{self.name}" is not convertible to an integer.')
class OctalInt(int):
# NinjaBackend.get_user_option_args uses str() to converts it to a command line option
# UserUmaskOption.toint() uses int(str, 8) to convert it to an integer
# So we need to use oct instead of dec here if we do not want values to be misinterpreted.
def __str__(self) -> str:
return oct(int(self))
@dataclasses.dataclass
class UserUmaskOption(_UserIntegerBase[T.Union["Literal['preserve']", OctalInt]]):
min_value: T.Optional[int] = dataclasses.field(default=0, init=False)
max_value: T.Optional[int] = dataclasses.field(default=0o777, init=False)
def printable_value(self) -> str:
if isinstance(self.value, int):
return format(self.value, '04o')
return self.value
def validate_value(self, value: object) -> T.Union[Literal['preserve'], OctalInt]:
if value == 'preserve':
return 'preserve'
return OctalInt(super().validate_value(value))
def toint(self, valuestring: str) -> int:
try:
return int(valuestring, 8)
except ValueError as e:
raise MesonException(f'Invalid mode for option "{self.name}" {e}')
@dataclasses.dataclass
class UserComboOption(EnumeratedUserOption[str]):
def validate_value(self, value: object) -> str:
if value not in self.choices:
if isinstance(value, bool):
_type = 'boolean'
elif isinstance(value, (int, float)):
_type = 'number'
else:
_type = 'string'
optionsstring = ', '.join([f'"{item}"' for item in self.choices])
raise MesonException('Value "{}" (of type "{}") for option "{}" is not one of the choices.'
' Possible choices are (as string): {}.'.format(
value, _type, self.name, optionsstring))
assert isinstance(value, str), 'for mypy'
return value
@dataclasses.dataclass
class UserArrayOption(UserOption[T.List[_T]]):
value_: dataclasses.InitVar[T.Union[_T, T.List[_T]]]
choices: T.Optional[T.List[_T]] = None
split_args: bool = False
allow_dups: bool = False
def extend_value(self, value: T.Union[str, T.List[str]]) -> None:
"""Extend the value with an additional value."""
new = self.validate_value(value)
self.set_value(self.value + new)
def printable_choices(self) -> T.Optional[T.List[str]]:
if self.choices is None:
return None
return [str(c) for c in self.choices]
@dataclasses.dataclass
class UserStringArrayOption(UserArrayOption[str]):
def listify(self, value: object) -> T.List[str]:
try:
return listify_array_value(value, self.split_args)
except MesonException as e:
raise MesonException(f'error in option "{self.name}": {e!s}')
def validate_value(self, value: object) -> T.List[str]:
newvalue = self.listify(value)
if not self.allow_dups and len(set(newvalue)) != len(newvalue):
msg = 'Duplicated values in array option is deprecated. ' \
'This will become a hard error in meson 2.0.'
mlog.deprecation(msg)
for i in newvalue:
if not isinstance(i, str):
raise MesonException(f'String array element "{newvalue!s}" for option "{self.name}" is not a string.')
if self.choices:
bad = [x for x in newvalue if x not in self.choices]
if bad:
raise MesonException('Value{} "{}" for option "{}" {} not in allowed choices: "{}"'.format(
'' if len(bad) == 1 else 's',
', '.join(bad),
self.name,
'is' if len(bad) == 1 else 'are',
', '.join(self.choices))
)
return newvalue
@dataclasses.dataclass
class UserFeatureOption(UserComboOption):
choices: T.List[str] = dataclasses.field(
# Ensure we get a copy with the lambda
default_factory=lambda: ['enabled', 'disabled', 'auto'], init=False)
def is_enabled(self) -> bool:
return self.value == 'enabled'
def is_disabled(self) -> bool:
return self.value == 'disabled'
def is_auto(self) -> bool:
return self.value == 'auto'
_U = T.TypeVar('_U', bound=UserOption)
def choices_are_different(a: _U, b: _U) -> bool:
"""Are the choices between two options the same?
:param a: A UserOption[T]
:param b: A second UserOption[T]
:return: True if the choices have changed, otherwise False
"""
if isinstance(a, EnumeratedUserOption):
# We expect `a` and `b` to be of the same type, but can't really annotate it that way.
assert isinstance(b, EnumeratedUserOption), 'for mypy'
return a.choices != b.choices
elif isinstance(a, UserArrayOption):
# We expect `a` and `b` to be of the same type, but can't really annotate it that way.
assert isinstance(b, UserArrayOption), 'for mypy'
return a.choices != b.choices
elif isinstance(a, _UserIntegerBase):
assert isinstance(b, _UserIntegerBase), 'for mypy'
return a.max_value != b.max_value or a.min_value != b.min_value
return False
class UserStdOption(UserComboOption):
'''
UserOption specific to c_std and cpp_std options. User can set a list of
STDs in preference order and it selects the first one supported by current
compiler.
For historical reasons, some compilers (msvc) allowed setting a GNU std and
silently fell back to C std. This is now deprecated. Projects that support
both GNU and MSVC compilers should set e.g. c_std=gnu11,c11.
This is not using self.deprecated mechanism we already have for project
options because we want to print a warning if ALL values are deprecated, not
if SOME values are deprecated.
'''
def __init__(self, lang: str, all_stds: T.List[str]) -> None:
self.lang = lang.lower()
self.all_stds = ['none'] + all_stds
# Map a deprecated std to its replacement. e.g. gnu11 -> c11.
self.deprecated_stds: T.Dict[str, str] = {}
opt_name = 'cpp_std' if lang == 'c++' else f'{lang}_std'
super().__init__(opt_name, f'{lang} language standard to use', 'none', choices=['none'])
def set_versions(self, versions: T.List[str], gnu: bool = False, gnu_deprecated: bool = False) -> None:
assert all(std in self.all_stds for std in versions)
self.choices += versions
if gnu:
gnu_stds_map = {f'gnu{std[1:]}': std for std in versions}
if gnu_deprecated:
self.deprecated_stds.update(gnu_stds_map)
else:
self.choices += gnu_stds_map.keys()
def validate_value(self, value: object) -> str:
try:
candidates = listify_array_value(value)
except MesonException as e:
raise MesonException(f'error in option "{self.name}": {e!s}')
for std in candidates:
if not isinstance(std, str):
raise MesonException(f'String array element "{candidates!s}" for option "{self.name}" is not a string.')
unknown = ','.join(std for std in candidates if std not in self.all_stds)
if unknown:
raise MesonException(f'Unknown option "{self.name}" value {unknown}. Possible values are {self.all_stds}.')
# Check first if any of the candidates are not deprecated
for std in candidates:
if std in self.choices:
return std
# Fallback to a deprecated std if any
for std in candidates:
newstd = self.deprecated_stds.get(std)
if newstd is not None:
mlog.deprecation(
f'None of the values {candidates} are supported by the {self.lang} compiler.\n' +
f'However, the deprecated {std} std currently falls back to {newstd}.\n' +
'This will be an error in meson 2.0.\n' +
'If the project supports both GNU and MSVC compilers, a value such as\n' +
'"c_std=gnu11,c11" specifies that GNU is preferred but it can safely fallback to plain c11.', once=True)
return newstd
raise MesonException(f'None of values {candidates} are supported by the {self.lang.upper()} compiler. ' +
f'Possible values for option "{self.name}" are {self.choices}')
def argparse_name_to_arg(name: str) -> str:
if name == 'warning_level':
return '--warnlevel'
return '--' + name.replace('_', '-')
def argparse_prefixed_default(opt: AnyOptionType, name: OptionKey, prefix: str = '') -> ElementaryOptionValues:
if isinstance(opt, (UserComboOption, UserIntegerOption, UserUmaskOption)):
return T.cast('ElementaryOptionValues', opt.default)
try:
return BUILTIN_DIR_NOPREFIX_OPTIONS[name][prefix]
except KeyError:
return T.cast('ElementaryOptionValues', opt.default)
def option_to_argparse(option: AnyOptionType, name: OptionKey, parser: argparse.ArgumentParser, help_suffix: str) -> None:
kwargs: ArgparseKWs = {}
if isinstance(option, (EnumeratedUserOption, UserArrayOption)):
c = option.choices
else:
c = None
b = 'store_true' if isinstance(option.default, bool) else None
h = option.description
if not b:
h = '{} (default: {}).'.format(h.rstrip('.'), argparse_prefixed_default(option, name))
else:
kwargs['action'] = b
if c and not b:
kwargs['choices'] = c
kwargs['default'] = argparse.SUPPRESS
kwargs['dest'] = str(name)
cmdline_name = argparse_name_to_arg(str(name))
parser.add_argument(cmdline_name, help=h + help_suffix, **kwargs)
# Update `docs/markdown/Builtin-options.md` after changing the options below
# Also update mesonlib._BUILTIN_NAMES. See the comment there for why this is required.
# Please also update completion scripts in $MESONSRC/data/shell-completions/
BUILTIN_DIR_OPTIONS: T.Mapping[OptionKey, AnyOptionType] = {
OptionKey(o.name): o for o in [
UserStringOption('prefix', 'Installation prefix', default_prefix()),
UserStringOption('bindir', 'Executable directory', 'bin'),
UserStringOption('datadir', 'Data file directory', default_datadir()),
UserStringOption('includedir', 'Header file directory', default_includedir()),
UserStringOption('infodir', 'Info page directory', default_infodir()),
UserStringOption('libdir', 'Library directory', default_libdir()),
UserStringOption('licensedir', 'Licenses directory', ''),
UserStringOption('libexecdir', 'Library executable directory', default_libexecdir()),
UserStringOption('localedir', 'Locale data directory', default_localedir()),
UserStringOption('localstatedir', 'Localstate data directory', 'var'),
UserStringOption('mandir', 'Manual page directory', default_mandir()),
UserStringOption('sbindir', 'System executable directory', default_sbindir()),
UserStringOption('sharedstatedir', 'Architecture-independent data directory', 'com'),
UserStringOption('sysconfdir', 'Sysconf data directory', default_sysconfdir()),
]
}
BUILTIN_CORE_OPTIONS: T.Mapping[OptionKey, AnyOptionType] = {
OptionKey(o.name): o for o in T.cast('T.List[AnyOptionType]', [
UserFeatureOption('auto_features', "Override value of all 'auto' features", 'auto'),
UserComboOption('backend', 'Backend to use', 'ninja', choices=backendlist, readonly=True),
UserComboOption(
'genvslite',
'Setup multiple buildtype-suffixed ninja-backend build directories, '
'and a [builddir]_vs containing a Visual Studio meta-backend with multiple configurations that calls into them',
'vs2022',
choices=genvslitelist
),
UserComboOption('buildtype', 'Build type to use', 'debug', choices=buildtypelist),
UserBooleanOption('debug', 'Enable debug symbols and other information', True),
UserComboOption('default_library', 'Default library type', 'shared', choices=['shared', 'static', 'both'],
yielding=False),
UserComboOption('default_both_libraries', 'Default library type for both_libraries', 'shared',
choices=['shared', 'static', 'auto']),
UserBooleanOption('errorlogs', "Whether to print the logs from failing tests", True),
UserUmaskOption('install_umask', 'Default umask to apply on permissions of installed files', OctalInt(0o022)),
UserComboOption('layout', 'Build directory layout', 'mirror', choices=['mirror', 'flat']),
UserComboOption('optimization', 'Optimization level', '0', choices=['plain', '0', 'g', '1', '2', '3', 's']),
UserBooleanOption('prefer_static', 'Whether to try static linking before shared linking', False),
UserBooleanOption('stdsplit', 'Split stdout and stderr in test logs', True),
UserBooleanOption('strip', 'Strip targets on install', False),
UserComboOption('unity', 'Unity build', 'off', choices=['on', 'off', 'subprojects']),
UserIntegerOption('unity_size', 'Unity block size', 4, min_value=2),
UserComboOption('warning_level', 'Compiler warning level to use', '1', choices=['0', '1', '2', '3', 'everything'],
yielding=False),
UserBooleanOption('werror', 'Treat warnings as errors', False, yielding=False),
UserComboOption('wrap_mode', 'Wrap mode', 'default', choices=['default', 'nofallback', 'nodownload', 'forcefallback', 'nopromote']),
UserStringArrayOption('force_fallback_for', 'Force fallback for those subprojects', []),
UserBooleanOption('vsenv', 'Activate Visual Studio environment', False, readonly=True),
# Pkgconfig module
UserBooleanOption('pkgconfig.relocatable', 'Generate pkgconfig files as relocatable', False),
# Python module
UserIntegerOption('python.bytecompile', 'Whether to compile bytecode', 0, min_value=-1, max_value=2),
UserComboOption('python.install_env', 'Which python environment to install to', 'prefix',
choices=['auto', 'prefix', 'system', 'venv']),
UserStringOption('python.platlibdir', 'Directory for site-specific, platform-specific files.', ''),
UserStringOption('python.purelibdir', 'Directory for site-specific, non-platform-specific files.', ''),
UserBooleanOption('python.allow_limited_api', 'Whether to allow use of the Python Limited API', True),
])
}
BUILTIN_OPTIONS = OrderedDict(chain(BUILTIN_DIR_OPTIONS.items(), BUILTIN_CORE_OPTIONS.items()))
BUILTIN_OPTIONS_PER_MACHINE: T.Mapping[OptionKey, AnyOptionType] = {
OptionKey(o.name): o for o in [
UserStringArrayOption('pkg_config_path', 'List of additional paths for pkg-config to search', []),
UserStringArrayOption('cmake_prefix_path', 'List of additional prefixes for cmake to search', []),
]
}
# Special prefix-dependent defaults for installation directories that reside in
# a path outside of the prefix in FHS and common usage.
BUILTIN_DIR_NOPREFIX_OPTIONS: T.Dict[OptionKey, T.Dict[str, str]] = {
OptionKey('sysconfdir'): {'/usr': '/etc'},
OptionKey('localstatedir'): {'/usr': '/var', '/usr/local': '/var/local'},
OptionKey('sharedstatedir'): {'/usr': '/var/lib', '/usr/local': '/var/local/lib'},
OptionKey('python.platlibdir'): {},
OptionKey('python.purelibdir'): {},
}
MSCRT_VALS = ['none', 'md', 'mdd', 'mt', 'mtd']
COMPILER_BASE_OPTIONS: T.Mapping[OptionKey, AnyOptionType] = {
OptionKey(o.name): o for o in T.cast('T.List[AnyOptionType]', [
UserBooleanOption('b_pch', 'Use precompiled headers', True),
UserBooleanOption('b_lto', 'Use link time optimization', False),
UserIntegerOption('b_lto_threads', 'Use multiple threads for Link Time Optimization', 0),
UserComboOption('b_lto_mode', 'Select between different LTO modes.', 'default', choices=['default', 'thin']),
UserBooleanOption('b_thinlto_cache', 'Use LLVM ThinLTO caching for faster incremental builds', False),
UserStringOption('b_thinlto_cache_dir', 'Directory to store ThinLTO cache objects', ''),
UserStringArrayOption('b_sanitize', 'Code sanitizer to use', []),
UserBooleanOption('b_lundef', 'Use -Wl,--no-undefined when linking', True),
UserBooleanOption('b_asneeded', 'Use -Wl,--as-needed when linking', True),
UserComboOption(
'b_pgo', 'Use profile guided optimization', 'off', choices=['off', 'generate', 'use']),
UserBooleanOption('b_coverage', 'Enable coverage tracking.', False),
UserComboOption(
'b_colorout', 'Use colored output', 'always', choices=['auto', 'always', 'never']),
UserComboOption(
'b_ndebug', 'Disable asserts', 'false', choices=['true', 'false', 'if-release']),
UserBooleanOption('b_staticpic', 'Build static libraries as position independent', True),
UserBooleanOption('b_pie', 'Build executables as position independent', False),
UserBooleanOption('b_bitcode', 'Generate and embed bitcode (only macOS/iOS/tvOS)', False),
UserComboOption(
'b_vscrt', 'VS run-time library type to use.', 'from_buildtype',
choices=MSCRT_VALS + ['from_buildtype', 'static_from_buildtype']),
])
}
class OptionStore:
DEFAULT_DEPENDENTS = {'plain': ('plain', False),
'debug': ('0', True),
'debugoptimized': ('2', True),
'release': ('3', False),
'minsize': ('s', True),
}
def __init__(self, is_cross: bool) -> None:
self.options: T.Dict['OptionKey', 'AnyOptionType'] = {}
self.subprojects: T.Set[str] = set()
self.project_options: T.Set[OptionKey] = set()
self.module_options: T.Set[OptionKey] = set()
from .compilers import all_languages
self.all_languages = set(all_languages)
self.augments: OptionDict = {}
self.is_cross = is_cross
# Pending options are configuration dependent options that could be
# initialized later, such as compiler options
self.pending_options: OptionDict = {}
# Subproject options from toplevel project()
self.pending_subproject_options: OptionDict = {}
def ensure_and_validate_key(self, key: T.Union[OptionKey, str]) -> OptionKey:
if isinstance(key, str):
return OptionKey(key)
# FIXME. When not cross building all "build" options need to fall back
# to "host" options due to how the old code worked.
#
# This is NOT how it should be.
#
# This needs to be changed to that trying to add or access "build" keys
# is a hard error and fix issues that arise.
#
# I did not do this yet, because it would make this MR even
# more massive than it already is. Later then.
if not self.is_cross and key.machine == MachineChoice.BUILD:
key = key.as_host()
return key
def get_pending_value(self, key: T.Union[OptionKey, str], default: T.Optional[ElementaryOptionValues] = None) -> ElementaryOptionValues:
key = self.ensure_and_validate_key(key)
if key in self.options:
return self.options[key].value
return self.pending_options.get(key, default)
def get_value(self, key: T.Union[OptionKey, str]) -> ElementaryOptionValues:
return self.get_value_for(key)
def __len__(self) -> int:
return len(self.options)
def get_value_object_for(self, key: 'T.Union[OptionKey, str]') -> AnyOptionType:
key = self.ensure_and_validate_key(key)
potential = self.options.get(key, None)
if self.is_project_option(key):
assert key.subproject is not None
if potential is None:
raise KeyError(f'Tried to access nonexistant project option {key}.')
else:
if potential is None:
parent_key = OptionKey(key.name, subproject=None, machine=key.machine)
if parent_key not in self.options:
raise KeyError(f'Tried to access nonexistant project parent option {parent_key}.')
# This is a global option but it can still have per-project
# augment, so return the subproject key.
return self.options[parent_key]
return potential
def get_value_object_and_value_for(self, key: OptionKey) -> T.Tuple[AnyOptionType, ElementaryOptionValues]:
assert isinstance(key, OptionKey)
key = self.ensure_and_validate_key(key)
vobject = self.get_value_object_for(key)
computed_value = vobject.value
if key in self.augments:
assert key.subproject is not None
computed_value = self.augments[key]
elif vobject.yielding:
computed_value = vobject.parent.value
return (vobject, computed_value)
def option_has_value(self, key: OptionKey, value: ElementaryOptionValues) -> bool:
vobject, current_value = self.get_value_object_and_value_for(key)
return vobject.validate_value(value) == current_value
def get_value_for(self, name: 'T.Union[OptionKey, str]', subproject: T.Optional[str] = None) -> ElementaryOptionValues:
if isinstance(name, str):
key = OptionKey(name, subproject)
else:
assert subproject is None
key = name
vobject, resolved_value = self.get_value_object_and_value_for(key)
return resolved_value
def add_system_option(self, key: T.Union[OptionKey, str], valobj: AnyOptionType) -> None:
key = self.ensure_and_validate_key(key)
if '.' in key.name:
raise MesonException(f'Internal error: non-module option has a period in its name {key.name}.')
self.add_system_option_internal(key, valobj)
def add_system_option_internal(self, key: OptionKey, valobj: AnyOptionType) -> None:
assert isinstance(valobj, UserOption)
if not isinstance(valobj.name, str):
assert isinstance(valobj.name, str)
if key in self.options:
return
pval = self.pending_options.pop(key, None)
if key.subproject:
proj_key = key.evolve(subproject=None)
self.add_system_option_internal(proj_key, valobj)
if pval is not None:
self.augments[key] = pval
else:
self.options[key] = valobj
if pval is not None:
self.set_option(key, pval)
def add_compiler_option(self, language: str, key: T.Union[OptionKey, str], valobj: AnyOptionType) -> None:
key = self.ensure_and_validate_key(key)
if not key.name.startswith(language + '_'):
raise MesonException(f'Internal error: all compiler option names must start with language prefix. ({key.name} vs {language}_)')
self.add_system_option(key, valobj)
def add_project_option(self, key: T.Union[OptionKey, str], valobj: AnyOptionType) -> None:
key = self.ensure_and_validate_key(key)
assert key.subproject is not None
if key in self.options:
raise MesonException(f'Internal error: tried to add a project option {key} that already exists.')
if valobj.yielding and key.subproject:
parent_key = key.as_root()
try:
parent_option = self.options[parent_key]
# If parent object has different type, do not yield.
# This should probably be an error.
if type(parent_option) is type(valobj):
valobj.parent = parent_option
except KeyError:
# Subproject is set to yield, but top level
# project does not have an option of the same
pass
valobj.yielding = bool(valobj.parent)
self.options[key] = valobj
self.project_options.add(key)
pval = self.pending_options.pop(key, None)
if pval is not None:
self.set_option(key, pval)
def add_module_option(self, modulename: str, key: T.Union[OptionKey, str], valobj: AnyOptionType) -> None:
key = self.ensure_and_validate_key(key)
if key.name.startswith('build.'):
raise MesonException('FATAL internal error: somebody goofed option handling.')
if not key.name.startswith(modulename + '.'):
raise MesonException('Internal error: module option name {key.name} does not start with module prefix {modulename}.')
self.add_system_option_internal(key, valobj)
self.module_options.add(key)
def sanitize_prefix(self, prefix: str) -> str:
prefix = os.path.expanduser(prefix)
if not os.path.isabs(prefix):
raise MesonException(f'prefix value {prefix!r} must be an absolute path')
if prefix.endswith('/') or prefix.endswith('\\'):
# On Windows we need to preserve the trailing slash if the
# string is of type 'C:\' because 'C:' is not an absolute path.
if len(prefix) == 3 and prefix[1] == ':':
pass
# If prefix is a single character, preserve it since it is
# the root directory.
elif len(prefix) == 1:
pass
else:
prefix = prefix[:-1]
return prefix
def sanitize_dir_option_value(self, prefix: str, option: OptionKey, value: T.Any) -> T.Any:
'''
If the option is an installation directory option, the value is an
absolute path and resides within prefix, return the value
as a path relative to the prefix. Otherwise, return it as is.
This way everyone can do f.ex, get_option('libdir') and usually get
the library directory relative to prefix, even though it really
should not be relied upon.
'''
try:
value = pathlib.PurePath(value)
except TypeError:
return value
if option.name.endswith('dir') and value.is_absolute() and \
option not in BUILTIN_DIR_NOPREFIX_OPTIONS:
try:
# Try to relativize the path.
value = value.relative_to(prefix)
except ValueError:
# Path is not relative, let’s keep it as is.
pass
if '..' in value.parts:
raise MesonException(
f"The value of the '{option}' option is '{value}' but "
"directory options are not allowed to contain '..'.\n"
f"If you need a path outside of the {prefix!r} prefix, "
"please use an absolute path."
)
# .as_posix() keeps the posix-like file separators Meson uses.
return value.as_posix()
def set_option(self, key: OptionKey, new_value: ElementaryOptionValues, first_invocation: bool = False) -> bool:
changed = False
error_key = key
if error_key.subproject == '':
error_key = error_key.evolve(subproject=None)
if key.name == 'prefix':
assert isinstance(new_value, str), 'for mypy'
new_value = self.sanitize_prefix(new_value)
elif self.is_builtin_option(key):
prefix = self.get_value_for('prefix')
assert isinstance(prefix, str), 'for mypy'
new_value = self.sanitize_dir_option_value(prefix, key, new_value)
try:
opt = self.get_value_object_for(key)
except KeyError:
raise MesonException(f'Unknown option: "{error_key}".')
if opt.deprecated is True:
mlog.deprecation(f'Option "{error_key}" is deprecated')
elif isinstance(opt.deprecated, list):
for v in opt.listify(new_value):
if v in opt.deprecated:
mlog.deprecation(f'Option "{error_key}" value {v!r} is deprecated')
elif isinstance(opt.deprecated, dict):
def replace(v: str) -> str:
assert isinstance(opt.deprecated, dict) # No, Mypy can not tell this from two lines above
newvalue = opt.deprecated.get(v)
if newvalue is not None:
mlog.deprecation(f'Option "{error_key}" value {v!r} is replaced by {newvalue!r}')
return newvalue
return v
valarr = [replace(v) for v in opt.listify(new_value)]
new_value = ','.join(valarr)
elif isinstance(opt.deprecated, str):
mlog.deprecation(f'Option "{error_key}" is replaced by {opt.deprecated!r}')
# Change both this aption and the new one pointed to.
changed |= self.set_option(key.evolve(name=opt.deprecated), new_value, first_invocation)
new_value = opt.validate_value(new_value)
if key in self.options:
old_value = opt.value
opt.set_value(new_value)
opt.yielding = False
else:
assert key.subproject is not None
old_value = self.augments.get(key, opt.value)
self.augments[key] = new_value
changed |= old_value != new_value
if opt.readonly and changed and not first_invocation:
raise MesonException(f'Tried to modify read only option "{error_key}"')
if key.name == 'prefix' and first_invocation and changed:
assert isinstance(old_value, str), 'for mypy'
assert isinstance(new_value, str), 'for mypy'
self.reset_prefixed_options(old_value, new_value)
if changed and key.name == 'buildtype' and new_value != 'custom':
assert isinstance(new_value, str), 'for mypy'
optimization, debug = self.DEFAULT_DEPENDENTS[new_value]
dkey = key.evolve(name='debug')
optkey = key.evolve(name='optimization')
self.set_option(dkey, debug, first_invocation)
self.set_option(optkey, optimization, first_invocation)
return changed
def set_user_option(self, o: OptionKey, new_value: ElementaryOptionValues, first_invocation: bool = False) -> bool:
if not self.is_cross and o.is_for_build():
return False
# This is complicated by the fact that a string can have two meanings:
#
# default_options: 'foo=bar'
#
# can be either
#
# A) a system option in which case the subproject is None
# B) a project option, in which case the subproject is ''
#
# The key parsing function can not handle the difference between the two
# and defaults to A.
if o in self.options:
return self.set_option(o, new_value, first_invocation)
# could also be an augment...
global_option = o.evolve(subproject=None)
if o.subproject is not None and global_option in self.options:
return self.set_option(o, new_value, first_invocation)
if self.accept_as_pending_option(o, first_invocation=first_invocation):
old_value = self.pending_options.get(o, None)
self.pending_options[o] = new_value
return old_value is None or str(old_value) != new_value
elif o.subproject is None:
o = o.as_root()
return self.set_option(o, new_value, first_invocation)
else:
raise MesonException(f'Unknown option: "{o}".')
def set_from_configure_command(self, D_args: T.Dict[OptionKey, T.Optional[str]]) -> bool:
dirty = False
for key, valstr in D_args.items():
if valstr is not None:
dirty |= self.set_user_option(key, valstr)
continue
if key in self.augments:
del self.augments[key]
dirty = True
else:
if key not in self.options:
raise MesonException(f"Unknown option: {key}")
# TODO: For project options, "dropping an augment" means going
# back to the superproject's value. However, it's confusing
# that -U does not simply remove the option from the stored
# cmd_line_options. This may cause "meson setup --wipe" to
# have surprising behavior. For this to work, UserOption
# should only store the default value and the option values
# should be stored with their source (project(), subproject(),
# machine file, command line). This way the effective value
# can be easily recomputed.
opt = self.get_value_object(key)
dirty |= not opt.yielding and bool(opt.parent)
opt.yielding = bool(opt.parent)
return dirty
def reset_prefixed_options(self, old_prefix: str, new_prefix: str) -> None:
for optkey, prefix_mapping in BUILTIN_DIR_NOPREFIX_OPTIONS.items():
valobj = self.options[optkey]
new_value = valobj.value
if new_prefix not in prefix_mapping:
new_value = BUILTIN_OPTIONS[optkey].default
else:
if old_prefix in prefix_mapping:
# Only reset the value if it has not been changed from the default.
if prefix_mapping[old_prefix] == valobj.value:
new_value = prefix_mapping[new_prefix]
else:
new_value = prefix_mapping[new_prefix]
valobj.set_value(new_value)
# FIXME, this should be removed.or renamed to "change_type_of_existing_object" or something like that
def set_value_object(self, key: T.Union[OptionKey, str], new_object: AnyOptionType) -> None:
key = self.ensure_and_validate_key(key)
self.options[key] = new_object
def get_value_object(self, key: T.Union[OptionKey, str]) -> AnyOptionType:
key = self.ensure_and_validate_key(key)
return self.options[key]
def get_default_for_b_option(self, key: OptionKey) -> ElementaryOptionValues:
assert self.is_base_option(key)
try:
return T.cast('ElementaryOptionValues', COMPILER_BASE_OPTIONS[key.evolve(subproject=None)].default)
except KeyError:
raise MesonBugException(f'Requested base option {key} which does not exist.')
def remove(self, key: OptionKey) -> None:
del self.options[key]
try:
self.project_options.remove(key)
except KeyError:
pass
def __contains__(self, key: T.Union[str, OptionKey]) -> bool:
key = self.ensure_and_validate_key(key)
return key in self.options
def __repr__(self) -> str:
return repr(self.options)
def keys(self) -> T.KeysView[OptionKey]:
return self.options.keys()
def values(self) -> T.ValuesView[AnyOptionType]:
return self.options.values()
def items(self) -> T.ItemsView['OptionKey', 'AnyOptionType']:
return self.options.items()
# FIXME: this method must be deleted and users moved to use "add_xxx_option"s instead.
def update(self, **kwargs: AnyOptionType) -> None:
self.options.update(**kwargs)
def setdefault(self, k: OptionKey, o: AnyOptionType) -> AnyOptionType:
return self.options.setdefault(k, o)
def get(self, o: OptionKey, default: T.Optional[AnyOptionType] = None, **kwargs: T.Any) -> T.Optional[AnyOptionType]:
return self.options.get(o, default, **kwargs)
def is_project_option(self, key: OptionKey) -> bool:
"""Convenience method to check if this is a project option."""
return key in self.project_options
def is_per_machine_option(self, optname: OptionKey) -> bool:
if optname.evolve(subproject=None, machine=MachineChoice.HOST) in BUILTIN_OPTIONS_PER_MACHINE:
return True
return self.is_compiler_option(optname)
def is_reserved_name(self, key: OptionKey) -> bool:
if key.name in _BUILTIN_NAMES:
return True
if '_' not in key.name:
return False
prefix = key.name.split('_')[0]
# Pylint seems to think that it is faster to build a set object
# and all related work just to test whether a string has one of two
# values. It is not, thank you very much.
if prefix in ('b', 'backend'): # pylint: disable=R6201
return True
if prefix in self.all_languages:
return True
return False
def is_builtin_option(self, key: OptionKey) -> bool:
"""Convenience method to check if this is a builtin option."""
return key.name in _BUILTIN_NAMES or self.is_module_option(key)
def is_base_option(self, key: OptionKey) -> bool:
"""Convenience method to check if this is a base option."""
return key.name.startswith('b_')
def is_backend_option(self, key: OptionKey) -> bool:
"""Convenience method to check if this is a backend option."""
if isinstance(key, str):
name: str = key
else:
name = key.name
return name.startswith('backend_')
def is_compiler_option(self, key: OptionKey) -> bool:
"""Convenience method to check if this is a compiler option."""
# FIXME, duplicate of is_reserved_name above. Should maybe store a cache instead.
if '_' not in key.name:
return False
prefix = key.name.split('_')[0]
if prefix in self.all_languages:
return True
return False
def is_module_option(self, key: OptionKey) -> bool:
return key in self.module_options
def prefix_split_options(self, coll: OptionDict) -> T.Tuple[T.Optional[str], OptionDict]:
prefix = None
others_d: OptionDict = {}
for k, v in coll.items():
if k.name == 'prefix':
if not isinstance(v, str):
raise MesonException('Incorrect type for prefix option (expected string)')
prefix = v
else:
others_d[k] = v
return (prefix, others_d)
def first_handle_prefix(self,
project_default_options: OptionDict,
cmd_line_options: OptionDict,
machine_file_options: OptionDict) \
-> T.Tuple[OptionDict, OptionDict, OptionDict]:
# Copy to avoid later mutation
nopref_machine_file_options = copy.copy(machine_file_options)
prefix = None
(possible_prefix, nopref_project_default_options) = self.prefix_split_options(project_default_options)
prefix = prefix if possible_prefix is None else possible_prefix
possible_prefixv = nopref_machine_file_options.pop(OptionKey('prefix'), None)
assert possible_prefixv is None or isinstance(possible_prefixv, str), 'mypy: prefix from machine file was not a string?'
prefix = prefix if possible_prefixv is None else possible_prefixv
(possible_prefix, nopref_cmd_line_options) = self.prefix_split_options(cmd_line_options)
prefix = prefix if possible_prefix is None else possible_prefix
if prefix is not None:
self.hard_reset_from_prefix(prefix)
return (nopref_project_default_options, nopref_cmd_line_options, nopref_machine_file_options)
def hard_reset_from_prefix(self, prefix: str) -> None:
prefix = self.sanitize_prefix(prefix)
for optkey, prefix_mapping in BUILTIN_DIR_NOPREFIX_OPTIONS.items():
valobj = self.options[optkey]
if prefix in prefix_mapping:
new_value = prefix_mapping[prefix]
else:
_v = BUILTIN_OPTIONS[optkey].default
assert isinstance(_v, str), 'for mypy'
new_value = _v
valobj.set_value(new_value)
self.options[OptionKey('prefix')].set_value(prefix)
def initialize_from_top_level_project_call(self,
project_default_options_in: OptionDict,
cmd_line_options_in: OptionDict,
machine_file_options_in: OptionDict) -> None:
(project_default_options, cmd_line_options, machine_file_options) = self.first_handle_prefix(project_default_options_in,
cmd_line_options_in,
machine_file_options_in)
for key, valstr in project_default_options.items():
# Due to backwards compatibility we ignore build-machine options
# when building natively.
if not self.is_cross and key.is_for_build():
continue
if key.subproject:
# Subproject options from toplevel project() have low priority
# and will be processed when the subproject is found
self.pending_subproject_options[key] = valstr
else:
# Setting a project option with default_options
# should arguably be a hard error; the default
# value of project option should be set in the option
# file, not in the project call.
self.set_user_option(key, valstr, True)
# ignore subprojects for now for machine file and command line
# options; they are applied later
for key, valstr in machine_file_options.items():
# Due to backwards compatibility we ignore all build-machine options
# when building natively.
if not self.is_cross and key.is_for_build():
continue
if not key.subproject:
self.set_user_option(key, valstr, True)
for key, valstr in cmd_line_options.items():
# Due to backwards compatibility we ignore all build-machine options
# when building natively.
if not self.is_cross and key.is_for_build():
continue
if not key.subproject:
self.set_user_option(key, valstr, True)
def accept_as_pending_option(self, key: OptionKey, first_invocation: bool = False) -> bool:
# Some base options (sanitizers etc) might get added later.
# Permitting them all is not strictly correct.
if self.is_compiler_option(key):
return True
if first_invocation and self.is_backend_option(key):
return True
return (self.is_base_option(key) and
key.evolve(subproject=None, machine=MachineChoice.HOST) in COMPILER_BASE_OPTIONS)
def validate_cmd_line_options(self, cmd_line_options: OptionDict) -> None:
unknown_options = []
for key, valstr in cmd_line_options.items():
if key in self.pending_options and not self.accept_as_pending_option(key):
unknown_options.append(f'"{key}"')
if unknown_options:
keys = ', '.join(unknown_options)
raise MesonException(f'Unknown options: {keys}')
def initialize_from_subproject_call(self,
subproject: str,
spcall_default_options: OptionDict,
project_default_options: OptionDict,
cmd_line_options: OptionDict,
machine_file_options: OptionDict) -> None:
options: OptionDict = {}
# project() default_options
for key, valstr in project_default_options.items():
if key.subproject == subproject:
without_subp = key.evolve(subproject=None)
raise MesonException(f'subproject name not needed in default_options; use "{without_subp}" instead of "{key}"')
if key.subproject is None:
key = key.evolve(subproject=subproject)
options[key] = valstr
# augments from the toplevel project() default_options
for key, valstr in self.pending_subproject_options.items():
if key.subproject == subproject:
options[key] = valstr
# subproject() default_options
for key, valstr in spcall_default_options.items():
if key.subproject == subproject:
without_subp = key.evolve(subproject=None)
raise MesonException(f'subproject name not needed in default_options; use "{without_subp}" instead of "{key}"')
if key.subproject is None:
key = key.evolve(subproject=subproject)
options[key] = valstr
# then global settings from machine file and command line
# **but not if they are toplevel project options**
for key, valstr in itertools.chain(machine_file_options.items(), cmd_line_options.items()):
if key.subproject is None and not self.is_project_option(key.as_root()):
subp_key = key.evolve(subproject=subproject)
self.pending_subproject_options.pop(subp_key, None)
options.pop(subp_key, None)
# then finally per project augments from machine file and command line
for key, valstr in itertools.chain(machine_file_options.items(), cmd_line_options.items()):
if key.subproject == subproject:
options[key] = valstr
# merge everything that has been computed above, while giving self.augments priority
for key, valstr in options.items():
if key.subproject != subproject:
if key.subproject in self.subprojects and not self.option_has_value(key, valstr):
mlog.warning('option {key} is set in subproject {subproject} but has already been processed')
continue
# Subproject options from project() will be processed when the subproject is found
self.pending_subproject_options[key] = valstr
continue
self.pending_subproject_options.pop(key, None)
self.pending_options.pop(key, None)
if key not in self.augments:
self.set_user_option(key, valstr, True)
self.subprojects.add(subproject)
def update_project_options(self, project_options: MutableKeyedOptionDictType, subproject: SubProject) -> None:
for key, value in project_options.items():
if key not in self.options:
self.add_project_option(key, value)
continue
if key.subproject != subproject:
raise MesonBugException(f'Tried to set an option for subproject {key.subproject} from {subproject}!')
oldval = self.get_value_object(key)
if type(oldval) is not type(value):
self.set_option(key, value.value)
elif choices_are_different(oldval, value):
# If the choices have changed, use the new value, but attempt
# to keep the old options. If they are not valid keep the new
# defaults but warn.
self.set_value_object(key, value)
try:
value.set_value(oldval.value)
except MesonException:
mlog.warning(f'Old value(s) of {key} are no longer valid, resetting to default ({value.value}).',
fatal=False)
# Find any extranious keys for this project and remove them
potential_removed_keys = self.options.keys() - project_options.keys()
for key in potential_removed_keys:
if self.is_project_option(key) and key.subproject == subproject:
self.remove(key)