Repository: mesonbuild/meson

Issue:
[Suggestion] has_function should accept 'required' keyword
has_header_symbol accepts the required keyword so it can error when a symbol is missing. has_function is more robust since it checks that it's able to link, but doesn't accept the required keyword.
Since they're similar in purpose and usage, I see no reason has_function shouldn't offer the required keyword for convenience as well.

Checked against Meson 0.55 on Debian Bullseye GNU/Linux.
```
project('f', 'c')
cc = meson.get_compiler('c')
cc.has_function('puts', required : true)
```



Discussion:
Would be a nice contribution if you can submit a PR. Could be added to all those check functions IMHO.

Resolving patch:
diff --git a/mesonbuild/interpreter/compiler.py b/mesonbuild/interpreter/compiler.py
--- a/mesonbuild/interpreter/compiler.py
+++ b/mesonbuild/interpreter/compiler.py
@@ -30,6 +30,7 @@
     from ..interpreterbase import TYPE_var, TYPE_kwargs
     from .kwargs import ExtractRequired, ExtractSearchDirs
     from .interpreter.interpreter import SourceOutputs
+    from ..mlog import TV_LoggableList
 
     from typing_extensions import TypedDict, Literal
 
@@ -68,6 +69,12 @@ class CompupteIntKW(CommonKW):
     class HeaderKW(CommonKW, ExtractRequired):
         pass
 
+    class HasKW(CommonKW, ExtractRequired):
+        pass
+
+    class HasArgumentKW(ExtractRequired):
+        pass
+
     class FindLibraryKW(ExtractRequired, ExtractSearchDirs):
 
         disabler: bool
@@ -164,6 +171,7 @@ def stderr_method(self, args: T.List['TYPE_var'], kwargs: 'TYPE_kwargs') -> str:
 _COMPILES_KWS: T.List[KwargInfo] = [_NAME_KW, _ARGS_KW, _DEPENDENCIES_KW, _INCLUDE_DIRS_KW, _NO_BUILTIN_ARGS_KW]
 
 _HEADER_KWS: T.List[KwargInfo] = [REQUIRED_KW.evolve(since='0.50.0', default=False), *_COMMON_KWS]
+_HAS_REQUIRED_KW = REQUIRED_KW.evolve(since='1.3.0', default=False)
 
 class CompilerHolder(ObjectHolder['Compiler']):
     preprocess_uid = itertools.count()
