| # SPDX-License-Identifier: Apache-2.0 |
| # Copyright 2013-2024 Contributors to the The Meson project |
| |
| from collections import OrderedDict |
| from itertools import chain |
| import argparse |
| |
| from .mesonlib import ( |
| HoldableObject, |
| OptionKey, |
| default_prefix, |
| default_datadir, |
| default_includedir, |
| default_infodir, |
| default_libdir, |
| default_libexecdir, |
| default_localedir, |
| default_mandir, |
| default_sbindir, |
| default_sysconfdir, |
| MesonException, |
| listify_array_value, |
| ) |
| |
| from . import mlog |
| |
| import typing as T |
| |
| 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'] |
| |
| |
| class UserOption(T.Generic[_T], HoldableObject): |
| def __init__(self, name: str, description: str, choices: T.Optional[T.Union[str, T.List[_T]]], |
| yielding: bool, |
| deprecated: T.Union[bool, str, T.Dict[str, str], T.List[str]] = False): |
| super().__init__() |
| self.name = name |
| self.choices = choices |
| self.description = description |
| if not isinstance(yielding, bool): |
| raise MesonException('Value of "yielding" must be a boolean.') |
| self.yielding = yielding |
| self.deprecated = deprecated |
| self.readonly = False |
| |
| def listify(self, value: T.Any) -> T.List[T.Any]: |
| return [value] |
| |
| def printable_value(self) -> T.Union[str, int, bool, T.List[T.Union[str, int, bool]]]: |
| assert isinstance(self.value, (str, int, bool, list)) |
| return self.value |
| |
| # 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: T.Any) -> _T: |
| raise RuntimeError('Derived option class did not override validate_value.') |
| |
| def set_value(self, newvalue: T.Any) -> bool: |
| oldvalue = getattr(self, 'value', None) |
| self.value = self.validate_value(newvalue) |
| return self.value != oldvalue |
| |
| _U = T.TypeVar('_U', bound=UserOption[_T]) |
| |
| |
| class UserStringOption(UserOption[str]): |
| def __init__(self, name: str, description: str, value: T.Any, yielding: bool = DEFAULT_YIELDING, |
| deprecated: T.Union[bool, str, T.Dict[str, str], T.List[str]] = False): |
| super().__init__(name, description, None, yielding, deprecated) |
| self.set_value(value) |
| |
| def validate_value(self, value: T.Any) -> str: |
| if not isinstance(value, str): |
| raise MesonException(f'The value of option "{self.name}" is "{value}", which is not a string.') |
| return value |
| |
| class UserBooleanOption(UserOption[bool]): |
| def __init__(self, name: str, description: str, value: bool, yielding: bool = DEFAULT_YIELDING, |
| deprecated: T.Union[bool, str, T.Dict[str, str], T.List[str]] = False): |
| super().__init__(name, description, [True, False], yielding, deprecated) |
| self.set_value(value) |
| |
| def __bool__(self) -> bool: |
| return self.value |
| |
| def validate_value(self, value: T.Any) -> 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 UserIntegerOption(UserOption[int]): |
| def __init__(self, name: str, description: str, value: T.Any, yielding: bool = DEFAULT_YIELDING, |
| deprecated: T.Union[bool, str, T.Dict[str, str], T.List[str]] = False): |
| min_value, max_value, default_value = value |
| self.min_value = min_value |
| self.max_value = max_value |
| c: T.List[str] = [] |
| if min_value is not None: |
| c.append('>=' + str(min_value)) |
| if max_value is not None: |
| c.append('<=' + str(max_value)) |
| choices = ', '.join(c) |
| super().__init__(name, description, choices, yielding, deprecated) |
| self.set_value(default_value) |
| |
| def validate_value(self, value: T.Any) -> int: |
| if isinstance(value, str): |
| value = 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 value |
| |
| 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)) |
| |
| class UserUmaskOption(UserIntegerOption, UserOption[T.Union[str, OctalInt]]): |
| def __init__(self, name: str, description: str, value: T.Any, yielding: bool = DEFAULT_YIELDING, |
| deprecated: T.Union[bool, str, T.Dict[str, str], T.List[str]] = False): |
| super().__init__(name, description, (0, 0o777, value), yielding, deprecated) |
| self.choices = ['preserve', '0000-0777'] |
| |
| def printable_value(self) -> str: |
| if self.value == 'preserve': |
| return self.value |
| return format(self.value, '04o') |
| |
| def validate_value(self, value: T.Any) -> T.Union[str, OctalInt]: |
| if value == 'preserve': |
| return 'preserve' |
| return OctalInt(super().validate_value(value)) |
| |
| def toint(self, valuestring: T.Union[str, OctalInt]) -> int: |
| try: |
| return int(valuestring, 8) |
| except ValueError as e: |
| raise MesonException(f'Invalid mode for option "{self.name}" {e}') |
| |
| class UserComboOption(UserOption[str]): |
| def __init__(self, name: str, description: str, choices: T.List[str], value: T.Any, |
| yielding: bool = DEFAULT_YIELDING, |
| deprecated: T.Union[bool, str, T.Dict[str, str], T.List[str]] = False): |
| super().__init__(name, description, choices, yielding, deprecated) |
| if not isinstance(self.choices, list): |
| raise MesonException(f'Combo choices for option "{self.name}" must be an array.') |
| for i in self.choices: |
| if not isinstance(i, str): |
| raise MesonException(f'Combo choice elements for option "{self.name}" must be strings.') |
| self.set_value(value) |
| |
| def validate_value(self, value: T.Any) -> 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)) |
| return value |
| |
| class UserArrayOption(UserOption[T.List[str]]): |
| def __init__(self, name: str, description: str, value: T.Union[str, T.List[str]], |
| split_args: bool = False, |
| allow_dups: bool = False, yielding: bool = DEFAULT_YIELDING, |
| choices: T.Optional[T.List[str]] = None, |
| deprecated: T.Union[bool, str, T.Dict[str, str], T.List[str]] = False): |
| super().__init__(name, description, choices if choices is not None else [], yielding, deprecated) |
| self.split_args = split_args |
| self.allow_dups = allow_dups |
| self.set_value(value) |
| |
| def listify(self, value: T.Any) -> T.List[T.Any]: |
| 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: T.Union[str, T.List[str]]) -> 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 the future.' |
| 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 |
| |
| 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) |
| |
| |
| class UserFeatureOption(UserComboOption): |
| static_choices = ['enabled', 'disabled', 'auto'] |
| |
| def __init__(self, name: str, description: str, value: T.Any, yielding: bool = DEFAULT_YIELDING, |
| deprecated: T.Union[bool, str, T.Dict[str, str], T.List[str]] = False): |
| super().__init__(name, description, self.static_choices, value, yielding, deprecated) |
| self.name: T.Optional[str] = None # TODO: Refactor options to all store their name |
| |
| 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' |
| |
| 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'], '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: T.Union[str, T.List[str]]) -> str: |
| try: |
| candidates = listify_array_value(value) |
| except MesonException as e: |
| raise MesonException(f'error in option "{self.name}": {e!s}') |
| 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 the future.\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.') |
| 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}') |
| |
| |
| class BuiltinOption(T.Generic[_T, _U]): |
| |
| """Class for a builtin option type. |
| |
| There are some cases that are not fully supported yet. |
| """ |
| |
| def __init__(self, opt_type: T.Type[_U], description: str, default: T.Any, yielding: bool = True, *, |
| choices: T.Any = None, readonly: bool = False): |
| self.opt_type = opt_type |
| self.description = description |
| self.default = default |
| self.choices = choices |
| self.yielding = yielding |
| self.readonly = readonly |
| |
| def init_option(self, name: 'OptionKey', value: T.Optional[T.Any], prefix: str) -> _U: |
| """Create an instance of opt_type and return it.""" |
| if value is None: |
| value = self.prefixed_default(name, prefix) |
| keywords = {'yielding': self.yielding, 'value': value} |
| if self.choices: |
| keywords['choices'] = self.choices |
| o = self.opt_type(name.name, self.description, **keywords) |
| o.readonly = self.readonly |
| return o |
| |
| def _argparse_action(self) -> T.Optional[str]: |
| # If the type is a boolean, the presence of the argument in --foo form |
| # is to enable it. Disabling happens by using -Dfoo=false, which is |
| # parsed under `args.projectoptions` and does not hit this codepath. |
| if isinstance(self.default, bool): |
| return 'store_true' |
| return None |
| |
| def _argparse_choices(self) -> T.Any: |
| if self.opt_type is UserBooleanOption: |
| return [True, False] |
| elif self.opt_type is UserFeatureOption: |
| return UserFeatureOption.static_choices |
| return self.choices |
| |
| @staticmethod |
| def argparse_name_to_arg(name: str) -> str: |
| if name == 'warning_level': |
| return '--warnlevel' |
| else: |
| return '--' + name.replace('_', '-') |
| |
| def prefixed_default(self, name: 'OptionKey', prefix: str = '') -> T.Any: |
| if self.opt_type in [UserComboOption, UserIntegerOption]: |
| return self.default |
| try: |
| return BUILTIN_DIR_NOPREFIX_OPTIONS[name][prefix] |
| except KeyError: |
| pass |
| return self.default |
| |
| def add_to_argparse(self, name: str, parser: argparse.ArgumentParser, help_suffix: str) -> None: |
| kwargs = OrderedDict() |
| |
| c = self._argparse_choices() |
| b = self._argparse_action() |
| h = self.description |
| if not b: |
| h = '{} (default: {}).'.format(h.rstrip('.'), self.prefixed_default(name)) |
| else: |
| kwargs['action'] = b |
| if c and not b: |
| kwargs['choices'] = c |
| kwargs['default'] = argparse.SUPPRESS |
| kwargs['dest'] = name |
| |
| cmdline_name = self.argparse_name_to_arg(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.Dict['OptionKey', 'BuiltinOption'] = OrderedDict([ |
| (OptionKey('prefix'), BuiltinOption(UserStringOption, 'Installation prefix', default_prefix())), |
| (OptionKey('bindir'), BuiltinOption(UserStringOption, 'Executable directory', 'bin')), |
| (OptionKey('datadir'), BuiltinOption(UserStringOption, 'Data file directory', default_datadir())), |
| (OptionKey('includedir'), BuiltinOption(UserStringOption, 'Header file directory', default_includedir())), |
| (OptionKey('infodir'), BuiltinOption(UserStringOption, 'Info page directory', default_infodir())), |
| (OptionKey('libdir'), BuiltinOption(UserStringOption, 'Library directory', default_libdir())), |
| (OptionKey('licensedir'), BuiltinOption(UserStringOption, 'Licenses directory', '')), |
| (OptionKey('libexecdir'), BuiltinOption(UserStringOption, 'Library executable directory', default_libexecdir())), |
| (OptionKey('localedir'), BuiltinOption(UserStringOption, 'Locale data directory', default_localedir())), |
| (OptionKey('localstatedir'), BuiltinOption(UserStringOption, 'Localstate data directory', 'var')), |
| (OptionKey('mandir'), BuiltinOption(UserStringOption, 'Manual page directory', default_mandir())), |
| (OptionKey('sbindir'), BuiltinOption(UserStringOption, 'System executable directory', default_sbindir())), |
| (OptionKey('sharedstatedir'), BuiltinOption(UserStringOption, 'Architecture-independent data directory', 'com')), |
| (OptionKey('sysconfdir'), BuiltinOption(UserStringOption, 'Sysconf data directory', default_sysconfdir())), |
| ]) |
| |
| BUILTIN_CORE_OPTIONS: T.Dict['OptionKey', 'BuiltinOption'] = OrderedDict([ |
| (OptionKey('auto_features'), BuiltinOption(UserFeatureOption, "Override value of all 'auto' features", 'auto')), |
| (OptionKey('backend'), BuiltinOption(UserComboOption, 'Backend to use', 'ninja', choices=backendlist, |
| readonly=True)), |
| (OptionKey('genvslite'), |
| BuiltinOption( |
| UserComboOption, |
| '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) |
| ), |
| (OptionKey('buildtype'), BuiltinOption(UserComboOption, 'Build type to use', 'debug', |
| choices=buildtypelist)), |
| (OptionKey('debug'), BuiltinOption(UserBooleanOption, 'Enable debug symbols and other information', True)), |
| (OptionKey('default_library'), BuiltinOption(UserComboOption, 'Default library type', 'shared', choices=['shared', 'static', 'both'], |
| yielding=False)), |
| (OptionKey('errorlogs'), BuiltinOption(UserBooleanOption, "Whether to print the logs from failing tests", True)), |
| (OptionKey('install_umask'), BuiltinOption(UserUmaskOption, 'Default umask to apply on permissions of installed files', '022')), |
| (OptionKey('layout'), BuiltinOption(UserComboOption, 'Build directory layout', 'mirror', choices=['mirror', 'flat'])), |
| (OptionKey('optimization'), BuiltinOption(UserComboOption, 'Optimization level', '0', choices=['plain', '0', 'g', '1', '2', '3', 's'])), |
| (OptionKey('prefer_static'), BuiltinOption(UserBooleanOption, 'Whether to try static linking before shared linking', False)), |
| (OptionKey('stdsplit'), BuiltinOption(UserBooleanOption, 'Split stdout and stderr in test logs', True)), |
| (OptionKey('strip'), BuiltinOption(UserBooleanOption, 'Strip targets on install', False)), |
| (OptionKey('unity'), BuiltinOption(UserComboOption, 'Unity build', 'off', choices=['on', 'off', 'subprojects'])), |
| (OptionKey('unity_size'), BuiltinOption(UserIntegerOption, 'Unity block size', (2, None, 4))), |
| (OptionKey('warning_level'), BuiltinOption(UserComboOption, 'Compiler warning level to use', '1', choices=['0', '1', '2', '3', 'everything'], yielding=False)), |
| (OptionKey('werror'), BuiltinOption(UserBooleanOption, 'Treat warnings as errors', False, yielding=False)), |
| (OptionKey('wrap_mode'), BuiltinOption(UserComboOption, 'Wrap mode', 'default', choices=['default', 'nofallback', 'nodownload', 'forcefallback', 'nopromote'])), |
| (OptionKey('force_fallback_for'), BuiltinOption(UserArrayOption, 'Force fallback for those subprojects', [])), |
| (OptionKey('vsenv'), BuiltinOption(UserBooleanOption, 'Activate Visual Studio environment', False, readonly=True)), |
| |
| # Pkgconfig module |
| (OptionKey('relocatable', module='pkgconfig'), |
| BuiltinOption(UserBooleanOption, 'Generate pkgconfig files as relocatable', False)), |
| |
| # Python module |
| (OptionKey('bytecompile', module='python'), |
| BuiltinOption(UserIntegerOption, 'Whether to compile bytecode', (-1, 2, 0))), |
| (OptionKey('install_env', module='python'), |
| BuiltinOption(UserComboOption, 'Which python environment to install to', 'prefix', choices=['auto', 'prefix', 'system', 'venv'])), |
| (OptionKey('platlibdir', module='python'), |
| BuiltinOption(UserStringOption, 'Directory for site-specific, platform-specific files.', '')), |
| (OptionKey('purelibdir', module='python'), |
| BuiltinOption(UserStringOption, 'Directory for site-specific, non-platform-specific files.', '')), |
| (OptionKey('allow_limited_api', module='python'), |
| BuiltinOption(UserBooleanOption, '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.Dict['OptionKey', 'BuiltinOption'] = OrderedDict([ |
| (OptionKey('pkg_config_path'), BuiltinOption(UserArrayOption, 'List of additional paths for pkg-config to search', [])), |
| (OptionKey('cmake_prefix_path'), BuiltinOption(UserArrayOption, '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('platlibdir', module='python'): {}, |
| OptionKey('purelibdir', module='python'): {}, |
| } |
| |
| |
| class OptionStore: |
| def __init__(self): |
| # This class will hold all options for a given build directory |
| self.dummy = None |