Repository: mesonbuild/meson

Issue:
windows msvc cross-compilation linker arguments are wrong for sanity-check
given a cross-file:
``
[binaries]
c = 'C:\\\\Program Files (x86)\\\\Microsoft Visual Studio\\\\2017\\\\Professional\\\\VC\\\\Tools\\\\MSVC\\\\14.11.25503\\\\bin\\\\Hostx64\\\\x86\\\\cl'


[properties]
c_link_args = ['/LIBPATH:C:\\\\Program Files (x86)\\\\Microsoft Visual Studio\\\\2017\\\\Professional\\\\VC\\\\Tools\\\\MSVC\\\\14.11.25503\\\\lib\\\\x86',
    '/LIBPATH:C:\\\\Program Files (x86)\\\\Windows Kits\\\\NETFXSDK\\\\4.6.1\\\\lib\\\\um\\\\x86',
    '/LIBPATH:C:\\\\Program Files (x86)\\\\Windows Kits\\\\10\\\\lib\\\\10.0.16299.0\\\\ucrt\\\\x86',
    '/LIBPATH:C:\\\\Program Files (x86)\\\\Windows Kits\\\\10\\\\lib\\\\10.0.16299.0\\\\um\\\\x86',
    '/MACHINE:X86']
``

it tries to run cl.exe sanity-check.c with linker arguments which are not recognized (linker arguments must be prefixed with /link and passed after all other options and source files) 


Discussion:
> linker arguments must be prefixed with /link and passed after all other options and source files

We do this correctly for arguments added from other sources, but I guess we missed this and no one ever tested it.

What happens if you add `/link` yourself in that list in the cross file?
Seems like I am just stumbling over the same issue:
The log output of my build contains:

Appending CFLAGS from environment: ' /DWIN32 /D_WINDOWS /W3 /utf-8  /MD /O2 /Oi /Gy /DNDEBUG /Z7'
Appending LDFLAGS from environment: ' /DEBUG  /INCREMENTAL:NO /OPT:REF /OPT:ICF'
Appending CPPFLAGS from environment: ' '
Sanity testing C compiler: cl
Is cross compiler: False.
Sanity check compiler command line: cl /MDd /DEBUG /INCREMENTAL:NO /OPT:REF /OPT:ICF C:\BUILDDIR\sanitycheckc.c /FeC:\BUILDDIR\sanitycheckc.exe

and a few lines later:

Sanity check compile stderr:
Microsoft (R) C/C++ Optimizing Compiler Version 19.21.27702.2 for x64
Copyright (C) Microsoft Corporation.  All rights reserved.

cl : Command line warning D9002 : ignoring unknown option '/OP'
cl : Command line warning D9002 : ignoring unknown option '/OT'
cl : Command line warning D9002 : ignoring unknown option '/O:'
cl : Command line warning D9002 : ignoring unknown option '/OR'
cl : Command line warning D9002 : ignoring unknown option '/OE'
cl : Command line warning D9002 : ignoring unknown option '/OF'
cl : Command line warning D9002 : ignoring unknown option '/OP'
cl : Command line warning D9002 : ignoring unknown option '/OT'
cl : Command line warning D9002 : ignoring unknown option '/O:'
cl : Command line warning D9002 : ignoring unknown option '/OI'
cl : Command line warning D9002 : ignoring unknown option '/OC'
cl : Command line warning D9002 : ignoring unknown option '/OF'

