Repository: mesonbuild/meson

Issue:
Add compiler flags detection method
This is especially important for C/C++ compilers, but I assume that other compilers that expose particular flags on the command line would benefit from it.

Currently, if I wanted to select a set of compiler arguments to pass to my projects I need to write something like this:

```
test_args = [
  '-ffast-math',
  '-fstrict-aliasing',
  '-Wpointer-arith',
  '-Wformat=2',
  '-Wstrict-prototype',
  '-Werror=init-self'
]
test_args_prog = '''
int someVariableJustForFun;
'''
foreach arg: test_args
  if cc.compiles(c_test_prog, name: 'check for ' + arg, c_args: [ arg ])
    common_c_args += arg
  endif
endforeach
```

This does not really work, because `cc.compiles()` does not have a `c_args` argument.

(translate to other, non-C languages for other compilers)

Since the `compiler` object already has various discovery methods, a `has_flag` or `has_argument` method would allow a more compact syntax:

```
foreach arg: test_args
  if cc.has_flag(arg)
    common_c_args += [ arg ]
  endif
endforeach
```

as well as ensuring portability.



Discussion:
Another option would be to have a:

```
array_of_found_flags = compiler.has_flags(array_of_flags)
```

method that tests all the flags in the array and returns an array containing all the found compiler flags.

Most Meson API that takes arrays of strings also takes a single string (or several strings) for convenience, so it makes sense so have this.

I'm just wondering how the check would actually be done. Trying to compile something with `-Werror -someflag`? Getting a full list via `$CC --help` or equivalent? Something else?

Trying to compile would work for me, and it would be similar to how header files are checked.

Something like this has been planned but not implemented due to some open questions:
- can we do this reliably across compilers, that is are there compilers that swallow unknown args (`-fdonothing`) or where the check depends on the code being compiled
  - is is sufficient to just compile or do people want to test linker flags too, the latter is complicated because when cross compiling linking sometimes does not work (see the sanity check tests inside `compilers.py` for an example)
  - what should we use for the compile test? Is a plain `int foo;` sufficien

The way I usually do this on autotools is to have a m4 macro that checks a list of flags, and stops at the first successful check, so I can do something like:

```
  check_TESTSET_CFLAG([CFLAGS], [-Wformat=2], [-Wformat])
```

which adds either `-Wformat=2` or `-Wformat` to the `CFLAGS` variable, depending on which one is found first; this can also be used to pass equivalent flags for multiple compilers, e.g. first pass a GCC argument, then pass a MSVC argument, etc. without have to deal with different branches matching the compiler name.

Looking at the various m4 macros I've used and shared among projects, `AC_COMPILE_IFELSE` is deemed enough to check for a compiler flag; see, for instance: http://git.savannah.gnu.org/cgit/autoconf-archive.git/tree/m4/ax_check_compile_flag.m4

 The source code for the test program can be limited to `int i;`

And if none of the flags are supported it should return empty array instead of erroring out?

I'm not sure I've ever encountered a case of a required compiler flag, so erroring out if none compiler flags are found sounds a bit harsh to me.

Returning an empty array would allow simpler constructs, like:

```
# Enable formating warnings on GCC-like compilers only
common_cflags += compiler.has_flags([ '-Wformat=2', '-Wformat-y2k' ])
```

instead of:

```
if compiler.get_id() == 'gcc' or compiler.get_id() == 'clang':
  common_cflags += compiler.has_flags([ '-Wformat=2', '-Wformat-y2k' ])
endif
```

But that's a minor issue.


Resolving patch:
diff --git a/mesonbuild/compilers.py b/mesonbuild/compilers.py
--- a/mesonbuild/compilers.py
+++ b/mesonbuild/compilers.py
@@ -293,6 +293,9 @@ def find_library(self, libname, extra_dirs):
     def get_library_dirs(self):
         return []
 
+    def has_argument(self, arg):
+        raise EnvironmentException('Language {} does not support has_arg.'.format(self.language))
+
 class CCompiler(Compiler):
     def __init__(self, exelist, version, is_cross, exe_wrapper=None):
         super().__init__(exelist, version)
@@ -506,6 +509,8 @@ def compile(self, code, srcname, extra_args=[]):
         return p
 
     def compiles(self, code, extra_args = []):
+        if isinstance(extra_args, str):
+            extra_args = [extra_args]
         suflen = len(self.default_suffix)
         (fd, srcname) = tempfile.mkstemp(suffix='.'+self.default_suffix)
         os.close(fd)
@@ -819,6 +824,9 @@ def thread_flags(self):
     def thread_link_flags(self):
         return ['-pthread']
 
+    def has_argument(self, arg):
+        return self.compiles('int i;\n', extra_args=arg)
+
 class CPPCompiler(CCompiler):
     def __init__(self, exelist, version, is_cross, exe_wrap):
         CCompiler.__init__(self, exelist, version, is_cross, exe_wrap)
