diff --git a/mesonbuild/coredata.py b/mesonbuild/coredata.py index 310174f48..8b272e853 100644 --- a/mesonbuild/coredata.py +++ b/mesonbuild/coredata.py @@ -47,6 +47,175 @@ default_yielding = False # Can't bind this near the class method it seems, sadly. _T = T.TypeVar('_T') + +class ArgumentGroup(enum.Enum): + + """Enum used to specify what kind of argument a thing is.""" + + BUILTIN = 0 + BASE = 1 + COMPILER = 2 + USER = 3 + BACKEND = 4 + + +def classify_argument(key: 'OptionKey') -> ArgumentGroup: + """Classify arguments into groups so we know which dict to assign them to.""" + + from .compilers import all_languages, base_options + all_builtins = set(BUILTIN_OPTIONS) | set(BUILTIN_OPTIONS_PER_MACHINE) | set(builtin_dir_noprefix_options) + lang_prefixes = tuple('{}_'.format(l) for l in all_languages) + + if key.name in base_options: + assert key.machine is MachineChoice.HOST + return ArgumentGroup.BASE + elif key.name.startswith(lang_prefixes): + return ArgumentGroup.COMPILER + elif key.name in all_builtins: + # if for_machine is MachineChoice.BUILD: + # if option in BUILTIN_OPTIONS_PER_MACHINE: + # return ArgumentGroup.BUILTIN + # raise MesonException('Argument {} is not allowed per-machine'.format(option)) + return ArgumentGroup.BUILTIN + elif key.name.startswith('backend_'): + return ArgumentGroup.BACKEND + else: + assert key.machine is MachineChoice.HOST + return ArgumentGroup.USER + + +class OptionKey: + + """Represents an option key in the various option dictionaries. + + This provides a flexible, powerful way to map option names from their + external form (things like subproject:build.option) to something that + internally easier to reason about and produce. + """ + + __slots__ = ['name', 'subproject', 'machine', 'lang', '_hash'] + + name: str + subproject: str + machine: MachineChoice + lang: T.Optional[str] + + def __init__(self, name: str, subproject: str = '', + machine: MachineChoice = MachineChoice.HOST, + lang: T.Optional[str] = None): + object.__setattr__(self, 'name', name) + object.__setattr__(self, 'subproject', subproject) + object.__setattr__(self, 'machine', machine) + object.__setattr__(self, 'lang', lang) + object.__setattr__(self, '_hash', hash((name, subproject, machine, lang))) + + def __setattr__(self, key: str, value: T.Any) -> None: + raise AttributeError('OptionKey instances do not support mutation.') + + def __getstate__(self) -> T.Dict[str, T.Any]: + return { + 'name': self.name, + 'subproject': self.subproject, + 'machine': self.machine, + 'lang': self.lang, + } + + def __setstate__(self, state: T.Dict[str, T.Any]) -> None: + """De-serialize the state of a pickle. + + This is very clever. __init__ is not a constructor, it's an + initializer, therefore it's safe to call more than once. We create a + state in the custom __getstate__ method, which is valid to pass + unsplatted to the initializer. + """ + self.__init__(**state) + + def __hash__(self) -> int: + return self._hash + + def __eq__(self, other: object) -> bool: + if isinstance(other, OptionKey): + return ( + self.name == other.name and + self.subproject == other.subproject and + self.machine is other.machine and + self.lang == other.lang) + return NotImplemented + + def __str__(self) -> str: + out = self.name + if self.lang: + out = f'{self.lang}_{out}' + if self.machine is MachineChoice.BUILD: + out = f'build.{out}' + if self.subproject: + out = f'{self.subproject}:{out}' + return out + + def __repr__(self) -> str: + return f'OptionKey({repr(self.name)}, {repr(self.subproject)}, {repr(self.machine)}, {repr(self.lang)})' + + @classmethod + def from_string(cls, raw: str) -> 'OptionKey': + """Parse the raw command line format into a three part tuple. + + This takes strings like `mysubproject:build.myoption` and Creates an + OptionKey out of them. + """ + from .compilers import all_languages + if any(raw.startswith(f'{l}_') for l in all_languages): + lang, raw2 = raw.split('_', 1) + else: + lang, raw2 = None, raw + + try: + subproject, raw3 = raw2.split(':') + except ValueError: + subproject, raw3 = '', raw2 + + if raw3.startswith('build.'): + opt = raw3.lstrip('build.') + for_machine = MachineChoice.BUILD + else: + opt = raw3 + for_machine = MachineChoice.HOST + assert ':' not in opt + assert 'build.' not in opt + + return cls(opt, subproject, for_machine, lang) + + def evolve(self, name: T.Optional[str] = None, subproject: T.Optional[str] = None, + machine: T.Optional[MachineChoice] = None, lang: T.Optional[str] = '') -> 'OptionKey': + """Create a new copy of this key, but with alterted members. + + For example: + >>> a = OptionKey('foo', '', MachineChoice.Host) + >>> b = OptionKey('foo', 'bar', MachineChoice.Host) + >>> b == a.evolve(subproject='bar') + True + """ + # 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, + lang if lang != '' else self.lang, + ) + + def as_root(self) -> 'OptionKey': + """Convenience method for key.evolve(subproject='').""" + return self.evolve(subproject='') + + def as_build(self) -> 'OptionKey': + """Convenience method for key.evolve(machine=MachinceChoice.BUILD).""" + return self.evolve(machine=MachineChoice.BUILD) + + def as_host(self) -> 'OptionKey': + """Convenience method for key.evolve(machine=MachinceChoice.HOST).""" + return self.evolve(machine=MachineChoice.HOST) + + class MesonVersionMismatchException(MesonException): '''Build directory generated with Meson version is incompatible with current version''' def __init__(self, old_version: str, current_version: str) -> None: