aboutsummaryrefslogtreecommitdiff
path: root/mesonbuild/options.py
diff options
context:
space:
mode:
Diffstat (limited to 'mesonbuild/options.py')
-rw-r--r--mesonbuild/options.py189
1 files changed, 107 insertions, 82 deletions
diff --git a/mesonbuild/options.py b/mesonbuild/options.py
index bc4d79f..317acbd 100644
--- a/mesonbuild/options.py
+++ b/mesonbuild/options.py
@@ -805,6 +805,7 @@ class OptionStore:
def __init__(self, is_cross: bool) -> None:
self.options: T.Dict['OptionKey', 'AnyOptionType'] = {}
+ self.subprojects: T.Set[str] = set()
self.project_options: T.Set[OptionKey] = set()
self.module_options: T.Set[OptionKey] = set()
from .compilers import all_languages
@@ -812,13 +813,11 @@ class OptionStore:
self.augments: OptionDict = {}
self.is_cross = is_cross
- # Pending options are options that need to be initialized later, either
- # configuration dependent options like compiler options, or options for
- # a different subproject
+ # Pending options are configuration dependent options that could be
+ # initialized later, such as compiler options
self.pending_options: OptionDict = {}
-
- def clear_pending(self) -> None:
- self.pending_options = {}
+ # Subproject options from toplevel project()
+ self.pending_subproject_options: OptionDict = {}
def ensure_and_validate_key(self, key: T.Union[OptionKey, str]) -> OptionKey:
if isinstance(key, str):
@@ -849,7 +848,7 @@ class OptionStore:
def __len__(self) -> int:
return len(self.options)
- def get_value_object_for(self, key: 'T.Union[OptionKey, str]') -> AnyOptionType:
+ def get_key_and_value_object_for(self, key: 'T.Union[OptionKey, str]') -> T.Tuple[OptionKey, AnyOptionType]:
key = self.ensure_and_validate_key(key)
potential = self.options.get(key, None)
if self.is_project_option(key):
@@ -862,32 +861,41 @@ class OptionStore:
# Subproject is set to yield, but top level
# project does not have an option of the same
# name. Return the subproject option.
- return potential
+ return key, potential
# 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
+ return parent_key, parent_option
+ return key, potential
if potential is None:
raise KeyError(f'Tried to access nonexistant project option {key}.')
- return potential
+ return key, potential
else:
if potential is None:
parent_key = OptionKey(key.name, subproject=None, machine=key.machine)
if parent_key not in self.options:
raise KeyError(f'Tried to access nonexistant project parent option {parent_key}.')
- return self.options[parent_key]
- return potential
+ # This is a global option but it can still have per-project
+ # augment, so return the subproject key.
+ return key, self.options[parent_key]
+ return key, potential
+
+ def get_value_object_for(self, key: 'T.Union[OptionKey, str]') -> AnyOptionType:
+ return self.get_key_and_value_object_for(key)[1]
def get_value_object_and_value_for(self, key: OptionKey) -> T.Tuple[AnyOptionType, ElementaryOptionValues]:
assert isinstance(key, OptionKey)
- vobject = self.get_value_object_for(key)
+ _, vobject = self.get_key_and_value_object_for(key)
computed_value = vobject.value
- if key.subproject is not None:
- if key in self.augments:
- computed_value = vobject.validate_value(self.augments[key])
+ if key in self.augments:
+ assert key.subproject is not None
+ computed_value = self.augments[key]
return (vobject, computed_value)
+ def option_has_value(self, key: OptionKey, value: ElementaryOptionValues) -> bool:
+ vobject, current_value = self.get_value_object_and_value_for(key)
+ return vobject.validate_value(value) == current_value
+
def get_value_for(self, name: 'T.Union[OptionKey, str]', subproject: T.Optional[str] = None) -> ElementaryOptionValues:
if isinstance(name, str):
key = OptionKey(name, subproject)
@@ -998,6 +1006,7 @@ class OptionStore:
return value.as_posix()
def set_option(self, key: OptionKey, new_value: ElementaryOptionValues, first_invocation: bool = False) -> bool:
+ changed = False
error_key = key
if error_key.subproject == '':
error_key = error_key.evolve(subproject=None)
@@ -1011,7 +1020,7 @@ class OptionStore:
new_value = self.sanitize_dir_option_value(prefix, key, new_value)
try:
- opt = self.get_value_object_for(key)
+ actual_key, opt = self.get_key_and_value_object_for(key)
except KeyError:
raise MesonException(f'Unknown option: "{error_key}".')
@@ -1034,13 +1043,23 @@ class OptionStore:
elif isinstance(opt.deprecated, str):
mlog.deprecation(f'Option "{error_key}" is replaced by {opt.deprecated!r}')
# Change both this aption and the new one pointed to.
- dirty = self.set_option(key.evolve(name=opt.deprecated), new_value)
- dirty |= opt.set_value(new_value)
- return dirty
+ changed |= self.set_option(key.evolve(name=opt.deprecated), new_value, first_invocation)
- old_value = opt.value
- changed = opt.set_value(new_value)
+ new_value = opt.validate_value(new_value)
+ if key in self.options:
+ if actual_key.subproject == key.subproject:
+ old_value = opt.value
+ opt.set_value(new_value)
+ else:
+ # the key must have pointed to a yielding option;
+ # do not overwrite the global value in that case
+ return changed
+ else:
+ assert key.subproject is not None
+ old_value = self.augments.get(key, opt.value)
+ self.augments[key] = new_value
+ changed |= old_value != new_value
if opt.readonly and changed and not first_invocation:
raise MesonException(f'Tried to modify read only option "{error_key}"')
@@ -1054,12 +1073,12 @@ class OptionStore:
optimization, debug = self.DEFAULT_DEPENDENTS[new_value]
dkey = key.evolve(name='debug')
optkey = key.evolve(name='optimization')
- self.options[dkey].set_value(debug)
- self.options[optkey].set_value(optimization)
+ self.set_option(dkey, debug, first_invocation)
+ self.set_option(optkey, optimization, first_invocation)
return changed
- def set_option_maybe_root(self, o: OptionKey, new_value: ElementaryOptionValues, first_invocation: bool = False) -> bool:
+ def set_user_option(self, o: OptionKey, new_value: ElementaryOptionValues, first_invocation: bool = False) -> bool:
if not self.is_cross and o.is_for_build():
return False
@@ -1070,37 +1089,34 @@ class OptionStore:
# 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)
+ # B) a project option, in which case the subproject is ''
#
# The key parsing function can not handle the difference between the two
# and defaults to A.
if o in self.options:
return self.set_option(o, new_value, first_invocation)
+
+ # could also be an augment...
+ global_option = o.evolve(subproject=None)
+ if o.subproject is not None and global_option in self.options:
+ return self.set_option(o, new_value, first_invocation)
+
if self.accept_as_pending_option(o, first_invocation=first_invocation):
old_value = self.pending_options.get(o, None)
self.pending_options[o] = new_value
- return old_value is None or str(old_value) == new_value
- else:
+ return old_value is None or str(old_value) != new_value
+ elif o.subproject is None:
o = o.as_root()
return self.set_option(o, new_value, first_invocation)
+ else:
+ raise MesonException(f'Unknown option: "{o}".')
def set_from_configure_command(self, D_args: T.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_maybe_root(key, valstr)
- for key, valstr in project_options:
- dirty |= self.set_option_maybe_root(key, valstr)
- for key, valstr in perproject_global_options:
- if key in self.augments:
- if self.augments[key] != valstr:
- self.augments[key] = valstr
- dirty = True
- else:
- self.augments[key] = valstr
- dirty = True
+ for key, valstr in self.parse_D_arguments(D_args):
+ dirty |= self.set_user_option(key, valstr)
for keystr in U_args:
key = OptionKey.from_string(keystr)
if key in self.augments:
@@ -1226,23 +1242,13 @@ 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[OptionKey, str]],
- T.List[T.Tuple[OptionKey, str]]]:
- global_options = []
- project_options = []
- perproject_global_options = []
+ def parse_D_arguments(self, D: T.List[str]) -> T.List[T.Tuple[OptionKey, str]]:
+ 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:
- perproject_global_options.append(valuetuple)
- return (global_options, perproject_global_options, project_options)
+ options.append((key, valstr))
+ return options
def prefix_split_options(self, coll: OptionDict) -> T.Tuple[T.Optional[str], OptionDict]:
prefix = None
@@ -1305,15 +1311,15 @@ class OptionStore:
if not self.is_cross and key.is_for_build():
continue
if key.subproject:
- # do apply project() default_options for subprojects here, because
- # they have low priority
- self.pending_options[key] = valstr
+ # Subproject options from toplevel project() have low priority
+ # and will be processed when the subproject is found
+ self.pending_subproject_options[key] = valstr
else:
# Setting a project option with default_options
# should arguably be a hard error; the default
# value of project option should be set in the option
# file, not in the project call.
- self.set_option_maybe_root(key, valstr, True)
+ self.set_user_option(key, valstr, True)
# ignore subprojects for now for machine file and command line
# options; they are applied later
@@ -1323,25 +1329,18 @@ class OptionStore:
if not self.is_cross and key.is_for_build():
continue
if not key.subproject:
- self.set_option_maybe_root(key, valstr, True)
+ self.set_user_option(key, valstr, True)
for key, valstr in cmd_line_options.items():
# Due to backwards compatibility we ignore all build-machine options
# when building natively.
if not self.is_cross and key.is_for_build():
continue
if not key.subproject:
- self.set_option_maybe_root(key, valstr, True)
+ self.set_user_option(key, valstr, True)
- def accept_as_pending_option(self, key: OptionKey, known_subprojects: T.Optional[T.Container[str]] = None,
- first_invocation: bool = False) -> bool:
- # Fail on unknown options that we can know must exist at this point in time.
- # Subproject and compiler options are resolved later.
- #
+ def accept_as_pending_option(self, key: OptionKey, first_invocation: bool = False) -> bool:
# Some base options (sanitizers etc) might get added later.
# Permitting them all is not strictly correct.
- if key.subproject:
- if known_subprojects is None or key.subproject not in known_subprojects:
- return True
if self.is_compiler_option(key):
return True
if first_invocation and self.is_backend_option(key):
@@ -1365,23 +1364,40 @@ class OptionStore:
project_default_options: OptionDict,
cmd_line_options: OptionDict,
machine_file_options: OptionDict) -> None:
- # pick up pending per-project settings from the toplevel project() invocation
- options = {k: v for k, v in self.pending_options.items() if k.subproject == subproject}
- # apply project() and subproject() default_options
- for key, valstr in itertools.chain(project_default_options.items(), spcall_default_options.items()):
+ options: OptionDict = {}
+
+ # project() default_options
+ for key, valstr in project_default_options.items():
+ if key.subproject == subproject:
+ without_subp = key.evolve(subproject=None)
+ raise MesonException(f'subproject name not needed in default_options; use "{without_subp}" instead of "{key}"')
+
if key.subproject is None:
key = key.evolve(subproject=subproject)
- elif key.subproject == subproject:
+ options[key] = valstr
+
+ # augments from the toplevel project() default_options
+ for key, valstr in self.pending_subproject_options.items():
+ if key.subproject == subproject:
+ options[key] = valstr
+
+ # subproject() default_options
+ for key, valstr in spcall_default_options.items():
+ if key.subproject == subproject:
without_subp = key.evolve(subproject=None)
raise MesonException(f'subproject name not needed in default_options; use "{without_subp}" instead of "{key}"')
+
+ if key.subproject is None:
+ key = key.evolve(subproject=subproject)
options[key] = valstr
# then global settings from machine file and command line
+ # **but not if they are toplevel project options**
for key, valstr in itertools.chain(machine_file_options.items(), cmd_line_options.items()):
- if key.subproject is None:
+ if key.subproject is None and not self.is_project_option(key.as_root()):
subp_key = key.evolve(subproject=subproject)
- self.pending_options.pop(subp_key, None)
+ self.pending_subproject_options.pop(subp_key, None)
options.pop(subp_key, None)
# then finally per project augments from machine file and command line
@@ -1391,12 +1407,21 @@ class OptionStore:
# merge everything that has been computed above, while giving self.augments priority
for key, valstr in options.items():
+ if key.subproject != subproject:
+ if key.subproject in self.subprojects and not self.option_has_value(key, valstr):
+ mlog.warning('option {key} is set in subproject {subproject} but has already been processed')
+ continue
+
+ # Subproject options from project() will be processed when the subproject is found
+ self.pending_subproject_options[key] = valstr
+ continue
+
+ self.pending_subproject_options.pop(key, None)
self.pending_options.pop(key, None)
- valstr = self.augments.pop(key, valstr)
- if key in self.project_options:
- self.set_option(key, valstr, True)
- else:
- self.augments[key] = valstr
+ if key not in self.augments:
+ self.set_user_option(key, valstr, True)
+
+ self.subprojects.add(subproject)
def update_project_options(self, project_options: MutableKeyedOptionDictType, subproject: SubProject) -> None:
for key, value in project_options.items():