aboutsummaryrefslogtreecommitdiff
path: root/mesonbuild/options.py
diff options
context:
space:
mode:
authorJussi Pakkanen <jpakkane@gmail.com>2024-04-14 12:58:30 +0300
committerJussi Pakkanen <jpakkane@gmail.com>2025-02-13 15:26:44 +0200
commit518c732ea9b0f1975f6f28accff3286be4106538 (patch)
tree2be9544828545a2f9f0676efc6eb82ac197ec4bd /mesonbuild/options.py
parentea678ed82938ceac00682b2695b57193d36b71b4 (diff)
downloadmeson-optionrefactor3.zip
meson-optionrefactor3.tar.gz
meson-optionrefactor3.tar.bz2
Make all Meson level options overridable per subproject.optionrefactor3
Diffstat (limited to 'mesonbuild/options.py')
-rw-r--r--mesonbuild/options.py635
1 files changed, 580 insertions, 55 deletions
diff --git a/mesonbuild/options.py b/mesonbuild/options.py
index c31254d..7c22332 100644
--- a/mesonbuild/options.py
+++ b/mesonbuild/options.py
@@ -8,6 +8,10 @@ from itertools import chain
from functools import total_ordering
import argparse
import dataclasses
+import itertools
+import os
+import pathlib
+
import typing as T
from .mesonlib import (
@@ -23,6 +27,7 @@ from .mesonlib import (
default_sbindir,
default_sysconfdir,
MesonException,
+ MesonBugException,
listify_array_value,
MachineChoice,
)
@@ -45,6 +50,8 @@ if T.TYPE_CHECKING:
default: str
choices: T.List
+ OptionValueType: TypeAlias = T.Union[str, int, bool, T.List[str]]
+
DEFAULT_YIELDING = False
# Can't bind this near the class method it seems, sadly.
@@ -54,7 +61,6 @@ backendlist = ['ninja', 'vs', 'vs2010', 'vs2012', 'vs2013', 'vs2015', 'vs2017',
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...
@@ -98,6 +104,8 @@ _BUILTIN_NAMES = {
'vsenv',
}
+_BAD_VALUE = 'Qwert Zuiopü'
+
@total_ordering
class OptionKey:
@@ -111,16 +119,23 @@ class OptionKey:
__slots__ = ['name', 'subproject', 'machine', '_hash']
name: str
- subproject: str
+ subproject: T.Optional[str] # None is global, empty string means top level project
machine: MachineChoice
_hash: int
- def __init__(self, name: str, subproject: str = '',
+ def __init__(self,
+ name: str,
+ subproject: T.Optional[str] = None,
machine: MachineChoice = MachineChoice.HOST):
+ 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}')
# the _type option to the constructor is kinda private. We want to be
# able to save the state and avoid the lookup function when
# pickling/unpickling, but we need to be able to calculate it when
# constructing a new OptionKey
+ assert ':' not in name
object.__setattr__(self, 'name', name)
object.__setattr__(self, 'subproject', subproject)
object.__setattr__(self, 'machine', machine)
@@ -160,6 +175,10 @@ class OptionKey:
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
@@ -167,7 +186,7 @@ class OptionKey:
out = self.name
if self.machine is MachineChoice.BUILD:
out = f'build.{out}'
- if self.subproject:
+ if self.subproject is not None:
out = f'{self.subproject}:{out}'
return out
@@ -181,10 +200,11 @@ class OptionKey:
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 = '', raw
+ subproject, raw2 = None, raw
for_machine = MachineChoice.HOST
try:
@@ -202,7 +222,9 @@ class OptionKey:
return cls(opt, subproject, for_machine)
- def evolve(self, name: T.Optional[str] = None, subproject: T.Optional[str] = None,
+ 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.
@@ -214,14 +236,14 @@ class OptionKey:
"""
# 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 is not None else self.subproject,
- machine if machine is not None else self.machine,
- )
+ 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='')."""
+ if self.subproject is None or self.subproject == '':
+ return self
return self.evolve(subproject='')
def as_build(self) -> 'OptionKey':
@@ -232,11 +254,6 @@ class OptionKey:
"""Convenience method for key.evolve(machine=MachineChoice.HOST)."""
return self.evolve(machine=MachineChoice.HOST)
- def is_project_hack_for_optionsview(self) -> bool:
- """This method will be removed once we can delete OptionsView."""
- import sys
- sys.exit('FATAL internal error. This should not make it into an actual release. File a bug.')
-
def has_module_prefix(self) -> bool:
return '.' in self.name
@@ -251,6 +268,11 @@ class OptionKey:
return self.evolve(newname)
return self
+ def is_for_build(self) -> bool:
+ return self.machine is MachineChoice.BUILD
+
+if T.TYPE_CHECKING:
+ OptionStringLikeDict: TypeAlias = T.Dict[T.Union[OptionKey, str], str]
@dataclasses.dataclass
class UserOption(T.Generic[_T], HoldableObject):
@@ -286,7 +308,6 @@ class UserOption(T.Generic[_T], HoldableObject):
self.value = self.validate_value(newvalue)
return self.value != oldvalue
-
@dataclasses.dataclass
class EnumeratedUserOption(UserOption[_T]):
@@ -298,7 +319,6 @@ class EnumeratedUserOption(UserOption[_T]):
return [str(c) for c in self.choices]
-@dataclasses.dataclass
class UserStringOption(UserOption[str]):
def validate_value(self, value: T.Any) -> str:
@@ -649,7 +669,6 @@ class BuiltinOption(T.Generic[_T]):
cmdline_name = self.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/
@@ -737,52 +756,138 @@ BUILTIN_DIR_NOPREFIX_OPTIONS: T.Dict[OptionKey, T.Dict[str, str]] = {
OptionKey('python.purelibdir'): {},
}
+
class OptionStore:
- def __init__(self) -> None:
- self.d: T.Dict['OptionKey', AnyOptionType] = {}
+ 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.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.build_options = None
+ self.project_options = set()
+ self.augments: T.Dict[str, str] = {}
+ self.pending_project_options: T.Dict[OptionKey, str] = {}
+ self.is_cross = is_cross
- def __len__(self) -> int:
- return len(self.d)
+ def clear_pending(self) -> None:
+ self.pending_project_options = {}
- def ensure_key(self, key: T.Union[OptionKey, str]) -> OptionKey:
+ 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.evolve(machine=MachineChoice.HOST)
return key
- def get_value_object(self, key: T.Union[OptionKey, str]) -> AnyOptionType:
- return self.d[self.ensure_key(key)]
-
- def get_value(self, key: T.Union[OptionKey, str]) -> 'T.Any':
+ def get_value(self, key: T.Union[OptionKey, str]) -> 'OptionValueType':
return self.get_value_object(key).value
+ 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 not None and potential.yielding:
+ parent_key = key.evolve(subproject='')
+ 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(potential):
+ return parent_option
+ return potential
+ if potential is None:
+ raise KeyError(f'Tried to access nonexistant project option {key}.')
+ return potential
+ else:
+ if potential is None:
+ parent_key = key.evolve(subproject=None)
+ if parent_key not in self.options:
+ raise KeyError(f'Tried to access nonexistant project parent option {parent_key}.')
+ return self.options[parent_key]
+ return potential
+
+ def get_value_object_and_value_for(self, key: OptionKey) -> 'T.Tuple[AnyOptionType, OptionValueType]':
+ assert isinstance(key, OptionKey)
+ vobject = self.get_value_object_for(key)
+ computed_value = vobject.value
+ if key.subproject is not None:
+ keystr = str(key)
+ if keystr in self.augments:
+ computed_value = vobject.validate_value(self.augments[keystr])
+ return (vobject, computed_value)
+
+ def get_value_for(self, name: 'T.Union[OptionKey, str]', subproject: T.Optional[str] = None) -> 'OptionValueType':
+ 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 num_options(self) -> int:
+ basic = len(self.options)
+ build = len(self.build_options) if self.build_options else 0
+ return basic + build
+
def add_system_option(self, key: T.Union[OptionKey, str], valobj: AnyOptionType) -> None:
- key = self.ensure_key(key)
+ 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: T.Union[OptionKey, str], valobj: AnyOptionType) -> None:
- key = self.ensure_key(key)
+ key = self.ensure_and_validate_key(key)
assert isinstance(valobj, UserOption)
- self.d[key] = valobj
+ if not isinstance(valobj.name, str):
+ assert isinstance(valobj.name, str)
+ if key not in self.options:
+ self.options[key] = valobj
+ pval = self.pending_project_options.pop(key, None)
+ 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_key(key)
+ 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_key(key)
- self.d[key] = valobj
- self.project_options.add(key)
+ key = self.ensure_and_validate_key(key)
+ assert key.subproject is not None
+ pval = self.pending_project_options.pop(key, None)
+ if key in self.options:
+ raise MesonException(f'Internal error: tried to add a project option {key} that already exists.')
+ else:
+ self.options[key] = valobj
+ self.project_options.add(key)
+ 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_key(key)
+ 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 + '.'):
@@ -790,43 +895,244 @@ class OptionStore:
self.add_system_option_internal(key, valobj)
self.module_options.add(key)
- def set_value(self, key: T.Union[OptionKey, str], new_value: 'T.Any') -> bool:
- key = self.ensure_key(key)
- return self.d[key].set_value(new_value)
+ 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_value(self, key: T.Union[OptionKey, str], new_value: 'T.Any', first_invocation: bool = False) -> bool:
+ key = self.ensure_and_validate_key(key)
+ if key.name == 'prefix':
+ new_value = self.sanitize_prefix(new_value)
+ elif self.is_builtin_option(key):
+ prefix = self.get_value_for('prefix')
+ assert isinstance(prefix, str)
+ new_value = self.sanitize_dir_option_value(prefix, key, new_value)
+ if key not in self.options:
+ raise MesonException(f'Unknown options: "{key.name}" not found.')
+
+ valobj = self.options[key]
+ old_value = valobj.value
+ changed = valobj.set_value(new_value)
+
+ if valobj.readonly and changed and not first_invocation:
+ raise MesonException(f'Tried to modify read only option {str(key)!r}')
+
+ if key.name == 'prefix' and first_invocation and changed:
+ assert isinstance(old_value, str)
+ self.reset_prefixed_options(old_value, new_value)
+
+ if changed:
+ self.set_dependents(key, new_value)
+
+ return changed
+
+ def set_dependents(self, key: OptionKey, value: str) -> None:
+ if key.name != 'buildtype':
+ return
+ opt, debug = self.DEFAULT_DEPENDENTS[value]
+ dkey = key.evolve(name='debug')
+ optkey = key.evolve(name='optimization')
+ self.options[dkey].set_value(debug)
+ self.options[optkey].set_value(opt)
+
+ def set_option(self, key: OptionKey, new_value: str, first_invocation: bool = False) -> bool:
+ assert isinstance(key, OptionKey)
+ # FIXME, dupe of set_value
+ # Remove one of the two before merging to master.
+ if key.name == 'prefix':
+ new_value = self.sanitize_prefix(new_value)
+ elif self.is_builtin_option(key):
+ prefix = self.get_value_for('prefix')
+ assert isinstance(prefix, str)
+ new_value = self.sanitize_dir_option_value(prefix, key, new_value)
+ opt = self.get_value_object_for(key)
+ if opt.deprecated is True:
+ mlog.deprecation(f'Option {key.name!r} is deprecated')
+ elif isinstance(opt.deprecated, list):
+ for v in opt.listify(new_value):
+ if v in opt.deprecated:
+ mlog.deprecation(f'Option {key.name!r} value {v!r} is deprecated')
+ elif isinstance(opt.deprecated, dict):
+ def replace(v: T.Any) -> T.Any:
+ 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 {key.name!r} 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 {key.name!r} is replaced by {opt.deprecated!r}')
+ # Change both this aption and the new one pointed to.
+ dirty = self.set_option(key.evolve(name=opt.deprecated), new_value)
+ dirty |= opt.set_value(new_value)
+ return dirty
+
+ old_value = opt.value
+ changed = opt.set_value(new_value)
+
+ if opt.readonly and changed:
+ raise MesonException(f'Tried modify read only option {str(key)!r}')
+
+ if key.name == 'prefix' and first_invocation and changed:
+ assert isinstance(old_value, str), 'for mypy'
+ self.reset_prefixed_options(old_value, new_value)
+
+ if changed:
+ self.set_dependents(key, new_value)
+
+ return changed
+
+ def set_option_from_string(self, keystr: T.Union[OptionKey, str], new_value: str) -> bool:
+ if isinstance(keystr, OptionKey):
+ o = keystr
+ else:
+ o = OptionKey.from_string(keystr)
+ if o in self.options:
+ return self.set_value(o, new_value)
+ o = o.evolve(subproject='')
+ return self.set_value(o, new_value)
+
+ def set_subproject_options(self, subproject: str,
+ spcall_default_options: str,
+ project_default_options: str) -> None:
+ for o in itertools.chain(spcall_default_options, project_default_options):
+ keystr, valstr = o.split('=', 1)
+ assert ':' not in keystr
+ keystr = f'{subproject}:{keystr}'
+ if keystr not in self.augments:
+ self.augments[keystr] = valstr
+
+ def set_from_configure_command(self, D_args: T.List[str], U_args: T.List[str]) -> bool:
+ dirty = False
+ D_args = [] if D_args is None else D_args
+ (global_options, perproject_global_options, project_options) = self.classify_D_arguments(D_args)
+ U_args = [] if U_args is None else U_args
+ for key, valstr in global_options:
+ dirty |= self.set_option_from_string(key, valstr)
+ for key, valstr in project_options:
+ dirty |= self.set_option_from_string(key, valstr)
+ for keystr, valstr in perproject_global_options:
+ if keystr in self.augments:
+ if self.augments[keystr] != valstr:
+ self.augments[keystr] = valstr
+ dirty = True
+ else:
+ self.augments[keystr] = valstr
+ dirty = True
+ for delete in U_args:
+ if delete in self.augments:
+ del self.augments[delete]
+ dirty = True
+ 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_key(key)
- self.d[key] = new_object
+ 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_option_from_meson_file(self, key: OptionKey) -> 'T.Tuple[AnyOptionType, OptionValueType]':
+ assert isinstance(key, OptionKey)
+ (value_object, value) = self.get_value_object_and_value_for(key)
+ return (value_object, value)
def remove(self, key: OptionKey) -> None:
- del self.d[key]
+ del self.options[key]
+ try:
+ self.project_options.remove(key)
+ except KeyError:
+ pass
- def __contains__(self, key: OptionKey) -> bool:
- key = self.ensure_key(key)
- return key in self.d
+ 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.d)
+ return repr(self.options)
def keys(self) -> T.KeysView[OptionKey]:
- return self.d.keys()
+ return self.options.keys()
def values(self) -> T.ValuesView[AnyOptionType]:
- return self.d.values()
+ return self.options.values()
- def items(self) -> T.ItemsView['OptionKey', AnyOptionType]:
- return self.d.items()
+ 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.d.update(**kwargs)
+ self.options.update(**kwargs)
def setdefault(self, k: OptionKey, o: AnyOptionType) -> AnyOptionType:
- return self.d.setdefault(k, o)
+ return self.options.setdefault(k, o)
- def get(self, o: OptionKey, default: T.Optional[AnyOptionType] = None) -> T.Optional[AnyOptionType]:
- return self.d.get(o, default)
+ 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."""
@@ -857,7 +1163,11 @@ class OptionStore:
def is_backend_option(self, key: OptionKey) -> bool:
"""Convenience method to check if this is a backend option."""
- return key.name.startswith('backend_')
+ 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."""
@@ -872,3 +1182,218 @@ class OptionStore:
def is_module_option(self, key: OptionKey) -> bool:
return key in self.module_options
+
+ def classify_D_arguments(self, D: T.List[str]) -> T.Tuple[T.List[T.Tuple[OptionKey, str]],
+ T.List[T.Tuple[str, str]],
+ T.List[T.Tuple[OptionKey, str]]]:
+ global_options = []
+ project_options = []
+ perproject_global_options = []
+ for setval in D:
+ keystr, valstr = setval.split('=', 1)
+ key = OptionKey.from_string(keystr)
+ valuetuple = (key, valstr)
+ if self.is_project_option(key):
+ project_options.append(valuetuple)
+ elif key.subproject is None:
+ global_options.append(valuetuple)
+ else:
+ # FIXME, augments are currently stored as strings, not OptionKeys
+ strvaluetuple = (keystr, valstr)
+ perproject_global_options.append(strvaluetuple)
+ return (global_options, perproject_global_options, project_options)
+
+ def optlist2optdict(self, optlist: T.List[str]) -> T.Dict[str, str]:
+ optdict = {}
+ for p in optlist:
+ k, v = p.split('=', 1)
+ optdict[k] = v
+ return optdict
+
+ def prefix_split_options(self, coll: T.Union[T.List[str], OptionStringLikeDict]) -> T.Tuple[str, T.Union[T.List[str], OptionStringLikeDict]]:
+ prefix = None
+ if isinstance(coll, list):
+ others: T.List[str] = []
+ for e in coll:
+ if e.startswith('prefix='):
+ prefix = e.split('=', 1)[1]
+ else:
+ others.append(e)
+ return (prefix, others)
+ else:
+ others_d: OptionStringLikeDict = {}
+ for k, v in coll.items():
+ if isinstance(k, OptionKey) and k.name == 'prefix':
+ prefix = v
+ elif k == 'prefix':
+ prefix = v
+ else:
+ others_d[k] = v
+ return (prefix, others_d)
+
+ def first_handle_prefix(self,
+ project_default_options: T.Union[T.List[str], OptionStringLikeDict],
+ cmd_line_options: T.Union[T.List[str], OptionStringLikeDict],
+ native_file_options: T.Union[T.List[str], OptionStringLikeDict]) \
+ -> T.Tuple[T.Union[T.List[str], OptionStringLikeDict],
+ T.Union[T.List[str], OptionStringLikeDict],
+ T.Union[T.List[str], OptionStringLikeDict]]:
+ 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_prefix, nopref_native_file_options) = self.prefix_split_options(native_file_options)
+ prefix = prefix if possible_prefix is None else possible_prefix
+ (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_native_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:
+ new_value = BUILTIN_OPTIONS[optkey].default
+ valobj.set_value(new_value)
+ self.options[OptionKey('prefix')].set_value(prefix)
+
+ def initialize_from_top_level_project_call(self,
+ project_default_options_in: T.Union[T.List[str], OptionStringLikeDict],
+ cmd_line_options_in: T.Union[T.List[str], OptionStringLikeDict],
+ native_file_options_in: T.Union[T.List[str], OptionStringLikeDict]) -> None:
+ first_invocation = True
+ (project_default_options, cmd_line_options, native_file_options) = self.first_handle_prefix(project_default_options_in,
+ cmd_line_options_in,
+ native_file_options_in)
+ if isinstance(project_default_options, str):
+ project_default_options = [project_default_options]
+ if isinstance(project_default_options, list):
+ project_default_options = self.optlist2optdict(project_default_options) # type: ignore [assignment]
+ if project_default_options is None:
+ project_default_options = {}
+ assert isinstance(native_file_options, dict)
+ for keystr, valstr in native_file_options.items():
+ if isinstance(keystr, str):
+ # FIXME, standardise on Key or string.
+ key = OptionKey.from_string(keystr)
+ else:
+ key = keystr
+ if key.subproject is not None:
+ #self.pending_project_options[key] = valstr
+ augstr = str(key)
+ self.augments[augstr] = valstr
+ elif key in self.options:
+ self.set_value(key, valstr, first_invocation)
+ else:
+ proj_key = key.evolve(subproject='')
+ if proj_key in self.options:
+ self.options[proj_key].set_value(valstr)
+ else:
+ self.pending_project_options[key] = valstr
+ assert isinstance(project_default_options, dict)
+ for keystr, valstr in project_default_options.items():
+ # Ths 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 '' (this method is only called from top level)
+ #
+ # The key parsing fucntion can not handle the difference between the two
+ # and defaults to A.
+ assert isinstance(keystr, str)
+ key = OptionKey.from_string(keystr)
+ # Due to backwards compatibility we ignore all cross options when building
+ # natively.
+ if not self.is_cross and key.is_for_build():
+ continue
+ if key.subproject is not None:
+ self.pending_project_options[key] = valstr
+ elif key in self.options:
+ self.set_option(key, valstr, first_invocation)
+ else:
+ # Setting a project option with default_options.
+ # Argubly this should be a hard error, the default
+ # value of project option should be set in the option
+ # file, not in the project call.
+ proj_key = key.evolve(subproject='')
+ if self.is_project_option(proj_key):
+ self.set_option(proj_key, valstr)
+ else:
+ self.pending_project_options[key] = valstr
+ assert isinstance(cmd_line_options, dict)
+ for keystr, valstr in cmd_line_options.items():
+ if isinstance(keystr, str):
+ key = OptionKey.from_string(keystr)
+ else:
+ key = keystr
+ # Due to backwards compatibility we ignore all cross options when building
+ # natively.
+ if not self.is_cross and key.is_for_build():
+ continue
+ if key in self.options:
+ self.set_value(key, valstr, True)
+ elif key.subproject is None:
+ projectkey = key.evolve(subproject='')
+ if projectkey in self.options:
+ self.options[projectkey].set_value(valstr)
+ else:
+ # Fail on unknown options that we can know must
+ # exist at this point in time. Subproject and compiler
+ # options are resolved later.
+ #
+ # Some base options (sanitizers etc) might get added later.
+ # Permitting them all is not strictly correct.
+ assert isinstance(keystr, str)
+ if ':' not in keystr and not self.is_compiler_option(key) and not self.is_base_option(key):
+ raise MesonException(f'Unknown options: "{keystr}"')
+ self.pending_project_options[key] = valstr
+ else:
+ self.pending_project_options[key] = valstr
+
+ def hacky_mchackface_back_to_list(self, optdict: T.Dict[str, str]) -> T.List[str]:
+ if isinstance(optdict, dict):
+ return [f'{k}={v}' for k, v in optdict.items()]
+ return optdict
+
+ def initialize_from_subproject_call(self,
+ subproject: str,
+ spcall_default_options: T.Union[T.List[str], OptionStringLikeDict],
+ project_default_options: T.Union[T.List[str], OptionStringLikeDict],
+ cmd_line_options: T.Union[T.List[str], OptionStringLikeDict]) -> None:
+ is_first_invocation = True
+ spcall_default_options = self.hacky_mchackface_back_to_list(spcall_default_options) # type: ignore [arg-type]
+ project_default_options = self.hacky_mchackface_back_to_list(project_default_options) # type: ignore [arg-type]
+ if isinstance(spcall_default_options, str):
+ spcall_default_options = [spcall_default_options]
+ for o in itertools.chain(project_default_options, spcall_default_options):
+ keystr, valstr = o.split('=', 1)
+ key = OptionKey.from_string(keystr)
+ assert key.subproject is None
+ key = key.evolve(subproject=subproject)
+ # If the key points to a project option, set the value from that.
+ # Otherwise set an augment.
+ if key in self.project_options:
+ self.set_value(key, valstr, is_first_invocation)
+ else:
+ self.pending_project_options.pop(key, None)
+ aug_str = f'{subproject}:{keystr}'
+ self.augments[aug_str] = valstr
+ # Check for pending options
+ assert isinstance(cmd_line_options, dict)
+ for key, valstr in cmd_line_options.items(): # type: ignore [assignment]
+ if not isinstance(key, OptionKey):
+ key = OptionKey.from_string(key)
+ if key.subproject != subproject:
+ continue
+ self.pending_project_options.pop(key, None)
+ if key in self.options:
+ self.set_value(key, valstr, is_first_invocation)
+ else:
+ self.augments[str(key)] = valstr