@@ -324,9 +332,13 @@ def symbols_have_underscore_prefix_method(self, args: T.List['TYPE_var'], kwargs
         return self.compiler.symbols_have_underscore_prefix(self.environment)
 
     @typed_pos_args('compiler.has_member', str, str)
-    @typed_kwargs('compiler.has_member', *_COMMON_KWS)
-    def has_member_method(self, args: T.Tuple[str, str], kwargs: 'CommonKW') -> bool:
+    @typed_kwargs('compiler.has_member', _HAS_REQUIRED_KW, *_COMMON_KWS)
+    def has_member_method(self, args: T.Tuple[str, str], kwargs: 'HasKW') -> bool:
         typename, membername = args
+        disabled, required, feature = extract_required_kwarg(kwargs, self.subproject, default=False)
+        if disabled:
+            mlog.log('Type', mlog.bold(typename, True), 'has member', mlog.bold(membername, True), 'skipped: feature', mlog.bold(feature), 'disabled')
+            return False
         extra_args = functools.partial(self._determine_args, kwargs['no_builtin_args'], kwargs['include_directories'], kwargs['args'])
         deps, msg = self._determine_dependencies(kwargs['dependencies'])
         had, cached = self.compiler.has_members(typename, [membername], kwargs['prefix'],
@@ -334,7 +346,9 @@ def has_member_method(self, args: T.Tuple[str, str], kwargs: 'CommonKW') -> bool
                                                 extra_args=extra_args,
                                                 dependencies=deps)
         cached_msg = mlog.blue('(cached)') if cached else ''
-        if had:
+        if required and not had:
+            raise InterpreterException(f'{self.compiler.get_display_language()} member {membername!r} of type {typename!r} not usable')
+        elif had:
             hadtxt = mlog.green('YES')
         else:
             hadtxt = mlog.red('NO')
@@ -343,9 +357,14 @@ def has_member_method(self, args: T.Tuple[str, str], kwargs: 'CommonKW') -> bool
         return had
 
     @typed_pos_args('compiler.has_members', str, varargs=str, min_varargs=1)
-    @typed_kwargs('compiler.has_members', *_COMMON_KWS)
-    def has_members_method(self, args: T.Tuple[str, T.List[str]], kwargs: 'CommonKW') -> bool:
+    @typed_kwargs('compiler.has_members', _HAS_REQUIRED_KW, *_COMMON_KWS)
+    def has_members_method(self, args: T.Tuple[str, T.List[str]], kwargs: 'HasKW') -> bool:
         typename, membernames = args
+        members = mlog.bold(', '.join([f'"{m}"' for m in membernames]))
+        disabled, required, feature = extract_required_kwarg(kwargs, self.subproject, default=False)
+        if disabled:
+            mlog.log('Type', mlog.bold(typename, True), 'has members', members, 'skipped: feature', mlog.bold(feature), 'disabled')
+            return False
         extra_args = functools.partial(self._determine_args, kwargs['no_builtin_args'], kwargs['include_directories'], kwargs['args'])
         deps, msg = self._determine_dependencies(kwargs['dependencies'])
         had, cached = self.compiler.has_members(typename, membernames, kwargs['prefix'],
@@ -353,26 +372,34 @@ def has_members_method(self, args: T.Tuple[str, T.List[str]], kwargs: 'CommonKW'
                                                 extra_args=extra_args,
                                                 dependencies=deps)
         cached_msg = mlog.blue('(cached)') if cached else ''
-        if had:
+        if required and not had:
+            # print members as array: ['member1', 'member2']
+            raise InterpreterException(f'{self.compiler.get_display_language()} members {membernames!r} of type {typename!r} not usable')
+        elif had:
             hadtxt = mlog.green('YES')
         else:
             hadtxt = mlog.red('NO')
-        members = mlog.bold(', '.join([f'"{m}"' for m in membernames]))
         mlog.log('Checking whether type', mlog.bold(typename, True),
                  'has members', members, msg, hadtxt, cached_msg)
         return had
 
     @typed_pos_args('compiler.has_function', str)
-    @typed_kwargs('compiler.has_function', *_COMMON_KWS)
-    def has_function_method(self, args: T.Tuple[str], kwargs: 'CommonKW') -> bool:
+    @typed_kwargs('compiler.has_function', _HAS_REQUIRED_KW, *_COMMON_KWS)
+    def has_function_method(self, args: T.Tuple[str], kwargs: 'HasKW') -> bool:
         funcname = args[0]
+        disabled, required, feature = extract_required_kwarg(kwargs, self.subproject, default=False)
+        if disabled:
+            mlog.log('Has function', mlog.bold(funcname, True), 'skipped: feature', mlog.bold(feature), 'disabled')
+            return False
         extra_args = self._determine_args(kwargs['no_builtin_args'], kwargs['include_directories'], kwargs['args'])
         deps, msg = self._determine_dependencies(kwargs['dependencies'], compile_only=False)
         had, cached = self.compiler.has_function(funcname, kwargs['prefix'], self.environment,
                                                  extra_args=extra_args,
                                                  dependencies=deps)
         cached_msg = mlog.blue('(cached)') if cached else ''
-        if had:
+        if required and not had:
+            raise InterpreterException(f'{self.compiler.get_display_language()} function {funcname!r} not usable')
+        elif had:
             hadtxt = mlog.green('YES')
         else:
             hadtxt = mlog.red('NO')
@@ -380,15 +407,21 @@ def has_function_method(self, args: T.Tuple[str], kwargs: 'CommonKW') -> bool:
         return had
 
     @typed_pos_args('compiler.has_type', str)
-    @typed_kwargs('compiler.has_type', *_COMMON_KWS)
-    def has_type_method(self, args: T.Tuple[str], kwargs: 'CommonKW') -> bool:
+    @typed_kwargs('compiler.has_type', _HAS_REQUIRED_KW, *_COMMON_KWS)
+    def has_type_method(self, args: T.Tuple[str], kwargs: 'HasKW') -> bool:
         typename = args[0]
+        disabled, required, feature = extract_required_kwarg(kwargs, self.subproject, default=False)
+        if disabled:
+            mlog.log('Has type', mlog.bold(typename, True), 'skipped: feature', mlog.bold(feature), 'disabled')
+            return False
         extra_args = functools.partial(self._determine_args, kwargs['no_builtin_args'], kwargs['include_directories'], kwargs['args'])
         deps, msg = self._determine_dependencies(kwargs['dependencies'])
         had, cached = self.compiler.has_type(typename, kwargs['prefix'], self.environment,
                                              extra_args=extra_args, dependencies=deps)
         cached_msg = mlog.blue('(cached)') if cached else ''
-        if had:
+        if required and not had:
+            raise InterpreterException(f'{self.compiler.get_display_language()} type {typename!r} not usable')
+        elif had:
             hadtxt = mlog.green('YES')
         else:
             hadtxt = mlog.red('NO')
@@ -639,33 +672,46 @@ def find_library_method(self, args: T.Tuple[str], kwargs: 'FindLibraryKW') -> 'd
         return lib
 
     def _has_argument_impl(self, arguments: T.Union[str, T.List[str]],
-                           mode: _TestMode = _TestMode.COMPILER) -> bool:
+                           mode: _TestMode = _TestMode.COMPILER,
+                           kwargs: T.Optional['ExtractRequired'] = None) -> bool:
         """Shared implementation for methods checking compiler and linker arguments."""
         # This simplifies the callers
         if isinstance(arguments, str):
             arguments = [arguments]
-        test = self.compiler.has_multi_link_arguments if mode is _TestMode.LINKER else self.compiler.has_multi_arguments
-        result, cached = test(arguments, self.environment)
-        cached_msg = mlog.blue('(cached)') if cached else ''
-        mlog.log(
+        logargs: TV_LoggableList = [
             'Compiler for',
             self.compiler.get_display_language(),
             'supports{}'.format(' link' if mode is _TestMode.LINKER else ''),
             'arguments {}:'.format(' '.join(arguments)),
+        ]
+        kwargs = kwargs or {'required': False}
+        disabled, required, feature = extract_required_kwarg(kwargs, self.subproject, default=False)
+        if disabled:
+            logargs += ['skipped: feature', mlog.bold(feature), 'disabled']
+            mlog.log(*logargs)
+            return False
+        test = self.compiler.has_multi_link_arguments if mode is _TestMode.LINKER else self.compiler.has_multi_arguments
+        result, cached = test(arguments, self.environment)
+        if required and not result:
+            logargs += ['not usable']
+            raise InterpreterException(*logargs)
+        logargs += [
             mlog.green('YES') if result else mlog.red('NO'),
-            cached_msg)
+            mlog.blue('(cached)') if cached else '',
+        ]
+        mlog.log(*logargs)
         return result
 
-    @noKwargs
     @typed_pos_args('compiler.has_argument', str)
-    def has_argument_method(self, args: T.Tuple[str], kwargs: 'TYPE_kwargs') -> bool:
-        return self._has_argument_impl([args[0]])
+    @typed_kwargs('compiler.has_argument', _HAS_REQUIRED_KW)
+    def has_argument_method(self, args: T.Tuple[str], kwargs: 'HasArgumentKW') -> bool:
+        return self._has_argument_impl([args[0]], kwargs=kwargs)
 
-    @noKwargs
     @typed_pos_args('compiler.has_multi_arguments', varargs=str)
+    @typed_kwargs('compiler.has_multi_arguments', _HAS_REQUIRED_KW)
     @FeatureNew('compiler.has_multi_arguments', '0.37.0')
-    def has_multi_arguments_method(self, args: T.Tuple[T.List[str]], kwargs: 'TYPE_kwargs') -> bool:
-        return self._has_argument_impl(args[0])
+    def has_multi_arguments_method(self, args: T.Tuple[T.List[str]], kwargs: 'HasArgumentKW') -> bool:
+        return self._has_argument_impl(args[0], kwargs=kwargs)
 
     @FeatureNew('compiler.get_supported_arguments', '0.43.0')
     @typed_pos_args('compiler.get_supported_arguments', varargs=str)
@@ -700,16 +746,16 @@ def first_supported_argument_method(self, args: T.Tuple[T.List[str]], kwargs: 'T
         return []
 
     @FeatureNew('compiler.has_link_argument', '0.46.0')
-    @noKwargs
     @typed_pos_args('compiler.has_link_argument', str)
-    def has_link_argument_method(self, args: T.Tuple[str], kwargs: 'TYPE_kwargs') -> bool:
-        return self._has_argument_impl([args[0]], mode=_TestMode.LINKER)
+    @typed_kwargs('compiler.has_link_argument', _HAS_REQUIRED_KW)
+    def has_link_argument_method(self, args: T.Tuple[str], kwargs: 'HasArgumentKW') -> bool:
+        return self._has_argument_impl([args[0]], mode=_TestMode.LINKER, kwargs=kwargs)
 
     @FeatureNew('compiler.has_multi_link_argument', '0.46.0')
-    @noKwargs
     @typed_pos_args('compiler.has_multi_link_argument', varargs=str)
-    def has_multi_link_arguments_method(self, args: T.Tuple[T.List[str]], kwargs: 'TYPE_kwargs') -> bool:
-        return self._has_argument_impl(args[0], mode=_TestMode.LINKER)
+    @typed_kwargs('compiler.has_multi_link_argument', _HAS_REQUIRED_KW)
+    def has_multi_link_arguments_method(self, args: T.Tuple[T.List[str]], kwargs: 'HasArgumentKW') -> bool:
+        return self._has_argument_impl(args[0], mode=_TestMode.LINKER, kwargs=kwargs)
 
     @FeatureNew('compiler.get_supported_link_arguments', '0.46.0')
     @noKwargs
@@ -732,19 +778,33 @@ def first_supported_link_argument_method(self, args: T.Tuple[T.List[str]], kwarg
         mlog.log('First supported link argument:', mlog.red('None'))
         return []
 
-    def _has_function_attribute_impl(self, attr: str) -> bool:
+    def _has_function_attribute_impl(self, attr: str, kwargs: T.Optional['ExtractRequired'] = None) -> bool:
         """Common helper for function attribute testing."""
-        result, cached = self.compiler.has_func_attribute(attr, self.environment)
-        cached_msg = mlog.blue('(cached)') if cached else ''
-        h = mlog.green('YES') if result else mlog.red('NO')
-        mlog.log(f'Compiler for {self.compiler.get_display_language()} supports function attribute {attr}:', h, cached_msg)
-        return result
+        logargs: TV_LoggableList = [
+            f'Compiler for {self.compiler.get_display_language()} supports function attribute {attr}:',
+        ]
+        kwargs = kwargs or {'required': False}
+        disabled, required, feature = extract_required_kwarg(kwargs, self.subproject, default=False)
+        if disabled:
+            logargs += ['skipped: feature', mlog.bold(feature), 'disabled']
+            mlog.log(*logargs)
+            return False
+        had, cached = self.compiler.has_func_attribute(attr, self.environment)
+        if required and not had:
+            logargs += ['not usable']
+            raise InterpreterException(*logargs)
+        logargs += [
+            mlog.green('YES') if had else mlog.red('NO'),
+            mlog.blue('(cached)') if cached else ''
+        ]
+        mlog.log(*logargs)
+        return had
 
     @FeatureNew('compiler.has_function_attribute', '0.48.0')
-    @noKwargs
     @typed_pos_args('compiler.has_function_attribute', str)
-    def has_func_attribute_method(self, args: T.Tuple[str], kwargs: 'TYPE_kwargs') -> bool:
-        return self._has_function_attribute_impl(args[0])
+    @typed_kwargs('compiler.has_function_attribute', _HAS_REQUIRED_KW)
+    def has_func_attribute_method(self, args: T.Tuple[str], kwargs: 'HasArgumentKW') -> bool:
+        return self._has_function_attribute_impl(args[0], kwargs)
 
     @FeatureNew('compiler.get_supported_function_attributes', '0.48.0')
     @noKwargs


Test patch:
diff --git a/test cases/common/262 required keyword in has functions/meson.build b/test cases/common/262 required keyword in has functions/meson.build
new file mode 100644
--- /dev/null
+++ b/test cases/common/262 required keyword in has functions/meson.build	
@@ -0,0 +1,67 @@
+project('required keyword in has functions', 'c')
+
+cc = meson.get_compiler('c')
+opt = get_option('opt')
+
+cc.has_function('printf', prefix : '#include<stdio.h>', required : true)
+cc.has_type('time_t', prefix : '#include<time.h>', required : true)
+cc.has_member('struct tm', 'tm_sec', prefix : '#include<time.h>', required : true)
+cc.has_members('struct tm', ['tm_sec', 'tm_min'], prefix : '#include<time.h>', required : true)
+cc.has_header('time.h', required : true)
+cc.has_header_symbol('time.h', 'time', required : true)
+
+assert(not cc.has_function('printf', prefix : '#include<stdio.h>', required : opt))
+assert(not cc.has_type('time_t', prefix : '#include<time.h>', required : opt))
+assert(not cc.has_member('struct tm', 'tm_sec', prefix : '#include<time.h>', required : opt))
+assert(not cc.has_members('struct tm', ['tm_sec', 'tm_min'], prefix : '#include<time.h>', required : opt))
+assert(not cc.has_header('time.h', required : opt))
+assert(not cc.has_header_symbol('time.h', 'time', required : opt))
+
+# compiler.has_argument
+if cc.get_id() == 'msvc'
+  is_arg = '/O2'
+else
+  is_arg = '-O2'
+endif
+cc.has_argument(is_arg, required: true)
+assert(not cc.has_argument(is_arg, required: opt))
+
+# compiler.has_multi_arguments
+if cc.get_id() == 'gcc'
+  pre_arg = '-Wformat'
+  arg = '-Werror=format-security'
+  cc.has_multi_arguments([pre_arg, arg], required: true)
+  assert(not cc.has_multi_arguments(pre_arg, arg, required: opt))
+endif
+
+# compiler.has_link_argument
+if cc.get_argument_syntax() == 'msvc'
+  is_arg = '/OPT:REF'
+else
+  is_arg = '-Wl,-L/tmp'
+endif
+cc.has_link_argument(is_arg, required: true)
+assert(not cc.has_link_argument(is_arg, required: opt))
+
+# compiler.has_function_attribute
+if not ['pgi', 'msvc', 'clang-cl', 'intel-cl'].contains(cc.get_id())
+  a = 'aligned'
+  cc.has_function_attribute(a, required: true)
+  assert(not cc.has_function_attribute(a, required: opt))
+endif
+
+testcase expect_error('''compiler.has_function keyword argument 'required' was of type str but should have been one of: bool, UserFeatureOption''')
+  cc.has_function('printf', required : 'not a bool')
+endtestcase
+
+testcase expect_error('''C function 'asdfkawlegsdiovapfjhkr' not usable''')
+  cc.has_function('asdfkawlegsdiovapfjhkr', required : true)
+endtestcase
+
+testcase expect_error('''C header 'asdfkawlegsdiovapfjhkr.h' not found''')
+  cc.has_header('asdfkawlegsdiovapfjhkr.h', required : true)
+endtestcase
+
+testcase expect_error('''C symbol time_not_found not found in header time.h''')
+  cc.has_header_symbol('time.h', 'time_not_found', required : true)
+endtestcase
diff --git a/test cases/common/262 required keyword in has functions/meson_options.txt b/test cases/common/262 required keyword in has functions/meson_options.txt
new file mode 100644
--- /dev/null
+++ b/test cases/common/262 required keyword in has functions/meson_options.txt	
@@ -0,0 +1 @@
+option('opt', type: 'feature', value: 'disabled')