So it seems like LDFLAGS is just in the middle of the cl command line without prepending the necessary '/link' (see https://docs.microsoft.com/en-us/cpp/build/reference/compiler-command-line-syntax)

Digging into the mesonbuild code, I find that compiler.py does contain a routine 'linker_to_compiler_args' that prepends a '/link' option as well as a comment about correctly handling 'extra_flags' to deal with the /link flag.

However CLikeCompiler.sanity_check_impl uses its own code for constructing the command line which puts LDFLAGS somewhere in the middle of the options, which can't work for MSVC.
Most of those flags are things you should not need to have in CFLAGS et al. We provide builtin options for them, for example `/w3` is the internal option warning level and so on.
Well possible, but since LDFLAGS is passed on, I think it is important that it is passed on correctly.

Also, in my case, LDFLAGS is provided externally and having to do a reverse translation would be quite painful...
I'm trying to dig into this further and am wondering:

Why does CLikeCompiler.sanity_check_impl not go through the regular routines for compiling/linking in the first place? The routine constructs the command line in a completely non-standard way which is bound to lead to problems like this one.

B.t.w.: To answer your question @nirbheek : Adding /link to the cross file can't work. The sanity_check code not only fails to add /link but it also does not respect the necessary ordering. 'extra_flags' which contains the LDFLAGS is added before [source_name]

Resolving patch:
diff --git a/mesonbuild/compilers/clike.py b/mesonbuild/compilers/clike.py
--- a/mesonbuild/compilers/clike.py
+++ b/mesonbuild/compilers/clike.py
@@ -306,7 +306,8 @@ def sanity_check_impl(self, work_dir, environment, sname, code):
                 # a ton of compiler flags to differentiate between
                 # arm and x86_64. So just compile.
                 mode = 'compile'
-        extra_flags = self._get_basic_compiler_args(environment, mode)
+        cargs, largs = self._get_basic_compiler_args(environment, mode)
+        extra_flags = cargs + self.linker_to_compiler_args(largs)
 
         # Is a valid executable output for all toolchains and platforms
         binname += '.exe'
@@ -315,7 +316,9 @@ def sanity_check_impl(self, work_dir, environment, sname, code):
         with open(source_name, 'w') as ofile:
             ofile.write(code)
         # Compile sanity check
-        cmdlist = self.exelist + extra_flags + [source_name] + self.get_output_args(binary_name)
+        # NOTE: extra_flags must be added at the end. On MSVC, it might contain a '/link' argument
+        # after which all further arguments will be passed directly to the linker
+        cmdlist = self.exelist + [source_name] + self.get_output_args(binary_name) + extra_flags
         pc, stdo, stde = mesonlib.Popen_safe(cmdlist, cwd=work_dir)
         mlog.debug('Sanity check compiler command line:', ' '.join(cmdlist))
         mlog.debug('Sanity check compile stdout:')
@@ -380,10 +383,10 @@ def has_header_symbol(self, hname, symbol, prefix, env, *, extra_args=None, depe
                              dependencies=dependencies)
 
     def _get_basic_compiler_args(self, env, mode):
-        args = []
+        cargs, largs = [], []
         # Select a CRT if needed since we're linking
         if mode == 'link':
-            args += self.get_linker_debug_crt_args()
+            cargs += self.get_linker_debug_crt_args()
 
         # Add CFLAGS/CXXFLAGS/OBJCFLAGS/OBJCXXFLAGS and CPPFLAGS from the env
         sys_args = env.coredata.get_external_args(self.for_machine, self.language)
@@ -392,17 +395,17 @@ def _get_basic_compiler_args(self, env, mode):
         # also used during linking. These flags can break
         # argument checks. Thanks, Autotools.
         cleaned_sys_args = self.remove_linkerlike_args(sys_args)
-        args += cleaned_sys_args
+        cargs += cleaned_sys_args
 
         if mode == 'link':
             # Add LDFLAGS from the env
             sys_ld_args = env.coredata.get_external_link_args(self.for_machine, self.language)
             # CFLAGS and CXXFLAGS go to both linking and compiling, but we want them
             # to only appear on the command line once. Remove dupes.
-            args += [x for x in sys_ld_args if x not in sys_args]
+            largs += [x for x in sys_ld_args if x not in sys_args]
 
-        args += self.get_compiler_args_for_mode(mode)
-        return args
+        cargs += self.get_compiler_args_for_mode(mode)
+        return cargs, largs
 
     def _get_compiler_check_args(self, env, extra_args, dependencies, mode='compile'):
         if extra_args is None:
@@ -416,19 +419,27 @@ def _get_compiler_check_args(self, env, extra_args, dependencies, mode='compile'
         elif not isinstance(dependencies, list):
             dependencies = [dependencies]
         # Collect compiler arguments
-        args = compilers.CompilerArgs(self)
+        cargs = compilers.CompilerArgs(self)
+        largs = []
         for d in dependencies:
             # Add compile flags needed by dependencies
-            args += d.get_compile_args()
+            cargs += d.get_compile_args()
             if mode == 'link':
                 # Add link flags needed to find dependencies
-                args += d.get_link_args()
+                largs += d.get_link_args()
+
+        ca, la = self._get_basic_compiler_args(env, mode)
+        cargs += ca
+        largs += la
 
-        args += self._get_basic_compiler_args(env, mode)
+        cargs += self.get_compiler_check_args()
 
-        args += self.get_compiler_check_args()
-        # extra_args must override all other arguments, so we add them last
-        args += extra_args
+        # on MSVC compiler and linker flags must be separated by the "/link" argument
+        # at this point, the '/link' argument may already be part of extra_args, otherwise, it is added here
+        if self.linker_to_compiler_args([]) == ['/link'] and largs != [] and not ('/link' in extra_args):
+            extra_args += ['/link']
+
+        args = cargs + extra_args + largs
         return args
 
     def compiles(self, code, env, *, extra_args=None, dependencies=None, mode='compile', disable_cache=False):
@@ -1015,10 +1026,12 @@ def find_library_real(self, libname, env, extra_dirs, code, libtype: LibType):
         # search for .a. This is only allowed if libtype is LibType.PREFER_SHARED
         if ((not extra_dirs and libtype is LibType.PREFER_SHARED) or
                 libname in self.internal_libs):
-            args = ['-l' + libname]
-            largs = self.linker_to_compiler_args(self.get_allow_undefined_link_args())
-            if self.links(code, env, extra_args=(args + largs), disable_cache=True)[0]:
-                return args
+            cargs = ['-l' + libname]
+            largs = self.get_allow_undefined_link_args()
+            extra_args = cargs + self.linker_to_compiler_args(largs)
+
+            if self.links(code, env, extra_args=extra_args, disable_cache=True)[0]:
+                return cargs
             # Don't do a manual search for internal libs
             if libname in self.internal_libs:
                 return None


Test patch: