1433 lines
60 KiB
Python
1433 lines
60 KiB
Python
# SPDX-License-Identifier: Apache-2.0
|
||
# Copyright 2013-2024 Contributors to the The Meson project
|
||
# Copyright © 2019-2025 Intel Corporation
|
||
|
||
from __future__ import annotations
|
||
from collections import OrderedDict
|
||
from itertools import chain
|
||
import argparse
|
||
import copy
|
||
import dataclasses
|
||
import itertools
|
||
import os
|
||
import pathlib
|
||
|
||
import typing as T
|
||
|
||
from .mesonlib import (
|
||
HoldableObject,
|
||
default_prefix,
|
||
default_datadir,
|
||
default_includedir,
|
||
default_infodir,
|
||
default_libdir,
|
||
default_libexecdir,
|
||
default_localedir,
|
||
default_mandir,
|
||
default_sbindir,
|
||
default_sysconfdir,
|
||
MesonException,
|
||
MesonBugException,
|
||
listify_array_value,
|
||
MachineChoice,
|
||
)
|
||
from . import mlog
|
||
|
||
if T.TYPE_CHECKING:
|
||
from typing_extensions import Literal, Final, TypeAlias, TypedDict
|
||
|
||
from .interpreterbase import SubProject
|
||
|
||
DeprecatedType: TypeAlias = T.Union[bool, str, T.Dict[str, str], T.List[str]]
|
||
AnyOptionType: TypeAlias = T.Union[
|
||
'UserBooleanOption', 'UserComboOption', 'UserFeatureOption',
|
||
'UserIntegerOption', 'UserStdOption', 'UserStringArrayOption',
|
||
'UserStringOption', 'UserUmaskOption']
|
||
ElementaryOptionValues: TypeAlias = T.Union[str, int, bool, T.List[str]]
|
||
MutableKeyedOptionDictType: TypeAlias = T.Dict['OptionKey', AnyOptionType]
|
||
|
||
_OptionKeyTuple: TypeAlias = T.Tuple[T.Optional[str], MachineChoice, str]
|
||
|
||
class ArgparseKWs(TypedDict, total=False):
|
||
|
||
action: str
|
||
dest: str
|
||
default: str
|
||
choices: T.List
|
||
|
||
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']
|
||
|
||
# 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...
|
||
_BUILTIN_NAMES = {
|
||
'prefix',
|
||
'bindir',
|
||
'datadir',
|
||
'includedir',
|
||
'infodir',
|
||
'libdir',
|
||
'licensedir',
|
||
'libexecdir',
|
||
'localedir',
|
||
'localstatedir',
|
||
'mandir',
|
||
'sbindir',
|
||
'sharedstatedir',
|
||
'sysconfdir',
|
||
'auto_features',
|
||
'backend',
|
||
'buildtype',
|
||
'debug',
|
||
'default_library',
|
||
'default_both_libraries',
|
||
'errorlogs',
|
||
'genvslite',
|
||
'install_umask',
|
||
'layout',
|
||
'optimization',
|
||
'prefer_static',
|
||
'stdsplit',
|
||
'strip',
|
||
'unity',
|
||
'unity_size',
|
||
'warning_level',
|
||
'werror',
|
||
'wrap_mode',
|
||
'force_fallback_for',
|
||
'pkg_config_path',
|
||
'cmake_prefix_path',
|
||
'vsenv',
|
||
}
|
||
|
||
_BAD_VALUE = 'Qwert Zuiopü'
|
||
_optionkey_cache: T.Dict[_OptionKeyTuple, OptionKey] = {}
|
||
|
||
|
||
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', '_hash')
|
||
|
||
name: str
|
||
subproject: T.Optional[str] # None is global, empty string means top level project
|
||
machine: MachineChoice
|
||
_hash: int
|
||
|
||
def __new__(cls,
|
||
name: str = '',
|
||
subproject: T.Optional[str] = None,
|
||
machine: MachineChoice = MachineChoice.HOST) -> OptionKey:
|
||
"""The use of the __new__ method allows to add a transparent cache
|
||
to the OptionKey object creation, without breaking its API.
|
||
"""
|
||
if not name:
|
||
return super().__new__(cls) # for unpickling, do not cache now
|
||
|
||
tuple_: _OptionKeyTuple = (subproject, machine, name)
|
||
try:
|
||
return _optionkey_cache[tuple_]
|
||
except KeyError:
|
||
instance = super().__new__(cls)
|
||
instance._init(name, subproject, machine)
|
||
_optionkey_cache[tuple_] = instance
|
||
return instance
|
||
|
||
def _init(self, name: str, subproject: T.Optional[str], machine: MachineChoice) -> None:
|
||
# We don't use the __init__ method, because it would be called after __new__
|
||
# while we need __new__ to initialise the object before populating the cache.
|
||
|
||
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}')
|
||
assert ':' not in name
|
||
|
||
object.__setattr__(self, 'name', name)
|
||
object.__setattr__(self, 'subproject', subproject)
|
||
object.__setattr__(self, 'machine', machine)
|
||
object.__setattr__(self, '_hash', hash((name, subproject, machine)))
|
||
|
||
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,
|
||
}
|
||
|
||
def __setstate__(self, state: T.Dict[str, T.Any]) -> None:
|
||
# Here, the object is created using __new__()
|
||
self._init(**state)
|
||
_optionkey_cache[self._to_tuple()] = self
|
||
|
||
def __hash__(self) -> int:
|
||
return self._hash
|
||
|
||
def _to_tuple(self) -> _OptionKeyTuple:
|
||
return (self.subproject, self.machine, self.name)
|
||
|
||
def __eq__(self, other: object) -> bool:
|
||
if isinstance(other, OptionKey):
|
||
return self._to_tuple() == other._to_tuple()
|
||
return NotImplemented
|
||
|
||
def __ne__(self, other: object) -> bool:
|
||
if isinstance(other, OptionKey):
|
||
return self._to_tuple() != other._to_tuple()
|
||
return NotImplemented
|
||
|
||
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
|
||
|
||
def __le__(self, other: object) -> bool:
|
||
if isinstance(other, OptionKey):
|
||
if self.subproject is None and other.subproject is not None:
|
||
return True
|
||
elif self.subproject is not None and other.subproject is None:
|
||
return False
|
||
return self._to_tuple() <= other._to_tuple()
|
||
return NotImplemented
|
||
|
||
def __gt__(self, other: object) -> bool:
|
||
if isinstance(other, OptionKey):
|
||
if other.subproject is None:
|
||
return self.subproject is not None
|
||
elif self.subproject is None:
|
||
return False
|
||
return self._to_tuple() > other._to_tuple()
|
||
return NotImplemented
|
||
|
||
def __ge__(self, other: object) -> bool:
|
||
if isinstance(other, OptionKey):
|
||
if self.subproject is None and other.subproject is not None:
|
||
return False
|
||
elif self.subproject is not None and other.subproject is None:
|
||
return True
|
||
return self._to_tuple() >= other._to_tuple()
|
||
return NotImplemented
|
||
|
||
def __str__(self) -> str:
|
||
out = self.name
|
||
if self.machine is MachineChoice.BUILD:
|
||
out = f'build.{out}'
|
||
if self.subproject is not None:
|
||
out = f'{self.subproject}:{out}'
|
||
return out
|
||
|
||
def __repr__(self) -> str:
|
||
return f'OptionKey({self.name!r}, {self.subproject!r}, {self.machine!r})'
|
||
|
||
@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.
|
||
"""
|
||
assert isinstance(raw, str)
|
||
try:
|
||
subproject, raw2 = raw.split(':')
|
||
except ValueError:
|
||
subproject, raw2 = None, raw
|
||
|
||
for_machine = MachineChoice.HOST
|
||
try:
|
||
prefix, raw3 = raw2.split('.')
|
||
if prefix == 'build':
|
||
for_machine = MachineChoice.BUILD
|
||
else:
|
||
raw3 = raw2
|
||
except ValueError:
|
||
raw3 = raw2
|
||
|
||
opt = raw3
|
||
assert ':' not in opt
|
||
assert opt.count('.') < 2
|
||
|
||
return cls(opt, subproject, for_machine)
|
||
|
||
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.
|
||
|
||
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 != _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='')."""
|
||
return self.evolve(subproject='')
|
||
|
||
def as_build(self) -> OptionKey:
|
||
"""Convenience method for key.evolve(machine=MachineChoice.BUILD)."""
|
||
return self.evolve(machine=MachineChoice.BUILD)
|
||
|
||
def as_host(self) -> OptionKey:
|
||
"""Convenience method for key.evolve(machine=MachineChoice.HOST)."""
|
||
return self.evolve(machine=MachineChoice.HOST)
|
||
|
||
def has_module_prefix(self) -> bool:
|
||
return '.' in self.name
|
||
|
||
def get_module_prefix(self) -> T.Optional[str]:
|
||
if self.has_module_prefix():
|
||
return self.name.split('.', 1)[0]
|
||
return None
|
||
|
||
def is_for_build(self) -> bool:
|
||
return self.machine is MachineChoice.BUILD
|
||
|
||
if T.TYPE_CHECKING:
|
||
OptionDict: TypeAlias = T.Dict[T.Union[OptionKey, str], ElementaryOptionValues]
|
||
|
||
@dataclasses.dataclass
|
||
class UserOption(T.Generic[_T], HoldableObject):
|
||
|
||
name: str
|
||
description: str
|
||
value_: dataclasses.InitVar[_T]
|
||
yielding: bool = DEFAULT_YIELDING
|
||
deprecated: DeprecatedType = False
|
||
readonly: bool = dataclasses.field(default=False)
|
||
|
||
def __post_init__(self, value_: _T) -> None:
|
||
self.value = self.validate_value(value_)
|
||
# Final isn't technically allowed in a __post_init__ method
|
||
self.default: Final[_T] = self.value # type: ignore[misc]
|
||
|
||
def listify(self, value: ElementaryOptionValues) -> T.List[str]:
|
||
if isinstance(value, list):
|
||
return value
|
||
if isinstance(value, bool):
|
||
return ['true'] if value else ['false']
|
||
if isinstance(value, int):
|
||
return [str(value)]
|
||
return [value]
|
||
|
||
def printable_value(self) -> ElementaryOptionValues:
|
||
assert isinstance(self.value, (str, int, bool, list))
|
||
return self.value
|
||
|
||
def printable_choices(self) -> T.Optional[T.List[str]]:
|
||
return None
|
||
|
||
# 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: object) -> _T:
|
||
raise RuntimeError('Derived option class did not override validate_value.')
|
||
|
||
def set_value(self, newvalue: object) -> bool:
|
||
oldvalue = self.value
|
||
self.value = self.validate_value(newvalue)
|
||
return self.value != oldvalue
|
||
|
||
@dataclasses.dataclass
|
||
class EnumeratedUserOption(UserOption[_T]):
|
||
|
||
"""A generic UserOption that has enumerated values."""
|
||
|
||
choices: T.List[_T] = dataclasses.field(default_factory=list)
|
||
|
||
def printable_choices(self) -> T.Optional[T.List[str]]:
|
||
return [str(c) for c in self.choices]
|
||
|
||
|
||
class UserStringOption(UserOption[str]):
|
||
|
||
def validate_value(self, value: object) -> str:
|
||
if not isinstance(value, str):
|
||
raise MesonException(f'The value of option "{self.name}" is "{value}", which is not a string.')
|
||
return value
|
||
|
||
@dataclasses.dataclass
|
||
class UserBooleanOption(EnumeratedUserOption[bool]):
|
||
|
||
choices: T.List[bool] = dataclasses.field(default_factory=lambda: [True, False])
|
||
|
||
def __bool__(self) -> bool:
|
||
return self.value
|
||
|
||
def validate_value(self, value: object) -> 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 _UserIntegerBase(UserOption[_T]):
|
||
|
||
min_value: T.Optional[int]
|
||
max_value: T.Optional[int]
|
||
|
||
if T.TYPE_CHECKING:
|
||
def toint(self, v: str) -> int: ...
|
||
|
||
def __post_init__(self, value_: _T) -> None:
|
||
super().__post_init__(value_)
|
||
choices: T.List[str] = []
|
||
if self.min_value is not None:
|
||
choices.append(f'>= {self.min_value!s}')
|
||
if self.max_value is not None:
|
||
choices.append(f'<= {self.max_value!s}')
|
||
self.__choices: str = ', '.join(choices)
|
||
|
||
def printable_choices(self) -> T.Optional[T.List[str]]:
|
||
return [self.__choices]
|
||
|
||
def validate_value(self, value: object) -> _T:
|
||
if isinstance(value, str):
|
||
value = T.cast('_T', 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 T.cast('_T', value)
|
||
|
||
|
||
@dataclasses.dataclass
|
||
class UserIntegerOption(_UserIntegerBase[int]):
|
||
|
||
min_value: T.Optional[int] = None
|
||
max_value: T.Optional[int] = None
|
||
|
||
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))
|
||
|
||
|
||
@dataclasses.dataclass
|
||
class UserUmaskOption(_UserIntegerBase[T.Union["Literal['preserve']", OctalInt]]):
|
||
|
||
min_value: T.Optional[int] = dataclasses.field(default=0, init=False)
|
||
max_value: T.Optional[int] = dataclasses.field(default=0o777, init=False)
|
||
|
||
def printable_value(self) -> str:
|
||
if isinstance(self.value, int):
|
||
return format(self.value, '04o')
|
||
return self.value
|
||
|
||
def validate_value(self, value: object) -> T.Union[Literal['preserve'], OctalInt]:
|
||
if value == 'preserve':
|
||
return 'preserve'
|
||
return OctalInt(super().validate_value(value))
|
||
|
||
def toint(self, valuestring: str) -> int:
|
||
try:
|
||
return int(valuestring, 8)
|
||
except ValueError as e:
|
||
raise MesonException(f'Invalid mode for option "{self.name}" {e}')
|
||
|
||
|
||
@dataclasses.dataclass
|
||
class UserComboOption(EnumeratedUserOption[str]):
|
||
|
||
def validate_value(self, value: object) -> 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))
|
||
|
||
assert isinstance(value, str), 'for mypy'
|
||
return value
|
||
|
||
@dataclasses.dataclass
|
||
class UserArrayOption(UserOption[T.List[_T]]):
|
||
|
||
value_: dataclasses.InitVar[T.Union[_T, T.List[_T]]]
|
||
choices: T.Optional[T.List[_T]] = None
|
||
split_args: bool = False
|
||
allow_dups: bool = False
|
||
|
||
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)
|
||
|
||
def printable_choices(self) -> T.Optional[T.List[str]]:
|
||
if self.choices is None:
|
||
return None
|
||
return [str(c) for c in self.choices]
|
||
|
||
|
||
@dataclasses.dataclass
|
||
class UserStringArrayOption(UserArrayOption[str]):
|
||
|
||
def listify(self, value: object) -> T.List[str]:
|
||
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: object) -> 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 meson 2.0.'
|
||
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
|
||
|
||
|
||
@dataclasses.dataclass
|
||
class UserFeatureOption(UserComboOption):
|
||
|
||
choices: T.List[str] = dataclasses.field(
|
||
# Ensure we get a copy with the lambda
|
||
default_factory=lambda: ['enabled', 'disabled', 'auto'], init=False)
|
||
|
||
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'
|
||
|
||
|
||
_U = T.TypeVar('_U', bound=UserOption)
|
||
|
||
|
||
def choices_are_different(a: _U, b: _U) -> bool:
|
||
"""Are the choices between two options the same?
|
||
|
||
:param a: A UserOption[T]
|
||
:param b: A second UserOption[T]
|
||
:return: True if the choices have changed, otherwise False
|
||
"""
|
||
if isinstance(a, EnumeratedUserOption):
|
||
# We expect `a` and `b` to be of the same type, but can't really annotate it that way.
|
||
assert isinstance(b, EnumeratedUserOption), 'for mypy'
|
||
return a.choices != b.choices
|
||
elif isinstance(a, UserArrayOption):
|
||
# We expect `a` and `b` to be of the same type, but can't really annotate it that way.
|
||
assert isinstance(b, UserArrayOption), 'for mypy'
|
||
return a.choices != b.choices
|
||
elif isinstance(a, _UserIntegerBase):
|
||
assert isinstance(b, _UserIntegerBase), 'for mypy'
|
||
return a.max_value != b.max_value or a.min_value != b.min_value
|
||
|
||
return False
|
||
|
||
|
||
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', choices=['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: object) -> str:
|
||
try:
|
||
candidates = listify_array_value(value)
|
||
except MesonException as e:
|
||
raise MesonException(f'error in option "{self.name}": {e!s}')
|
||
for std in candidates:
|
||
if not isinstance(std, str):
|
||
raise MesonException(f'String array element "{candidates!s}" for option "{self.name}" is not a string.')
|
||
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 meson 2.0.\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.', once=True)
|
||
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}')
|
||
|
||
|
||
def argparse_name_to_arg(name: str) -> str:
|
||
if name == 'warning_level':
|
||
return '--warnlevel'
|
||
return '--' + name.replace('_', '-')
|
||
|
||
|
||
def argparse_prefixed_default(opt: AnyOptionType, name: OptionKey, prefix: str = '') -> ElementaryOptionValues:
|
||
if isinstance(opt, (UserComboOption, UserIntegerOption, UserUmaskOption)):
|
||
return T.cast('ElementaryOptionValues', opt.default)
|
||
try:
|
||
return BUILTIN_DIR_NOPREFIX_OPTIONS[name][prefix]
|
||
except KeyError:
|
||
return T.cast('ElementaryOptionValues', opt.default)
|
||
|
||
|
||
def option_to_argparse(option: AnyOptionType, name: OptionKey, parser: argparse.ArgumentParser, help_suffix: str) -> None:
|
||
kwargs: ArgparseKWs = {}
|
||
|
||
if isinstance(option, (EnumeratedUserOption, UserArrayOption)):
|
||
c = option.choices
|
||
else:
|
||
c = None
|
||
b = 'store_true' if isinstance(option.default, bool) else None
|
||
h = option.description
|
||
if not b:
|
||
h = '{} (default: {}).'.format(h.rstrip('.'), argparse_prefixed_default(option, name))
|
||
else:
|
||
kwargs['action'] = b
|
||
if c and not b:
|
||
kwargs['choices'] = c
|
||
kwargs['default'] = argparse.SUPPRESS
|
||
kwargs['dest'] = str(name)
|
||
|
||
cmdline_name = 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/
|
||
BUILTIN_DIR_OPTIONS: T.Mapping[OptionKey, AnyOptionType] = {
|
||
OptionKey(o.name): o for o in [
|
||
UserStringOption('prefix', 'Installation prefix', default_prefix()),
|
||
UserStringOption('bindir', 'Executable directory', 'bin'),
|
||
UserStringOption('datadir', 'Data file directory', default_datadir()),
|
||
UserStringOption('includedir', 'Header file directory', default_includedir()),
|
||
UserStringOption('infodir', 'Info page directory', default_infodir()),
|
||
UserStringOption('libdir', 'Library directory', default_libdir()),
|
||
UserStringOption('licensedir', 'Licenses directory', ''),
|
||
UserStringOption('libexecdir', 'Library executable directory', default_libexecdir()),
|
||
UserStringOption('localedir', 'Locale data directory', default_localedir()),
|
||
UserStringOption('localstatedir', 'Localstate data directory', 'var'),
|
||
UserStringOption('mandir', 'Manual page directory', default_mandir()),
|
||
UserStringOption('sbindir', 'System executable directory', default_sbindir()),
|
||
UserStringOption('sharedstatedir', 'Architecture-independent data directory', 'com'),
|
||
UserStringOption('sysconfdir', 'Sysconf data directory', default_sysconfdir()),
|
||
]
|
||
}
|
||
|
||
BUILTIN_CORE_OPTIONS: T.Mapping[OptionKey, AnyOptionType] = {
|
||
OptionKey(o.name): o for o in T.cast('T.List[AnyOptionType]', [
|
||
UserFeatureOption('auto_features', "Override value of all 'auto' features", 'auto'),
|
||
UserComboOption('backend', 'Backend to use', 'ninja', choices=backendlist, readonly=True),
|
||
UserComboOption(
|
||
'genvslite',
|
||
'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
|
||
),
|
||
UserComboOption('buildtype', 'Build type to use', 'debug', choices=buildtypelist),
|
||
UserBooleanOption('debug', 'Enable debug symbols and other information', True),
|
||
UserComboOption('default_library', 'Default library type', 'shared', choices=['shared', 'static', 'both'],
|
||
yielding=False),
|
||
UserComboOption('default_both_libraries', 'Default library type for both_libraries', 'shared',
|
||
choices=['shared', 'static', 'auto']),
|
||
UserBooleanOption('errorlogs', "Whether to print the logs from failing tests", True),
|
||
UserUmaskOption('install_umask', 'Default umask to apply on permissions of installed files', OctalInt(0o022)),
|
||
UserComboOption('layout', 'Build directory layout', 'mirror', choices=['mirror', 'flat']),
|
||
UserComboOption('optimization', 'Optimization level', '0', choices=['plain', '0', 'g', '1', '2', '3', 's']),
|
||
UserBooleanOption('prefer_static', 'Whether to try static linking before shared linking', False),
|
||
UserBooleanOption('stdsplit', 'Split stdout and stderr in test logs', True),
|
||
UserBooleanOption('strip', 'Strip targets on install', False),
|
||
UserComboOption('unity', 'Unity build', 'off', choices=['on', 'off', 'subprojects']),
|
||
UserIntegerOption('unity_size', 'Unity block size', 4, min_value=2),
|
||
UserComboOption('warning_level', 'Compiler warning level to use', '1', choices=['0', '1', '2', '3', 'everything'],
|
||
yielding=False),
|
||
UserBooleanOption('werror', 'Treat warnings as errors', False, yielding=False),
|
||
UserComboOption('wrap_mode', 'Wrap mode', 'default', choices=['default', 'nofallback', 'nodownload', 'forcefallback', 'nopromote']),
|
||
UserStringArrayOption('force_fallback_for', 'Force fallback for those subprojects', []),
|
||
UserBooleanOption('vsenv', 'Activate Visual Studio environment', False, readonly=True),
|
||
|
||
# Pkgconfig module
|
||
UserBooleanOption('pkgconfig.relocatable', 'Generate pkgconfig files as relocatable', False),
|
||
|
||
# Python module
|
||
UserIntegerOption('python.bytecompile', 'Whether to compile bytecode', 0, min_value=-1, max_value=2),
|
||
UserComboOption('python.install_env', 'Which python environment to install to', 'prefix',
|
||
choices=['auto', 'prefix', 'system', 'venv']),
|
||
UserStringOption('python.platlibdir', 'Directory for site-specific, platform-specific files.', ''),
|
||
UserStringOption('python.purelibdir', 'Directory for site-specific, non-platform-specific files.', ''),
|
||
UserBooleanOption('python.allow_limited_api', '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.Mapping[OptionKey, AnyOptionType] = {
|
||
OptionKey(o.name): o for o in [
|
||
UserStringArrayOption('pkg_config_path', 'List of additional paths for pkg-config to search', []),
|
||
UserStringArrayOption('cmake_prefix_path', '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('python.platlibdir'): {},
|
||
OptionKey('python.purelibdir'): {},
|
||
}
|
||
|
||
MSCRT_VALS = ['none', 'md', 'mdd', 'mt', 'mtd']
|
||
|
||
COMPILER_BASE_OPTIONS: T.Mapping[OptionKey, AnyOptionType] = {
|
||
OptionKey(o.name): o for o in T.cast('T.List[AnyOptionType]', [
|
||
UserBooleanOption('b_pch', 'Use precompiled headers', True),
|
||
UserBooleanOption('b_lto', 'Use link time optimization', False),
|
||
UserIntegerOption('b_lto_threads', 'Use multiple threads for Link Time Optimization', 0),
|
||
UserComboOption('b_lto_mode', 'Select between different LTO modes.', 'default', choices=['default', 'thin']),
|
||
UserBooleanOption('b_thinlto_cache', 'Use LLVM ThinLTO caching for faster incremental builds', False),
|
||
UserStringOption('b_thinlto_cache_dir', 'Directory to store ThinLTO cache objects', ''),
|
||
UserStringArrayOption('b_sanitize', 'Code sanitizer to use', []),
|
||
UserBooleanOption('b_lundef', 'Use -Wl,--no-undefined when linking', True),
|
||
UserBooleanOption('b_asneeded', 'Use -Wl,--as-needed when linking', True),
|
||
UserComboOption(
|
||
'b_pgo', 'Use profile guided optimization', 'off', choices=['off', 'generate', 'use']),
|
||
UserBooleanOption('b_coverage', 'Enable coverage tracking.', False),
|
||
UserComboOption(
|
||
'b_colorout', 'Use colored output', 'always', choices=['auto', 'always', 'never']),
|
||
UserComboOption(
|
||
'b_ndebug', 'Disable asserts', 'false', choices=['true', 'false', 'if-release']),
|
||
UserBooleanOption('b_staticpic', 'Build static libraries as position independent', True),
|
||
UserBooleanOption('b_pie', 'Build executables as position independent', False),
|
||
UserBooleanOption('b_bitcode', 'Generate and embed bitcode (only macOS/iOS/tvOS)', False),
|
||
UserComboOption(
|
||
'b_vscrt', 'VS run-time library type to use.', 'from_buildtype',
|
||
choices=MSCRT_VALS + ['from_buildtype', 'static_from_buildtype']),
|
||
])
|
||
}
|
||
|
||
class OptionStore:
|
||
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.project_options = set()
|
||
self.augments: T.Dict[OptionKey, ElementaryOptionValues] = {}
|
||
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
|
||
self.pending_options: T.Dict[OptionKey, ElementaryOptionValues] = {}
|
||
|
||
def clear_pending(self) -> None:
|
||
self.pending_options = {}
|
||
|
||
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.as_host()
|
||
return key
|
||
|
||
def get_value(self, key: T.Union[OptionKey, str]) -> ElementaryOptionValues:
|
||
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.as_root()
|
||
try:
|
||
parent_option = self.options[parent_key]
|
||
except KeyError:
|
||
# Subproject is set to yield, but top level
|
||
# project does not have an option of the same
|
||
# name. Return the subproject option.
|
||
return 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
|
||
if potential is None:
|
||
raise KeyError(f'Tried to access nonexistant project option {key}.')
|
||
return 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
|
||
|
||
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)
|
||
computed_value = vobject.value
|
||
if key.subproject is not None:
|
||
if key in self.augments:
|
||
computed_value = vobject.validate_value(self.augments[key])
|
||
return (vobject, computed_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)
|
||
else:
|
||
assert subproject is None
|
||
key = name
|
||
vobject, resolved_value = self.get_value_object_and_value_for(key)
|
||
return resolved_value
|
||
|
||
def add_system_option(self, key: T.Union[OptionKey, str], valobj: AnyOptionType) -> None:
|
||
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: OptionKey, valobj: AnyOptionType) -> None:
|
||
assert isinstance(valobj, UserOption)
|
||
if not isinstance(valobj.name, str):
|
||
assert isinstance(valobj.name, str)
|
||
if key in self.options:
|
||
return
|
||
|
||
self.options[key] = valobj
|
||
pval = self.pending_options.pop(key, None)
|
||
if key.subproject:
|
||
proj_key = key.evolve(subproject=None)
|
||
self.add_system_option_internal(proj_key, valobj)
|
||
if pval is None:
|
||
pval = self.options[proj_key].value
|
||
|
||
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_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_and_validate_key(key)
|
||
assert key.subproject is not None
|
||
if key in self.options:
|
||
raise MesonException(f'Internal error: tried to add a project option {key} that already exists.')
|
||
|
||
self.options[key] = valobj
|
||
self.project_options.add(key)
|
||
pval = self.pending_options.pop(key, None)
|
||
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_and_validate_key(key)
|
||
if key.name.startswith('build.'):
|
||
raise MesonException('FATAL internal error: somebody goofed option handling.')
|
||
if not key.name.startswith(modulename + '.'):
|
||
raise MesonException('Internal error: module option name {key.name} does not start with module prefix {modulename}.')
|
||
self.add_system_option_internal(key, valobj)
|
||
self.module_options.add(key)
|
||
|
||
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_option(self, key: OptionKey, new_value: ElementaryOptionValues, first_invocation: bool = False) -> bool:
|
||
if key.name == 'prefix':
|
||
assert isinstance(new_value, str), 'for mypy'
|
||
new_value = self.sanitize_prefix(new_value)
|
||
elif self.is_builtin_option(key):
|
||
prefix = self.get_value_for('prefix')
|
||
assert isinstance(prefix, str), 'for mypy'
|
||
new_value = self.sanitize_dir_option_value(prefix, key, new_value)
|
||
|
||
try:
|
||
opt = self.get_value_object_for(key)
|
||
except KeyError:
|
||
raise MesonException(f'Unknown options: "{key!s}" not found.')
|
||
|
||
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: str) -> str:
|
||
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 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), 'for mypy'
|
||
assert isinstance(new_value, str), 'for mypy'
|
||
self.reset_prefixed_options(old_value, new_value)
|
||
|
||
if changed and key.name == 'buildtype':
|
||
assert isinstance(new_value, str), 'for mypy'
|
||
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)
|
||
|
||
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_option(o, new_value)
|
||
o = o.as_root()
|
||
return self.set_option(o, new_value)
|
||
|
||
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 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 keystr in U_args:
|
||
key = OptionKey.from_string(keystr)
|
||
if key in self.augments:
|
||
del self.augments[key]
|
||
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_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_default_for_b_option(self, key: OptionKey) -> ElementaryOptionValues:
|
||
assert self.is_base_option(key)
|
||
try:
|
||
return T.cast('ElementaryOptionValues', COMPILER_BASE_OPTIONS[key.evolve(subproject=None)].default)
|
||
except KeyError:
|
||
raise MesonBugException(f'Requested base option {key} which does not exist.')
|
||
|
||
def remove(self, key: OptionKey) -> None:
|
||
del self.options[key]
|
||
try:
|
||
self.project_options.remove(key)
|
||
except KeyError:
|
||
pass
|
||
|
||
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.options)
|
||
|
||
def keys(self) -> T.KeysView[OptionKey]:
|
||
return self.options.keys()
|
||
|
||
def values(self) -> T.ValuesView[AnyOptionType]:
|
||
return self.options.values()
|
||
|
||
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.options.update(**kwargs)
|
||
|
||
def setdefault(self, k: OptionKey, o: AnyOptionType) -> AnyOptionType:
|
||
return self.options.setdefault(k, o)
|
||
|
||
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."""
|
||
return key in self.project_options
|
||
|
||
def is_per_machine_option(self, optname: OptionKey) -> bool:
|
||
if optname.evolve(subproject=None, machine=MachineChoice.HOST) in BUILTIN_OPTIONS_PER_MACHINE:
|
||
return True
|
||
return self.is_compiler_option(optname)
|
||
|
||
def is_reserved_name(self, key: OptionKey) -> bool:
|
||
if key.name in _BUILTIN_NAMES:
|
||
return True
|
||
if '_' not in key.name:
|
||
return False
|
||
prefix = key.name.split('_')[0]
|
||
# Pylint seems to think that it is faster to build a set object
|
||
# and all related work just to test whether a string has one of two
|
||
# values. It is not, thank you very much.
|
||
if prefix in ('b', 'backend'): # pylint: disable=R6201
|
||
return True
|
||
if prefix in self.all_languages:
|
||
return True
|
||
return False
|
||
|
||
def is_builtin_option(self, key: OptionKey) -> bool:
|
||
"""Convenience method to check if this is a builtin option."""
|
||
return key.name in _BUILTIN_NAMES or self.is_module_option(key)
|
||
|
||
def is_base_option(self, key: OptionKey) -> bool:
|
||
"""Convenience method to check if this is a base option."""
|
||
return key.name.startswith('b_')
|
||
|
||
def is_backend_option(self, key: OptionKey) -> bool:
|
||
"""Convenience method to check if this is a backend option."""
|
||
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."""
|
||
|
||
# FIXME, duplicate of is_reserved_name above. Should maybe store a cache instead.
|
||
if '_' not in key.name:
|
||
return False
|
||
prefix = key.name.split('_')[0]
|
||
if prefix in self.all_languages:
|
||
return True
|
||
return False
|
||
|
||
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 = []
|
||
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)
|
||
|
||
def prefix_split_options(self, coll: OptionDict) -> T.Tuple[T.Optional[str], OptionDict]:
|
||
prefix = None
|
||
others_d: OptionDict = {}
|
||
for k, v in coll.items():
|
||
if k == 'prefix' or (isinstance(k, OptionKey) and k.name == 'prefix'):
|
||
if not isinstance(v, str):
|
||
raise MesonException('Incorrect type for prefix option (expected string)')
|
||
prefix = v
|
||
else:
|
||
others_d[k] = v
|
||
return (prefix, others_d)
|
||
|
||
def first_handle_prefix(self,
|
||
project_default_options: OptionDict,
|
||
cmd_line_options: OptionDict,
|
||
machine_file_options: T.Mapping[OptionKey, ElementaryOptionValues]) \
|
||
-> T.Tuple[OptionDict,
|
||
OptionDict,
|
||
T.MutableMapping[OptionKey, ElementaryOptionValues]]:
|
||
# Copy to avoid later mutation
|
||
nopref_machine_file_options = T.cast(
|
||
'T.MutableMapping[OptionKey, ElementaryOptionValues]', copy.copy(machine_file_options))
|
||
|
||
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_prefixv = nopref_machine_file_options.pop(OptionKey('prefix'), None)
|
||
assert possible_prefixv is None or isinstance(possible_prefixv, str), 'mypy: prefix from machine file was not a string?'
|
||
prefix = prefix if possible_prefixv is None else possible_prefixv
|
||
|
||
(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_machine_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:
|
||
_v = BUILTIN_OPTIONS[optkey].default
|
||
assert isinstance(_v, str), 'for mypy'
|
||
new_value = _v
|
||
valobj.set_value(new_value)
|
||
self.options[OptionKey('prefix')].set_value(prefix)
|
||
|
||
def initialize_from_top_level_project_call(self,
|
||
project_default_options_in: OptionDict,
|
||
cmd_line_options_in: OptionDict,
|
||
machine_file_options_in: T.Mapping[OptionKey, ElementaryOptionValues]) -> None:
|
||
first_invocation = True
|
||
(project_default_options, cmd_line_options, machine_file_options) = self.first_handle_prefix(project_default_options_in,
|
||
cmd_line_options_in,
|
||
machine_file_options_in)
|
||
for key, valstr in machine_file_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 key.subproject:
|
||
self.augments[key] = valstr
|
||
elif key in self.options:
|
||
self.set_option(key, valstr, first_invocation)
|
||
else:
|
||
proj_key = key.as_root()
|
||
if proj_key in self.options:
|
||
self.set_option(proj_key, valstr, first_invocation)
|
||
else:
|
||
self.pending_options[key] = valstr
|
||
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 function can not handle the difference between the two
|
||
# and defaults to A.
|
||
if isinstance(keystr, str):
|
||
key = OptionKey.from_string(keystr)
|
||
else:
|
||
key = keystr
|
||
# Due to backwards compatibility we ignore build-machine options
|
||
# when building natively.
|
||
if not self.is_cross and key.is_for_build():
|
||
continue
|
||
if key.subproject:
|
||
self.augments[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.as_root()
|
||
if self.is_project_option(proj_key):
|
||
self.set_option(proj_key, valstr)
|
||
else:
|
||
self.pending_options[key] = valstr
|
||
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 build-machine options
|
||
# when building natively.
|
||
if not self.is_cross and key.is_for_build():
|
||
continue
|
||
if key.subproject:
|
||
self.augments[key] = valstr
|
||
elif key in self.options:
|
||
self.set_option(key, valstr, True)
|
||
else:
|
||
proj_key = key.as_root()
|
||
if proj_key in self.options:
|
||
self.set_option(proj_key, valstr, True)
|
||
else:
|
||
self.pending_options[key] = valstr
|
||
|
||
def validate_cmd_line_options(self, cmd_line_options: OptionDict) -> None:
|
||
unknown_options = []
|
||
for keystr, valstr in cmd_line_options.items():
|
||
if isinstance(keystr, str):
|
||
key = OptionKey.from_string(keystr)
|
||
else:
|
||
key = keystr
|
||
# 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.
|
||
if key.subproject is None and not self.is_compiler_option(key) and not self.is_base_option(key) and \
|
||
key in self.pending_options:
|
||
unknown_options.append(f'"{key}"')
|
||
|
||
if unknown_options:
|
||
keys = ', '.join(unknown_options)
|
||
raise MesonException(f'Unknown options: {keys}')
|
||
|
||
def initialize_from_subproject_call(self,
|
||
subproject: str,
|
||
spcall_default_options: OptionDict,
|
||
project_default_options: OptionDict,
|
||
cmd_line_options: OptionDict) -> None:
|
||
is_first_invocation = True
|
||
for keystr, valstr in itertools.chain(project_default_options.items(), spcall_default_options.items()):
|
||
if isinstance(keystr, str):
|
||
key = OptionKey.from_string(keystr)
|
||
else:
|
||
key = keystr
|
||
if key.subproject is None:
|
||
key = key.evolve(subproject=subproject)
|
||
elif 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 the key points to a project option, set the value from that.
|
||
# Otherwise set an augment.
|
||
if key in self.project_options:
|
||
self.set_option(key, valstr, is_first_invocation)
|
||
else:
|
||
self.pending_options.pop(key, None)
|
||
self.augments[key] = valstr
|
||
# Check for pending options
|
||
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_options.pop(key, None)
|
||
if key in self.options:
|
||
self.set_option(key, valstr, is_first_invocation)
|
||
else:
|
||
self.augments[key] = valstr
|
||
|
||
def update_project_options(self, project_options: MutableKeyedOptionDictType, subproject: SubProject) -> None:
|
||
for key, value in project_options.items():
|
||
if key not in self.options:
|
||
self.add_project_option(key, value)
|
||
continue
|
||
if key.subproject != subproject:
|
||
raise MesonBugException(f'Tried to set an option for subproject {key.subproject} from {subproject}!')
|
||
|
||
oldval = self.get_value_object(key)
|
||
if type(oldval) is not type(value):
|
||
self.set_option(key, value.value)
|
||
elif choices_are_different(oldval, value):
|
||
# If the choices have changed, use the new value, but attempt
|
||
# to keep the old options. If they are not valid keep the new
|
||
# defaults but warn.
|
||
self.set_value_object(key, value)
|
||
try:
|
||
value.set_value(oldval.value)
|
||
except MesonException:
|
||
mlog.warning(f'Old value(s) of {key} are no longer valid, resetting to default ({value.value}).',
|
||
fatal=False)
|
||
|
||
# Find any extranious keys for this project and remove them
|
||
potential_removed_keys = self.options.keys() - project_options.keys()
|
||
for key in potential_removed_keys:
|
||
if self.is_project_option(key) and key.subproject == subproject:
|
||
self.remove(key)
|