diff options
Diffstat (limited to 'mesonbuild/options.py')
-rw-r--r-- | mesonbuild/options.py | 189 |
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(): |