Repository: mesonbuild/meson

Issue:
CMake subproject does not add -pthread to the linker command line
**Describe the bug**

I've been trying to make [reproc](https://github.com/DaanDeMeyer/reproc) useable as a meson subproject. The current issue is that demo executables fail to link due to `-pthread` not being added to the linker command line by meson.

**To Reproduce**

meson.build:

```
project('tutorial', 'c')

cmake = import('cmake')

reproc = cmake.subproject('reproc')
libreproc = reproc.dependency('reproc')

executable('demo', ['main.c'], dependencies: [libreproc])
```

main.c (one of reproc's examples):

```c
#include <reproc/reproc.h>

#include <stdio.h>
#include <stdlib.h>

// Forwards the provided arguments to `reproc_start` which starts a child
// process that inherits the standard streams of the parent process. It will
// read its input from the stdin of this process and write its output to the
// stdout/stderr of this process.
int main(int argc, const char *argv[])
{
  if (argc <= 1) {
    fprintf(stderr,
            "No arguments provided. Example usage: ./inherit cmake --help");
    return 1;
  }

  reproc_t *process = reproc_new();

  reproc_options options = { .redirect = { .in = REPROC_REDIRECT_INHERIT,
                                           .out = REPROC_REDIRECT_INHERIT,
                                           .err = REPROC_REDIRECT_INHERIT } };
  int r = -1;

  r = reproc_start(process, argv + 1, options);
  if (r < 0) {
    goto cleanup;
  }

  r = reproc_wait(process, REPROC_INFINITE);
  if (r < 0) {
    goto cleanup;
  }

  r = reproc_exit_status(process);

cleanup:
  reproc_destroy(process);

  if (r < 0) {
    fprintf(stderr, "%s\n", reproc_error_string(r));
  }

  return abs(r);
}
```

```sh
git clone https://github.com/DaanDeMeyer/reproc.git subprojects/reproc
meson build
ninja -C build
```

**Expected behavior**

The build succeeds and I can run `build/demo cmake --help`. Instead, linking fails because `pthread_sigmask` cannot be found.

reproc enables `THREADS_PREFER_PTHREAD_FLAG` before calling `find_package(Threads)`. The pthreads dependency is added to reproc with `target_link_libraries(reproc PRIVATE Threads::Threads)`. Changing `PRIVATE` to `PUBLIC` or disabling `THREADS_PREFER_PTHREAD_FLAG` does not fix the issue.

**system parameters**
* Platform: Native
* OS: Arch Linux
* Python 3.8.1
* meson 0.52.1
* ninja 1.9.0



Discussion:
I was able to build your test project with the following `meson.build`:

```meson
project('tutorial', 'c')

cmake = import('cmake')

reproc = cmake.subproject('reproc')
libreproc = reproc.dependency('reproc')
threads = dependency('threads')

executable('demo', ['main.c'], dependencies: [libreproc, threads])
```

Unfortunately, you have to manually add the `threads` dependency, since meson (currently) does not know which flags from `reproc` are required for linking `demo`. Sadly, this is an internal limitation from mesons CMake integration and it is hard to work around this.
Alright, should anyone ever ask about using reproc with meson I can point them to this solution.

I assume the issue can be left open since there's still room for improvement?
Yes. I still have to figure out if this is a general problem or if it is pthread specific.
This is currently a general problem with static libraries. Unfortunately, CMake does not specify the required link args for *consuming* static libraries.
Yes you're right, passing `-DBUILD_SHARED_LIBS=ON` fixes the issue. I can report this in the CMake repository. Is it specifically the CMake File API that does not report the required link args?
Yes, this is the issue. Now that I think about it, it does make sense in some way that the link flags are not specified since they are not required for *building* the library. CMake does add the flags to an executable when linking with the static library. So, I am not sure if this is accidental or the intended behavior.

I think I can work around this by looking at the trace output and parsing `target_link_libraries`. We already use the CMake trace for `add_custom_command` anyway.

As for reporting this to CMake, I am not sure if they are willing to support this. See the discussion [here](https://gitlab.kitware.com/cmake/cmake/issues/19348). That being said, my [JSON trace MR](https://gitlab.kitware.com/cmake/cmake/merge_requests/4102) was merged recently.

Resolving patch:
diff --git a/mesonbuild/cmake/interpreter.py b/mesonbuild/cmake/interpreter.py
--- a/mesonbuild/cmake/interpreter.py
+++ b/mesonbuild/cmake/interpreter.py
@@ -301,6 +301,72 @@ def postprocess(self, output_target_map: OutputTargetMap, root_src_dir: str, sub
                 self.public_compile_opts += props.get('INTERFACE_COMPILE_DEFINITIONS', [])
                 self.public_compile_opts += props.get('INTERFACE_COMPILE_OPTIONS', [])
                 self.link_flags += props.get('INTERFACE_LINK_OPTIONS', [])
+
+            # TODO refactor this copy paste from CMakeDependency for future releases
+            reg_is_lib = re.compile(r'^(-l[a-zA-Z0-9_]+|-l?pthread)$')
+            to_process = [self.cmake_name]
+            processed = []
+            while len(to_process) > 0:
+                curr = to_process.pop(0)
+
+                if curr in processed or curr not in trace.targets:
+                    continue
+
+                tgt = trace.targets[curr]
+                cfgs = []
+                cfg = ''
+                otherDeps = []
+                libraries = []
+                mlog.debug(tgt)
+
+                if 'INTERFACE_COMPILE_DEFINITIONS' in tgt.properties:
+                    self.public_compile_opts += ['-D' + re.sub('^-D', '', x) for x in tgt.properties['INTERFACE_COMPILE_DEFINITIONS'] if x]
+
+                if 'INTERFACE_COMPILE_OPTIONS' in tgt.properties:
+                    self.public_compile_opts += [x for x in tgt.properties['INTERFACE_COMPILE_OPTIONS'] if x]
+
+                if 'IMPORTED_CONFIGURATIONS' in tgt.properties:
+                    cfgs += [x for x in tgt.properties['IMPORTED_CONFIGURATIONS'] if x]
+                    cfg = cfgs[0]
+
+                if 'CONFIGURATIONS' in tgt.properties:
+                    cfgs += [x for x in tgt.properties['CONFIGURATIONS'] if x]
+                    cfg = cfgs[0]
+
+                if 'RELEASE' in cfgs:
+                    cfg = 'RELEASE'
+
+                if 'IMPORTED_IMPLIB_{}'.format(cfg) in tgt.properties:
+                    libraries += [x for x in tgt.properties['IMPORTED_IMPLIB_{}'.format(cfg)] if x]
+                elif 'IMPORTED_IMPLIB' in tgt.properties:
+                    libraries += [x for x in tgt.properties['IMPORTED_IMPLIB'] if x]
+                elif 'IMPORTED_LOCATION_{}'.format(cfg) in tgt.properties:
+                    libraries += [x for x in tgt.properties['IMPORTED_LOCATION_{}'.format(cfg)] if x]
+                elif 'IMPORTED_LOCATION' in tgt.properties:
+                    libraries += [x for x in tgt.properties['IMPORTED_LOCATION'] if x]
+
+                if 'LINK_LIBRARIES' in tgt.properties:
+                    otherDeps += [x for x in tgt.properties['LINK_LIBRARIES'] if x]
+
+                if 'INTERFACE_LINK_LIBRARIES' in tgt.properties:
+                    otherDeps += [x for x in tgt.properties['INTERFACE_LINK_LIBRARIES'] if x]
+
+                if 'IMPORTED_LINK_DEPENDENT_LIBRARIES_{}'.format(cfg) in tgt.properties:
+                    otherDeps += [x for x in tgt.properties['IMPORTED_LINK_DEPENDENT_LIBRARIES_{}'.format(cfg)] if x]
+                elif 'IMPORTED_LINK_DEPENDENT_LIBRARIES' in tgt.properties:
+                    otherDeps += [x for x in tgt.properties['IMPORTED_LINK_DEPENDENT_LIBRARIES'] if x]
+
+                for j in otherDeps:
+                    if j in trace.targets:
+                        to_process += [j]
+                    elif reg_is_lib.match(j) or os.path.exists(j):
+                        libraries += [j]
+
+                for j in libraries:
+                    if j not in self.link_libraries:
+                        self.link_libraries += [j]
+
+                processed += [curr]
         elif self.type.upper() not in ['EXECUTABLE', 'OBJECT_LIBRARY']:
             mlog.warning('CMake: Target', mlog.bold(self.cmake_name), 'not found in CMake trace. This can lead to build errors')
 
diff --git a/mesonbuild/cmake/traceparser.py b/mesonbuild/cmake/traceparser.py
--- a/mesonbuild/cmake/traceparser.py
+++ b/mesonbuild/cmake/traceparser.py
@@ -89,6 +89,7 @@ def parse(self, trace: str) -> None:
             'target_compile_definitions': self._cmake_target_compile_definitions,
             'target_compile_options': self._cmake_target_compile_options,
             'target_include_directories': self._cmake_target_include_directories,
+            'target_link_libraries': self._cmake_target_link_libraries,
             'target_link_options': self._cmake_target_link_options,
             'add_dependencies': self._cmake_add_dependencies,
         }
@@ -432,6 +433,10 @@ def _cmake_target_link_options(self, tline: CMakeTraceLine) -> None:
         # DOC: https://cmake.org/cmake/help/latest/command/target_link_options.html
         self._parse_common_target_options('target_link_options', 'LINK_OPTIONS', 'INTERFACE_LINK_OPTIONS', tline)
 
+    def _cmake_target_link_libraries(self, tline: CMakeTraceLine) -> None:
+        # DOC: https://cmake.org/cmake/help/latest/command/target_link_libraries.html
+        self._parse_common_target_options('target_link_options', 'LINK_LIBRARIES', 'INTERFACE_LINK_LIBRARIES', tline)
+
     def _parse_common_target_options(self, func: str, private_prop: str, interface_prop: str, tline: CMakeTraceLine, ignore: Optional[List[str]] = None, paths: bool = False):
         if ignore is None:
             ignore = ['BEFORE']
@@ -453,14 +458,14 @@ def _parse_common_target_options(self, func: str, private_prop: str, interface_p
             if i in ignore:
                 continue
 
-            if i in ['INTERFACE', 'PUBLIC', 'PRIVATE']:
+            if i in ['INTERFACE', 'LINK_INTERFACE_LIBRARIES', 'PUBLIC', 'PRIVATE', 'LINK_PUBLIC', 'LINK_PRIVATE']:
                 mode = i
                 continue
 
-            if mode in ['INTERFACE', 'PUBLIC']:
+            if mode in ['INTERFACE', 'LINK_INTERFACE_LIBRARIES', 'PUBLIC', 'LINK_PUBLIC']:
                 interface += [i]
 
-            if mode in ['PUBLIC', 'PRIVATE']:
+            if mode in ['PUBLIC', 'PRIVATE', 'LINK_PRIVATE']:
                 private += [i]
 
         if paths:


Test patch:
diff --git a/test cases/cmake/16 threads/main.cpp b/test cases/cmake/16 threads/main.cpp
new file mode 100644
--- /dev/null
+++ b/test cases/cmake/16 threads/main.cpp	
@@ -0,0 +1,9 @@
+#include "cmMod.hpp"
+
+#include <cstdlib>
+
+int main() {
+  CmMod cc;
+  cc.asyncIncrement();
+  return cc.getNum() == 1 ? EXIT_SUCCESS : EXIT_FAILURE;
+}
diff --git a/test cases/cmake/16 threads/meson.build b/test cases/cmake/16 threads/meson.build
new file mode 100644
--- /dev/null
+++ b/test cases/cmake/16 threads/meson.build	
@@ -0,0 +1,8 @@
+project('cmMod', ['c', 'cpp'])
+
+cm       = import('cmake')
+cmMod    = cm.subproject('cmMod')
+cmModDep = cmMod.dependency('cmModLib')
+
+exe1 = executable('exe1', ['main.cpp'], dependencies: [cmModDep])
+test('exe1_OK', exe1)
diff --git a/test cases/cmake/16 threads/subprojects/cmMod/CMakeLists.txt b/test cases/cmake/16 threads/subprojects/cmMod/CMakeLists.txt
new file mode 100644
--- /dev/null
+++ b/test cases/cmake/16 threads/subprojects/cmMod/CMakeLists.txt	
@@ -0,0 +1,9 @@
+cmake_minimum_required(VERSION 3.5)
+
+project(cmMod CXX)
+set (CMAKE_CXX_STANDARD 14)
+
+find_package(Threads)
+
+add_library(cmModLib STATIC cmMod.cpp)
+target_link_libraries(cmModLib PRIVATE Threads::Threads)
diff --git a/test cases/cmake/16 threads/subprojects/cmMod/cmMod.cpp b/test cases/cmake/16 threads/subprojects/cmMod/cmMod.cpp
new file mode 100644
--- /dev/null
+++ b/test cases/cmake/16 threads/subprojects/cmMod/cmMod.cpp	
@@ -0,0 +1,15 @@
+#include "cmMod.hpp"
+
+#include <chrono>
+#include <thread>
+
+using namespace std::chrono_literals;
+
+void CmMod::asyncIncrement() {
+  std::thread t1([this]() {
+    std::this_thread::sleep_for(100ms);
+    num += 1;
+  });
+
+  t1.join();
+}
diff --git a/test cases/cmake/16 threads/subprojects/cmMod/cmMod.hpp b/test cases/cmake/16 threads/subprojects/cmMod/cmMod.hpp
new file mode 100644
--- /dev/null
+++ b/test cases/cmake/16 threads/subprojects/cmMod/cmMod.hpp	
@@ -0,0 +1,10 @@
+#pragma once
+
+class CmMod {
+private:
+  int num = 0;
+
+public:
+  inline int getNum() const { return num; }
+  void asyncIncrement();
+};
diff --git a/test cases/cmake/16 threads/subprojects/cmMod/main.cpp b/test cases/cmake/16 threads/subprojects/cmMod/main.cpp
new file mode 100644
--- /dev/null
+++ b/test cases/cmake/16 threads/subprojects/cmMod/main.cpp	
@@ -0,0 +1,9 @@
+#include "cmMod.hpp"
+
+#include <cstdlib>
+
+int main() {
+  CmMod cc;
+  cc.asyncIncrement();
+  return cc.getNum() == 1 ? EXIT_SUCCESS : EXIT_FAILURE;
+}
diff --git a/test cases/cmake/2 advanced/meson.build b/test cases/cmake/2 advanced/meson.build
--- a/test cases/cmake/2 advanced/meson.build	
+++ b/test cases/cmake/2 advanced/meson.build	
@@ -10,11 +10,14 @@ cm = import('cmake')
 # Test the "normal" subproject call
 sub_pro = cm.subproject('cmMod')
 sub_dep = sub_pro.dependency('cmModLib')
+sub_sta = sub_pro.dependency('cmModLibStatic')
 
 # Build some files
 exe1 = executable('main1', ['main.cpp'], dependencies: [sub_dep])
+exe2 = executable('main2', ['main.cpp'], dependencies: [sub_sta])
 test('test1', exe1)
+test('test2', exe2)
 
 # Test if we can also extract executables
 assert(sub_pro.target_type('testEXE') == 'executable', 'The type must be executable for obvious reasons')
-test('test2', sub_pro.target('testEXE'))
+test('test3', sub_pro.target('testEXE'))
diff --git a/test cases/cmake/2 advanced/subprojects/cmMod/CMakeLists.txt b/test cases/cmake/2 advanced/subprojects/cmMod/CMakeLists.txt
--- a/test cases/cmake/2 advanced/subprojects/cmMod/CMakeLists.txt	
+++ b/test cases/cmake/2 advanced/subprojects/cmMod/CMakeLists.txt	
@@ -10,7 +10,8 @@ include_directories(${CMAKE_CURRENT_BINARY_DIR} ${CMAKE_CURRENT_SOURCE_DIR} ${CM
 set(CONFIG_OPT 42)
 configure_file("config.h.in" "${CMAKE_CURRENT_BINARY_DIR}/config.h" @ONLY)
 
-add_library(cmModLib SHARED lib/cmMod.cpp)
+add_library(cmModLib       SHARED lib/cmMod.cpp)
+add_library(cmModLibStatic STATIC lib/cmMod.cpp)
 include(GenerateExportHeader)
 generate_export_header(cmModLib)
 
@@ -18,7 +19,10 @@ set_target_properties(cmModLib PROPERTIES VERSION 1.0.1)
 
 add_executable(testEXE main.cpp)
 
-target_link_libraries(cmModLib ZLIB::ZLIB)
+target_link_libraries(cmModLib       ZLIB::ZLIB)
+target_link_libraries(cmModLibStatic ZLIB::ZLIB)
 target_link_libraries(testEXE cmModLib)
 
+target_compile_definitions(cmModLibStatic PUBLIC CMMODLIB_STATIC_DEFINE)
+
 install(TARGETS cmModLib testEXE LIBRARY DESTINATION lib RUNTIME DESTINATION bin)
diff --git a/test cases/cmake/3 advanced no dep/meson.build b/test cases/cmake/3 advanced no dep/meson.build
--- a/test cases/cmake/3 advanced no dep/meson.build	
+++ b/test cases/cmake/3 advanced no dep/meson.build	
@@ -5,11 +5,14 @@ cm = import('cmake')
 # Test the "normal" subproject call
 sub_pro = cm.subproject('cmMod')
 sub_dep = sub_pro.dependency('cmModLib')
+sub_sta = sub_pro.dependency('cmModLibStatic')
 
 # Build some files
 exe1 = executable('main1', ['main.cpp'], dependencies: [sub_dep])
+exe2 = executable('main2', ['main.cpp'], dependencies: [sub_sta])
 test('test1', exe1)
+test('test2', exe2)
 
 # Test if we can also extract executables
 assert(sub_pro.target_type('testEXE') == 'executable', 'The type must be executable for obvious reasons')
-test('test2', sub_pro.target('testEXE'))
+test('test3', sub_pro.target('testEXE'))
diff --git a/test cases/cmake/3 advanced no dep/subprojects/cmMod/CMakeLists.txt b/test cases/cmake/3 advanced no dep/subprojects/cmMod/CMakeLists.txt
--- a/test cases/cmake/3 advanced no dep/subprojects/cmMod/CMakeLists.txt	
+++ b/test cases/cmake/3 advanced no dep/subprojects/cmMod/CMakeLists.txt	
@@ -8,7 +8,8 @@ include_directories(${CMAKE_CURRENT_BINARY_DIR} ${CMAKE_CURRENT_SOURCE_DIR} ${CM
 set(CONFIG_OPT 42)
 configure_file("config.h.in" "${CMAKE_CURRENT_BINARY_DIR}/config.h" @ONLY)
 
-add_library(cmModLib SHARED lib/cmMod.cpp)
+add_library(cmModLib       SHARED lib/cmMod.cpp)
+add_library(cmModLibStatic STATIC lib/cmMod.cpp)
 include(GenerateExportHeader)
 generate_export_header(cmModLib)
 
@@ -18,4 +19,6 @@ add_executable(testEXE main.cpp)
 
 target_link_libraries(testEXE cmModLib)
 
+target_compile_definitions(cmModLibStatic PUBLIC CMMODLIB_STATIC_DEFINE)
+
 install(TARGETS cmModLib testEXE LIBRARY DESTINATION lib RUNTIME DESTINATION bin)