aboutsummaryrefslogtreecommitdiff
path: root/mesonbuild/options.py
diff options
context:
space:
mode:
Diffstat (limited to 'mesonbuild/options.py')
-rw-r--r--mesonbuild/options.py480
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