@@ -1427,6 +1435,27 @@ def get_include_args(self, path, is_system):
         # msvc does not have a concept of system header dirs.
         return ['-I' + path]
 
+    # Visual Studio is special. It ignores arguments it does not
+    # understand and you can't tell it to error out on those.
+    # http://stackoverflow.com/questions/15259720/how-can-i-make-the-microsoft-c-compiler-treat-unknown-flags-as-errors-rather-t
+    def has_argument(self, arg):
+        warning_text = b'9002'
+        code = 'int i;\n'
+        (fd, srcname) = tempfile.mkstemp(suffix='.'+self.default_suffix)
+        os.close(fd)
+        ofile = open(srcname, 'w')
+        ofile.write(code)
+        ofile.close()
+        commands = self.exelist + [arg] + self.get_compile_only_args() + [srcname]
+        mlog.debug('Running VS compile:')
+        mlog.debug('Command line: ', ' '.join(commands))
+        mlog.debug('Code:\n', code)
+        p = subprocess.Popen(commands, cwd=os.path.split(srcname)[0], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+        (stde, stdo) = p.communicate()
+        if p.returncode != 0:
+            raise MesonException('Compiling test app failed.')
+        return not(warning_text in stde or warning_text in stdo)
+
 class VisualStudioCPPCompiler(VisualStudioCCompiler):
     def __init__(self, exelist, version, is_cross, exe_wrap):
         VisualStudioCCompiler.__init__(self, exelist, version, is_cross, exe_wrap)
diff --git a/mesonbuild/interpreter.py b/mesonbuild/interpreter.py
--- a/mesonbuild/interpreter.py
+++ b/mesonbuild/interpreter.py
@@ -583,6 +583,8 @@ def __init__(self, compiler, env):
                              'version' : self.version_method,
                              'cmd_array' : self.cmd_array_method,
                              'find_library': self.find_library_method,
+                             'has_argument' : self.has_argument_method,
+                             'first_supported_argument' : self.first_supported_argument_method,
                             })
 
     def version_method(self, args, kwargs):
@@ -789,6 +791,26 @@ def find_library_method(self, args, kwargs):
         lib = dependencies.ExternalLibrary(libname, linkargs)
         return ExternalLibraryHolder(lib)
 
+    def has_argument_method(self, args, kwargs):
+        args = mesonlib.stringlistify(args)
+        if len(args) != 1:
+            raise InterpreterException('Has_arg takes exactly one argument.')
+        result = self.compiler.has_argument(args[0])
+        if result:
+            h = mlog.green('YES')
+        else:
+            h = mlog.red('NO')
+        mlog.log('Compiler for {} supports argument {}:'.format(self.compiler.language, args[0]), h)
+        return result
+
+    def first_supported_argument_method(self, args, kwargs):
+        for i in mesonlib.stringlistify(args):
+            if self.compiler.has_argument(i):
+                mlog.log('First supported argument:', mlog.bold(i))
+                return [i]
+        mlog.log('First supported argument:', mlog.red('None'))
+        return []
+
 class ModuleState:
     pass
 


Test patch:
diff --git a/test cases/common/112 has arg/meson.build b/test cases/common/112 has arg/meson.build
new file mode 100644
--- /dev/null
+++ b/test cases/common/112 has arg/meson.build	
@@ -0,0 +1,35 @@
+project('has arg', 'c', 'cpp')
+
+cc = meson.get_compiler('c')
+cpp = meson.get_compiler('cpp')
+
+if cc.get_id() == 'msvc'
+  is_arg = '/O2'
+  useless = '/DFOO'
+else
+  is_arg = '-O2'
+  useless = '-DFOO'
+endif
+
+isnt_arg = '-fiambroken'
+
+assert(cc.has_argument(is_arg), 'Arg that should have worked does not work.')
+assert(not cc.has_argument(isnt_arg), 'Arg that should be broken is not.')
+
+assert(cpp.has_argument(is_arg), 'Arg that should have worked does not work.')
+assert(not cpp.has_argument(isnt_arg), 'Arg that should be broken is not.')
+
+# Have useless at the end to ensure that the search goes from front to back.
+l1 = cc.first_supported_argument([isnt_arg, is_arg, isnt_arg, useless])
+l2 = cc.first_supported_argument(isnt_arg, isnt_arg, isnt_arg)
+
+assert(l1.length() == 1, 'First supported returned wrong result.')
+assert(l1.get(0) == is_arg, 'First supported returned wrong argument.')
+assert(l2.length() == 0, 'First supported did not return empty array.')
+
+l1 = cpp.first_supported_argument([isnt_arg, is_arg, isnt_arg, useless])
+l2 = cpp.first_supported_argument(isnt_arg, isnt_arg, isnt_arg)
+
+assert(l1.length() == 1, 'First supported returned wrong result.')
+assert(l1.get(0) == is_arg, 'First supported returned wrong argument.')
+assert(l2.length() == 0, 'First supported did not return empty array.')