From 48c17b7ae651b5819938392502d341377cb4435a Mon Sep 17 00:00:00 2001 From: Xavier Claessens Date: Wed, 3 May 2023 09:39:26 -0400 Subject: [PATCH 1/3] UserArrayOption: Remove user_input argument The only place it can be set to False is from optinterpreter. Better check value there and deprecate string usage. --- mesonbuild/compilers/compilers.py | 4 ++-- mesonbuild/coredata.py | 17 +++++------------ mesonbuild/mcompile.py | 2 +- mesonbuild/optinterpreter.py | 7 ++++++- 4 files changed, 14 insertions(+), 16 deletions(-) diff --git a/mesonbuild/compilers/compilers.py b/mesonbuild/compilers/compilers.py index 7a8ae7247..c7af1cac6 100644 --- a/mesonbuild/compilers/compilers.py +++ b/mesonbuild/compilers/compilers.py @@ -1380,11 +1380,11 @@ def get_global_options(lang: str, cargs = coredata.UserArrayOption( description + ' compiler', - comp_options, split_args=True, user_input=True, allow_dups=True) + comp_options, split_args=True, allow_dups=True) largs = coredata.UserArrayOption( description + ' linker', - link_options, split_args=True, user_input=True, allow_dups=True) + link_options, split_args=True, allow_dups=True) if comp.INVOKES_LINKER and comp_key == envkey: # If the compiler acts as a linker driver, and we're using the diff --git a/mesonbuild/coredata.py b/mesonbuild/coredata.py index 54d9b1d02..975719e89 100644 --- a/mesonbuild/coredata.py +++ b/mesonbuild/coredata.py @@ -247,23 +247,16 @@ class UserComboOption(UserOption[str]): class UserArrayOption(UserOption[T.List[str]]): def __init__(self, description: str, value: T.Union[str, T.List[str]], - split_args: bool = False, user_input: bool = False, + split_args: bool = False, allow_dups: bool = False, yielding: bool = DEFAULT_YIELDING, choices: T.Optional[T.List[str]] = None, deprecated: T.Union[bool, str, T.Dict[str, str], T.List[str]] = False): super().__init__(description, choices if choices is not None else [], yielding, deprecated) self.split_args = split_args self.allow_dups = allow_dups - self.value = self.validate_value(value, user_input=user_input) - - def listify(self, value: T.Union[str, T.List[str]], user_input: bool = True) -> T.List[str]: - # User input is for options defined on the command line (via -D - # options). Users can put their input in as a comma separated - # string, but for defining options in meson_options.txt the format - # should match that of a combo - if not user_input and isinstance(value, str) and not value.startswith('['): - raise MesonException('Value does not define an array: ' + value) + self.set_value(value) + def listify(self, value: T.Union[str, T.List[str]]) -> T.List[str]: if isinstance(value, str): if value.startswith('['): try: @@ -283,8 +276,8 @@ class UserArrayOption(UserOption[T.List[str]]): raise MesonException(f'"{value}" should be a string array, but it is not') return newvalue - def validate_value(self, value: T.Union[str, T.List[str]], user_input: bool = True) -> T.List[str]: - newvalue = self.listify(value, user_input) + def validate_value(self, value: T.Union[str, T.List[str]]) -> 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. ' \ diff --git a/mesonbuild/mcompile.py b/mesonbuild/mcompile.py index 4e46702f0..950b27ca9 100644 --- a/mesonbuild/mcompile.py +++ b/mesonbuild/mcompile.py @@ -35,7 +35,7 @@ if T.TYPE_CHECKING: import argparse def array_arg(value: str) -> T.List[str]: - return UserArrayOption(None, value, allow_dups=True, user_input=True).value + return UserArrayOption(None, value, allow_dups=True).value def validate_builddir(builddir: Path) -> None: if not (builddir / 'meson-private' / 'coredata.dat').is_file(): diff --git a/mesonbuild/optinterpreter.py b/mesonbuild/optinterpreter.py index 8377614fe..2756997f5 100644 --- a/mesonbuild/optinterpreter.py +++ b/mesonbuild/optinterpreter.py @@ -20,7 +20,7 @@ from . import coredata from . import mesonlib from . import mparser from . import mlog -from .interpreterbase import FeatureNew, typed_pos_args, typed_kwargs, ContainerTypeInfo, KwargInfo +from .interpreterbase import FeatureNew, FeatureDeprecated, typed_pos_args, typed_kwargs, ContainerTypeInfo, KwargInfo from .interpreter.type_checking import NoneType, in_set_validator if T.TYPE_CHECKING: @@ -266,6 +266,11 @@ class OptionInterpreter: def string_array_parser(self, description: str, args: T.Tuple[bool, _DEPRECATED_ARGS], kwargs: StringArrayArgs) -> coredata.UserOption: choices = kwargs['choices'] value = kwargs['value'] if kwargs['value'] is not None else choices + if isinstance(value, str): + if value.startswith('['): + FeatureDeprecated('String value for array option', '1.2.0').use(self.subproject) + else: + raise mesonlib.MesonException('Value does not define an array: ' + value) return coredata.UserArrayOption(description, value, choices=choices, yielding=args[0], From 7600856e0a1b1e058ef684928ac29a92218b1257 Mon Sep 17 00:00:00 2001 From: Xavier Claessens Date: Wed, 3 May 2023 10:20:15 -0400 Subject: [PATCH 2/3] UserArrayOption: Make listify_value() a static method --- mesonbuild/coredata.py | 8 ++++++-- mesonbuild/mcompile.py | 2 +- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/mesonbuild/coredata.py b/mesonbuild/coredata.py index 975719e89..97261d67a 100644 --- a/mesonbuild/coredata.py +++ b/mesonbuild/coredata.py @@ -256,7 +256,8 @@ class UserArrayOption(UserOption[T.List[str]]): self.allow_dups = allow_dups self.set_value(value) - def listify(self, value: T.Union[str, T.List[str]]) -> T.List[str]: + @staticmethod + def listify_value(value: T.Union[str, T.List[str]], shlex_split_args: bool = False) -> T.List[str]: if isinstance(value, str): if value.startswith('['): try: @@ -266,7 +267,7 @@ class UserArrayOption(UserOption[T.List[str]]): elif value == '': newvalue = [] else: - if self.split_args: + if shlex_split_args: newvalue = split_args(value) else: newvalue = [v.strip() for v in value.split(',')] @@ -276,6 +277,9 @@ class UserArrayOption(UserOption[T.List[str]]): raise MesonException(f'"{value}" should be a string array, but it is not') return newvalue + def listify(self, value: T.Any) -> T.List[T.Any]: + return self.listify_value(value, self.split_args) + def validate_value(self, value: T.Union[str, T.List[str]]) -> T.List[str]: newvalue = self.listify(value) diff --git a/mesonbuild/mcompile.py b/mesonbuild/mcompile.py index 950b27ca9..b9bd71bd3 100644 --- a/mesonbuild/mcompile.py +++ b/mesonbuild/mcompile.py @@ -35,7 +35,7 @@ if T.TYPE_CHECKING: import argparse def array_arg(value: str) -> T.List[str]: - return UserArrayOption(None, value, allow_dups=True).value + return UserArrayOption.listify_value(value) def validate_builddir(builddir: Path) -> None: if not (builddir / 'meson-private' / 'coredata.dat').is_file(): From 82a8c72187f844713618526ed3890d7b313b2065 Mon Sep 17 00:00:00 2001 From: Xavier Claessens Date: Thu, 28 Apr 2022 16:08:24 -0400 Subject: [PATCH 3/3] c_std, cpp_std: Change to a list of desired versions in preference order Projects that prefer GNU C but can fallback to ISO C can now set for example `default_options: 'c_std=gnu11,c11'` and it will use gnu11 when available, fallback to c11 otherwise. It is an error only if none of the values are supported by the current compiler. This allows to deprecate gnuXX values from MSVC compiler, that means that `default_options: 'c_std=gnu11'` will now print warning with MSVC but still fallback to 'c11' value. No warning is printed if at least one of the values is valid, i.e. `default_options: 'c_std=gnu11,c11'`. In the future that deprecation warning will become an hard error because `c_std=gnu11` should mean GNU is required, for projects that cannot be built with MSVC for example. --- docs/markdown/Builtin-options.md | 15 +++ docs/markdown/snippets/cstd.md | 18 ++++ mesonbuild/compilers/c.py | 114 ++++++++++----------- mesonbuild/compilers/cpp.py | 76 +++++++------- mesonbuild/coredata.py | 53 ++++++++++ mesonbuild/optinterpreter.py | 2 +- test cases/unit/114 c cpp stds/meson.build | 6 ++ unittests/allplatformstests.py | 33 ++++++ unittests/baseplatformtests.py | 7 ++ 9 files changed, 229 insertions(+), 95 deletions(-) create mode 100644 docs/markdown/snippets/cstd.md create mode 100644 test cases/unit/114 c cpp stds/meson.build diff --git a/docs/markdown/Builtin-options.md b/docs/markdown/Builtin-options.md index fed893e10..cd86e22b7 100644 --- a/docs/markdown/Builtin-options.md +++ b/docs/markdown/Builtin-options.md @@ -296,6 +296,21 @@ is inherited from the main project. This is useful, for example, when the main project requires C++11, but a subproject requires C++14. The `cpp_std` value from the subproject's `default_options` is now respected. +Since *1.3.0* `c_std` and `cpp_std` options now accept a list of values. +Projects that prefer GNU C, but can fallback to ISO C, can now set, for +example, `default_options: 'c_std=gnu11,c11'`, and it will use `gnu11` when +available, but fallback to c11 otherwise. It is an error only if none of the +values are supported by the current compiler. +Likewise, a project that can take benefit of `c++17` but can still build with +`c++11` can set `default_options: 'cpp_std=c++17,c++11'`. +This allows us to deprecate `gnuXX` values from the MSVC compiler. That means +that `default_options: 'c_std=gnu11'` will now print a warning with MSVC +but fallback to `c11`. No warning is printed if at least one +of the values is valid, i.e. `default_options: 'c_std=gnu11,c11'`. +In the future that deprecation warning will become an hard error because +`c_std=gnu11` should mean GNU is required, for projects that cannot be +built with MSVC for example. + ## Specifying options per machine Since *0.51.0*, some options are specified per machine rather than diff --git a/docs/markdown/snippets/cstd.md b/docs/markdown/snippets/cstd.md new file mode 100644 index 000000000..cc1f08318 --- /dev/null +++ b/docs/markdown/snippets/cstd.md @@ -0,0 +1,18 @@ +## `c_std` and `cpp_std` options now accepts a list of values + +Projects that prefer GNU C, but can fallback to ISO C, can now set, for +example, `default_options: 'c_std=gnu11,c11'`, and it will use `gnu11` when +available, but fallback to `c11` otherwise. It is an error only if none of the +values are supported by the current compiler. + +Likewise, a project that can take benefit of `c++17` but can still build with +`c++11` can set `default_options: 'cpp_std=c++17,c++11'`. + +This allows us to deprecate `gnuXX` values from the MSVC compiler. That means +that `default_options: 'c_std=gnu11'` will now print a warning with MSVC +but fallback to `c11`. No warning is printed if at least one +of the values is valid, i.e. `default_options: 'c_std=gnu11,c11'`. + +In the future that deprecation warning will become an hard error because +`c_std=gnu11` should mean GNU is required, for projects that cannot be +built with MSVC for example. diff --git a/mesonbuild/compilers/c.py b/mesonbuild/compilers/c.py index d514650de..7f9e5844c 100644 --- a/mesonbuild/compilers/c.py +++ b/mesonbuild/compilers/c.py @@ -56,6 +56,10 @@ if T.TYPE_CHECKING: else: CompilerMixinBase = object +_ALL_STDS = ['c89', 'c9x', 'c90', 'c99', 'c1x', 'c11', 'c17', 'c18', 'c2x'] +_ALL_STDS += [f'gnu{std[1:]}' for std in _ALL_STDS] +_ALL_STDS += ['iso9899:1990', 'iso9899:199409', 'iso9899:1999', 'iso9899:2011', 'iso9899:2017', 'iso9899:2018'] + class CCompiler(CLikeCompiler, Compiler): def attribute_check_func(self, name: str) -> str: @@ -101,12 +105,9 @@ class CCompiler(CLikeCompiler, Compiler): def get_options(self) -> 'MutableKeyedOptionDictType': opts = super().get_options() + key = OptionKey('std', machine=self.for_machine, lang=self.language) opts.update({ - OptionKey('std', machine=self.for_machine, lang=self.language): coredata.UserComboOption( - 'C language standard to use', - ['none'], - 'none', - ) + key: coredata.UserStdOption('C', _ALL_STDS), }) return opts @@ -125,20 +126,18 @@ class _ClangCStds(CompilerMixinBase): def get_options(self) -> 'MutableKeyedOptionDictType': opts = super().get_options() - c_stds = ['c89', 'c99', 'c11'] - g_stds = ['gnu89', 'gnu99', 'gnu11'] + stds = ['c89', 'c99', 'c11'] # https://releases.llvm.org/6.0.0/tools/clang/docs/ReleaseNotes.html # https://en.wikipedia.org/wiki/Xcode#Latest_versions if version_compare(self.version, self._C17_VERSION): - c_stds += ['c17'] - g_stds += ['gnu17'] + stds += ['c17'] if version_compare(self.version, self._C18_VERSION): - c_stds += ['c18'] - g_stds += ['gnu18'] + stds += ['c18'] if version_compare(self.version, self._C2X_VERSION): - c_stds += ['c2x'] - g_stds += ['gnu2x'] - opts[OptionKey('std', machine=self.for_machine, lang=self.language)].choices = ['none'] + c_stds + g_stds + stds += ['c2x'] + std_opt = opts[OptionKey('std', machine=self.for_machine, lang=self.language)] + assert isinstance(std_opt, coredata.UserStdOption), 'for mypy' + std_opt.set_versions(stds, gnu=True) return opts @@ -244,8 +243,9 @@ class ArmclangCCompiler(ArmclangCompiler, CCompiler): def get_options(self) -> 'MutableKeyedOptionDictType': opts = CCompiler.get_options(self) - key = OptionKey('std', machine=self.for_machine, lang=self.language) - opts[key].choices = ['none', 'c90', 'c99', 'c11', 'gnu90', 'gnu99', 'gnu11'] + std_opt = opts[OptionKey('std', machine=self.for_machine, lang=self.language)] + assert isinstance(std_opt, coredata.UserStdOption), 'for mypy' + std_opt.set_versions(['c90', 'c99', 'c11'], gnu=True) return opts def get_option_compile_args(self, options: 'KeyedOptionDictType') -> T.List[str]: @@ -285,16 +285,15 @@ class GnuCCompiler(GnuCompiler, CCompiler): def get_options(self) -> 'MutableKeyedOptionDictType': opts = CCompiler.get_options(self) - c_stds = ['c89', 'c99', 'c11'] - g_stds = ['gnu89', 'gnu99', 'gnu11'] + stds = ['c89', 'c99', 'c11'] if version_compare(self.version, self._C18_VERSION): - c_stds += ['c17', 'c18'] - g_stds += ['gnu17', 'gnu18'] + stds += ['c17', 'c18'] if version_compare(self.version, self._C2X_VERSION): - c_stds += ['c2x'] - g_stds += ['gnu2x'] + stds += ['c2x'] key = OptionKey('std', machine=self.for_machine, lang=self.language) - opts[key].choices = ['none'] + c_stds + g_stds + std_opt = opts[key] + assert isinstance(std_opt, coredata.UserStdOption), 'for mypy' + std_opt.set_versions(stds, gnu=True) if self.info.is_windows() or self.info.is_cygwin(): opts.update({ key.evolve('winlibs'): coredata.UserArrayOption( @@ -370,7 +369,9 @@ class ElbrusCCompiler(ElbrusCompiler, CCompiler): stds += ['c90', 'c1x', 'gnu90', 'gnu1x', 'iso9899:2011'] if version_compare(self.version, '>=1.26.00'): stds += ['c17', 'c18', 'iso9899:2017', 'iso9899:2018', 'gnu17', 'gnu18'] - opts[OptionKey('std', machine=self.for_machine, lang=self.language)].choices = ['none'] + stds + std_opt = opts[OptionKey('std', machine=self.for_machine, lang=self.language)] + assert isinstance(std_opt, coredata.UserStdOption), 'for mypy' + std_opt.set_versions(stds) return opts # Elbrus C compiler does not have lchmod, but there is only linker warning, not compiler error. @@ -404,11 +405,12 @@ class IntelCCompiler(IntelGnuLikeCompiler, CCompiler): def get_options(self) -> 'MutableKeyedOptionDictType': opts = CCompiler.get_options(self) - c_stds = ['c89', 'c99'] - g_stds = ['gnu89', 'gnu99'] + stds = ['c89', 'c99'] if version_compare(self.version, '>=16.0.0'): - c_stds += ['c11'] - opts[OptionKey('std', machine=self.for_machine, lang=self.language)].choices = ['none'] + c_stds + g_stds + stds += ['c11'] + std_opt = opts[OptionKey('std', machine=self.for_machine, lang=self.language)] + assert isinstance(std_opt, coredata.UserStdOption), 'for mypy' + std_opt.set_versions(stds, gnu=True) return opts def get_option_compile_args(self, options: 'KeyedOptionDictType') -> T.List[str]: @@ -465,33 +467,23 @@ class VisualStudioCCompiler(MSVCCompiler, VisualStudioLikeCCompilerMixin, CCompi def get_options(self) -> 'MutableKeyedOptionDictType': opts = super().get_options() - c_stds = ['c89', 'c99'] - # Need to have these to be compatible with projects - # that set c_std to e.g. gnu99. - # https://github.com/mesonbuild/meson/issues/7611 - g_stds = ['gnu89', 'gnu90', 'gnu9x', 'gnu99'] + stds = ['c89', 'c99'] if version_compare(self.version, self._C11_VERSION): - c_stds += ['c11'] - g_stds += ['gnu1x', 'gnu11'] + stds += ['c11'] if version_compare(self.version, self._C17_VERSION): - c_stds += ['c17', 'c18'] - g_stds += ['gnu17', 'gnu18'] - key = OptionKey('std', machine=self.for_machine, lang=self.language) - opts[key].choices = ['none'] + c_stds + g_stds + stds += ['c17', 'c18'] + std_opt = opts[OptionKey('std', machine=self.for_machine, lang=self.language)] + assert isinstance(std_opt, coredata.UserStdOption), 'for mypy' + std_opt.set_versions(stds, gnu=True, gnu_deprecated=True) return opts def get_option_compile_args(self, options: 'KeyedOptionDictType') -> T.List[str]: args = [] std = options[OptionKey('std', machine=self.for_machine, lang=self.language)] - if std.value.startswith('gnu'): - mlog.log( - 'cl.exe does not actually support gnu standards, and meson ' - 'will instead demote to the nearest ISO C standard. This ' - 'may cause compilation to fail.', once=True) # As of MVSC 16.8, /std:c11 and /std:c17 are the only valid C standard options. - if std.value in {'c11', 'gnu1x', 'gnu11'}: + if std.value in {'c11'}: args.append('/std:c11') - elif std.value in {'c17', 'c18', 'gnu17', 'gnu18'}: + elif std.value in {'c17', 'c18'}: args.append('/std:c17') return args @@ -531,8 +523,9 @@ class IntelClCCompiler(IntelVisualStudioLikeCompiler, VisualStudioLikeCCompilerM def get_options(self) -> 'MutableKeyedOptionDictType': opts = super().get_options() - key = OptionKey('std', machine=self.for_machine, lang=self.language) - opts[key].choices = ['none', 'c89', 'c99', 'c11'] + std_opt = opts[OptionKey('std', machine=self.for_machine, lang=self.language)] + assert isinstance(std_opt, coredata.UserStdOption), 'for mypy' + std_opt.set_versions(['c89', 'c99', 'c11']) return opts def get_option_compile_args(self, options: 'KeyedOptionDictType') -> T.List[str]: @@ -564,8 +557,9 @@ class ArmCCompiler(ArmCompiler, CCompiler): def get_options(self) -> 'MutableKeyedOptionDictType': opts = CCompiler.get_options(self) - key = OptionKey('std', machine=self.for_machine, lang=self.language) - opts[key].choices = ['none', 'c89', 'c99', 'c11'] + std_opt = opts[OptionKey('std', machine=self.for_machine, lang=self.language)] + assert isinstance(std_opt, coredata.UserStdOption), 'for mypy' + std_opt.set_versions(['c89', 'c99', 'c11']) return opts def get_option_compile_args(self, options: 'KeyedOptionDictType') -> T.List[str]: @@ -593,8 +587,9 @@ class CcrxCCompiler(CcrxCompiler, CCompiler): def get_options(self) -> 'MutableKeyedOptionDictType': opts = CCompiler.get_options(self) - key = OptionKey('std', machine=self.for_machine, lang=self.language) - opts[key].choices = ['none', 'c89', 'c99'] + std_opt = opts[OptionKey('std', machine=self.for_machine, lang=self.language)] + assert isinstance(std_opt, coredata.UserStdOption), 'for mypy' + std_opt.set_versions(['c89', 'c99']) return opts def get_no_stdinc_args(self) -> T.List[str]: @@ -640,8 +635,9 @@ class Xc16CCompiler(Xc16Compiler, CCompiler): def get_options(self) -> 'MutableKeyedOptionDictType': opts = CCompiler.get_options(self) - key = OptionKey('std', machine=self.for_machine, lang=self.language) - opts[key].choices = ['none', 'c89', 'c99', 'gnu89', 'gnu99'] + std_opt = opts[OptionKey('std', machine=self.for_machine, lang=self.language)] + assert isinstance(std_opt, coredata.UserStdOption), 'for mypy' + std_opt.set_versions(['c89', 'c99'], gnu=True) return opts def get_no_stdinc_args(self) -> T.List[str]: @@ -685,8 +681,9 @@ class CompCertCCompiler(CompCertCompiler, CCompiler): def get_options(self) -> 'MutableKeyedOptionDictType': opts = CCompiler.get_options(self) - key = OptionKey('std', machine=self.for_machine, lang=self.language) - opts[key].choices = ['none', 'c89', 'c99'] + std_opt = opts[OptionKey('std', machine=self.for_machine, lang=self.language)] + assert isinstance(std_opt, coredata.UserStdOption), 'for mypy' + std_opt.set_versions(['c89', 'c99']) return opts def get_option_compile_args(self, options: 'KeyedOptionDictType') -> T.List[str]: @@ -722,8 +719,9 @@ class TICCompiler(TICompiler, CCompiler): def get_options(self) -> 'MutableKeyedOptionDictType': opts = CCompiler.get_options(self) - key = OptionKey('std', machine=self.for_machine, lang=self.language) - opts[key].choices = ['none', 'c89', 'c99', 'c11'] + std_opt = opts[OptionKey('std', machine=self.for_machine, lang=self.language)] + assert isinstance(std_opt, coredata.UserStdOption), 'for mypy' + std_opt.set_versions(['c89', 'c99', 'c11']) return opts def get_no_stdinc_args(self) -> T.List[str]: diff --git a/mesonbuild/compilers/cpp.py b/mesonbuild/compilers/cpp.py index 8c8043763..3e9668262 100644 --- a/mesonbuild/compilers/cpp.py +++ b/mesonbuild/compilers/cpp.py @@ -55,6 +55,10 @@ if T.TYPE_CHECKING: else: CompilerMixinBase = object +_ALL_STDS = ['c++98', 'c++0x', 'c++03', 'c++1y', 'c++1z', 'c++11', 'c++14', 'c++17', 'c++2a', 'c++20', 'c++23'] +_ALL_STDS += [f'gnu{std[1:]}' for std in _ALL_STDS] +_ALL_STDS += ['vc++11', 'vc++14', 'vc++17', 'vc++20', 'vc++latest', 'c++latest'] + def non_msvc_eh_options(eh: str, args: T.List[str]) -> None: if eh == 'none': @@ -178,11 +182,7 @@ class CPPCompiler(CLikeCompiler, Compiler): opts = super().get_options() key = OptionKey('std', machine=self.for_machine, lang=self.language) opts.update({ - key: coredata.UserComboOption( - 'C++ language standard to use', - ['none'], - 'none', - ), + key: coredata.UserStdOption('C++', _ALL_STDS), }) return opts @@ -257,17 +257,15 @@ class ClangCPPCompiler(_StdCPPLibMixin, ClangCompiler, CPPCompiler): key.evolve('rtti'): coredata.UserBooleanOption('Enable RTTI', True), }) cppstd_choices = [ - 'none', 'c++98', 'c++03', 'c++11', 'c++14', 'c++17', 'c++1z', - 'c++2a', 'c++20', 'gnu++11', 'gnu++14', 'gnu++17', 'gnu++1z', - 'gnu++2a', 'gnu++20', + 'c++98', 'c++03', 'c++11', 'c++14', 'c++17', 'c++1z', 'c++2a', 'c++20', ] if version_compare(self.version, self._CPP23_VERSION): cppstd_choices.append('c++23') - cppstd_choices.append('gnu++23') if version_compare(self.version, self._CPP26_VERSION): cppstd_choices.append('c++26') - cppstd_choices.append('gnu++26') - opts[key.evolve('std')].choices = cppstd_choices + std_opt = opts[key.evolve('std')] + assert isinstance(std_opt, coredata.UserStdOption), 'for mypy' + std_opt.set_versions(cppstd_choices, gnu=True) if self.info.is_windows() or self.info.is_cygwin(): opts.update({ key.evolve('winlibs'): coredata.UserArrayOption( @@ -371,10 +369,9 @@ class ArmclangCPPCompiler(ArmclangCompiler, CPPCompiler): 'default', ), }) - opts[key].choices = [ - 'none', 'c++98', 'c++03', 'c++11', 'c++14', 'c++17', 'gnu++98', - 'gnu++03', 'gnu++11', 'gnu++14', 'gnu++17', - ] + std_opt = opts[key] + assert isinstance(std_opt, coredata.UserStdOption), 'for mypy' + std_opt.set_versions(['c++98', 'c++03', 'c++11', 'c++14', 'c++17'], gnu=True) return opts def get_option_compile_args(self, options: 'KeyedOptionDictType') -> T.List[str]: @@ -426,17 +423,16 @@ class GnuCPPCompiler(_StdCPPLibMixin, GnuCompiler, CPPCompiler): ) }) cppstd_choices = [ - 'none', 'c++98', 'c++03', 'c++11', 'c++14', 'c++17', 'c++1z', - 'c++2a', 'c++20', 'gnu++03', 'gnu++11', 'gnu++14', 'gnu++17', - 'gnu++1z', 'gnu++2a', 'gnu++20', + 'c++98', 'c++03', 'c++11', 'c++14', 'c++17', 'c++1z', + 'c++2a', 'c++20', ] if version_compare(self.version, '>=12.2.0'): cppstd_choices.append('c++23') - cppstd_choices.append('gnu++23') if version_compare(self.version, '>=14.0.0'): cppstd_choices.append('c++26') - cppstd_choices.append('gnu++26') - opts[key].choices = cppstd_choices + std_opt = opts[key] + assert isinstance(std_opt, coredata.UserStdOption), 'for mypy' + std_opt.set_versions(cppstd_choices, gnu=True) if self.info.is_windows() or self.info.is_cygwin(): opts.update({ key.evolve('winlibs'): coredata.UserArrayOption( @@ -513,21 +509,21 @@ class ElbrusCPPCompiler(ElbrusCompiler, CPPCompiler): def get_options(self) -> 'MutableKeyedOptionDictType': opts = CPPCompiler.get_options(self) - cpp_stds = ['none', 'c++98', 'gnu++98'] + cpp_stds = ['c++98'] if version_compare(self.version, '>=1.20.00'): - cpp_stds += ['c++03', 'c++0x', 'c++11', 'gnu++03', 'gnu++0x', 'gnu++11'] + cpp_stds += ['c++03', 'c++0x', 'c++11'] if version_compare(self.version, '>=1.21.00') and version_compare(self.version, '<1.22.00'): - cpp_stds += ['c++14', 'gnu++14', 'c++1y', 'gnu++1y'] + cpp_stds += ['c++14', 'c++1y'] if version_compare(self.version, '>=1.22.00'): - cpp_stds += ['c++14', 'gnu++14'] + cpp_stds += ['c++14'] if version_compare(self.version, '>=1.23.00'): - cpp_stds += ['c++1y', 'gnu++1y'] + cpp_stds += ['c++1y'] if version_compare(self.version, '>=1.24.00'): - cpp_stds += ['c++1z', 'c++17', 'gnu++1z', 'gnu++17'] + cpp_stds += ['c++1z', 'c++17'] if version_compare(self.version, '>=1.25.00'): - cpp_stds += ['c++2a', 'gnu++2a'] + cpp_stds += ['c++2a'] if version_compare(self.version, '>=1.26.00'): - cpp_stds += ['c++20', 'gnu++20'] + cpp_stds += ['c++20'] key = OptionKey('std', machine=self.for_machine, lang=self.language) opts.update({ @@ -541,7 +537,9 @@ class ElbrusCPPCompiler(ElbrusCompiler, CPPCompiler): False, ), }) - opts[key].choices = cpp_stds + std_opt = opts[key] + assert isinstance(std_opt, coredata.UserStdOption), 'for mypy' + std_opt.set_versions(cpp_stds, gnu=True) return opts # Elbrus C++ compiler does not have lchmod, but there is only linker warning, not compiler error. @@ -615,7 +613,9 @@ class IntelCPPCompiler(IntelGnuLikeCompiler, CPPCompiler): key.evolve('rtti'): coredata.UserBooleanOption('Enable RTTI', True), key.evolve('debugstl'): coredata.UserBooleanOption('STL debug mode', False), }) - opts[key].choices = ['none'] + c_stds + g_stds + std_opt = opts[key] + assert isinstance(std_opt, coredata.UserStdOption), 'for mypy' + std_opt.set_versions(c_stds + g_stds) return opts def get_option_compile_args(self, options: 'KeyedOptionDictType') -> T.List[str]: @@ -682,7 +682,9 @@ class VisualStudioLikeCPPCompilerMixin(CompilerMixinBase): msvc_winlibs, ), }) - opts[key.evolve('std')].choices = cpp_stds + std_opt = opts[key] + assert isinstance(std_opt, coredata.UserStdOption), 'for mypy' + std_opt.set_versions(cpp_stds) return opts def get_option_compile_args(self, options: 'KeyedOptionDictType') -> T.List[str]: @@ -846,8 +848,9 @@ class ArmCPPCompiler(ArmCompiler, CPPCompiler): def get_options(self) -> 'MutableKeyedOptionDictType': opts = CPPCompiler.get_options(self) - key = OptionKey('std', machine=self.for_machine, lang=self.language) - opts[key].choices = ['none', 'c++03', 'c++11'] + std_opt = opts[OptionKey('std', machine=self.for_machine, lang=self.language)] + assert isinstance(std_opt, coredata.UserStdOption), 'for mypy' + std_opt.set_versions(['c++03', 'c++11']) return opts def get_option_compile_args(self, options: 'KeyedOptionDictType') -> T.List[str]: @@ -906,8 +909,9 @@ class TICPPCompiler(TICompiler, CPPCompiler): def get_options(self) -> 'MutableKeyedOptionDictType': opts = CPPCompiler.get_options(self) - key = OptionKey('std', machine=self.for_machine, lang=self.language) - opts[key].choices = ['none', 'c++03'] + std_opt = opts[OptionKey('std', machine=self.for_machine, lang=self.language)] + assert isinstance(std_opt, coredata.UserStdOption), 'for mypy' + std_opt.set_versions(['c++03']) return opts def get_option_compile_args(self, options: 'KeyedOptionDictType') -> T.List[str]: diff --git a/mesonbuild/coredata.py b/mesonbuild/coredata.py index 97261d67a..11848664a 100644 --- a/mesonbuild/coredata.py +++ b/mesonbuild/coredata.py @@ -320,6 +320,59 @@ class UserFeatureOption(UserComboOption): def is_auto(self) -> bool: return self.value == 'auto' +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] = {} + super().__init__(f'{lang} language standard to use', ['none'], '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: T.Union[str, T.List[str]]) -> str: + candidates = UserArrayOption.listify_value(value) + unknown = [std for std in candidates if std not in self.all_stds] + if unknown: + raise MesonException(f'Unknown {self.lang.upper()} std {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 the future.\n' + + 'If the project supports both GNU and MSVC compilers, a value such as\n' + + '"c_std=gnu11,c11" specifies that GNU is prefered but it can safely fallback to plain c11.') + return newstd + raise MesonException(f'None of values {candidates} are supported by the {self.lang.upper()} compiler. ' + + f'Possible values are {self.choices}') class DependencyCacheType(enum.Enum): diff --git a/mesonbuild/optinterpreter.py b/mesonbuild/optinterpreter.py index 2756997f5..8ad84aaa8 100644 --- a/mesonbuild/optinterpreter.py +++ b/mesonbuild/optinterpreter.py @@ -268,7 +268,7 @@ class OptionInterpreter: value = kwargs['value'] if kwargs['value'] is not None else choices if isinstance(value, str): if value.startswith('['): - FeatureDeprecated('String value for array option', '1.2.0').use(self.subproject) + FeatureDeprecated('String value for array option', '1.3.0').use(self.subproject) else: raise mesonlib.MesonException('Value does not define an array: ' + value) return coredata.UserArrayOption(description, value, diff --git a/test cases/unit/114 c cpp stds/meson.build b/test cases/unit/114 c cpp stds/meson.build new file mode 100644 index 000000000..0b15efc08 --- /dev/null +++ b/test cases/unit/114 c cpp stds/meson.build @@ -0,0 +1,6 @@ +project('c cpp stds', 'c', 'cpp', + default_options: [ + 'c_std=gnu89,c89', + 'cpp_std=gnu++98,vc++11', + ], +) diff --git a/unittests/allplatformstests.py b/unittests/allplatformstests.py index 7f38d4b09..1a34f5dd5 100644 --- a/unittests/allplatformstests.py +++ b/unittests/allplatformstests.py @@ -4776,3 +4776,36 @@ class AllPlatformTests(BasePlatformTests): self.assertNotEqual(olddata, newdata) olddata = newdata oldmtime = newmtime + + def test_c_cpp_stds(self): + testdir = os.path.join(self.unit_test_dir, '114 c cpp stds') + self.init(testdir) + # Invalid values should fail whatever compiler we have + with self.assertRaises(subprocess.CalledProcessError): + self.setconf('-Dc_std=invalid') + with self.assertRaises(subprocess.CalledProcessError): + self.setconf('-Dc_std=c89,invalid') + with self.assertRaises(subprocess.CalledProcessError): + self.setconf('-Dc_std=c++11') + env = get_fake_env() + cc = detect_c_compiler(env, MachineChoice.HOST) + if cc.get_id() == 'msvc': + # default_option should have selected those + self.assertEqual(self.getconf('c_std'), 'c89') + self.assertEqual(self.getconf('cpp_std'), 'vc++11') + # This is deprecated but works for C + self.setconf('-Dc_std=gnu99') + self.assertEqual(self.getconf('c_std'), 'c99') + # C++ however never accepted that fallback + with self.assertRaises(subprocess.CalledProcessError): + self.setconf('-Dcpp_std=gnu++11') + # The first supported std should be selected + self.setconf('-Dcpp_std=gnu++11,vc++11,c++11') + self.assertEqual(self.getconf('cpp_std'), 'vc++11') + elif cc.get_id() == 'gcc': + # default_option should have selected those + self.assertEqual(self.getconf('c_std'), 'gnu89') + self.assertEqual(self.getconf('cpp_std'), 'gnu++98') + # The first supported std should be selected + self.setconf('-Dcpp_std=c++11,gnu++11,vc++11') + self.assertEqual(self.getconf('cpp_std'), 'c++11') diff --git a/unittests/baseplatformtests.py b/unittests/baseplatformtests.py index 3008eb731..b7deda5e0 100644 --- a/unittests/baseplatformtests.py +++ b/unittests/baseplatformtests.py @@ -303,6 +303,13 @@ class BasePlatformTests(TestCase): ensure_backend_detects_changes(self.backend) self._run(self.mconf_command + arg + [self.builddir]) + def getconf(self, optname: str): + opts = self.introspect('--buildoptions') + for x in opts: + if x.get('name') == optname: + return x.get('value') + self.fail(f'Option {optname} not found') + def wipe(self): windows_proof_rmtree(self.builddir)