diff options
Diffstat (limited to 'mesonbuild/options.py')
-rw-r--r-- | mesonbuild/options.py | 480 |
1 files changed, 480 insertions, 0 deletions
diff --git a/mesonbuild/options.py b/mesonbuild/options.py new file mode 100644 index 0000000..0a20539 --- /dev/null +++ b/mesonbuild/options.py @@ -0,0 +1,480 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright 2013-2024 Contributors to the The Meson project + +from dataclasses import dataclass +from collections import OrderedDict, abc +from itertools import chain +import argparse + +from .mesonlib import ( + HoldableObject, + OptionKey, + default_prefix, + default_datadir, + default_includedir, + default_datadir, + default_infodir, + default_libdir, + default_libexecdir, + default_localedir, + default_mandir, + default_sbindir, + default_sysconfdir, + MesonException, + listify_array_value, +) + +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 |