Repository: mesonbuild/meson

Issue:
CMake detecting dependency assumes PACKAGE_LIBRARIES are libraries for one configuration
**Describe the bug**
[/mesonbuild/dependencies/cmake.py:451](https://github.com/mesonbuild/meson/blob/0.59.1/mesonbuild/dependencies/cmake.py#L451) assumes that PACKAGE_LIBRARIES are for only one configuration, but [FindOpenSSL.cmake](https://github.com/Kitware/CMake/blob/master/Modules/FindOpenSSL.cmake#L359-L360) for example uses [`select_library_configurations()`](https://cmake.org/cmake/help/latest/module/SelectLibraryConfigurations.html) which creates a list with both debug and release libraries that requires further parsing.

**To Reproduce**
```meson.build
project('hello-world', 'c')
openssl_dep = dependency('openssl')
executable('hello-world', 'main.c', dependencies: openssl_dep)
```
```cmd
meson setup --buildtype=debug build
meson.py compile -C build
```

**Expected behaviour**
The linker will attempt to link with only the debug OpenSSL libraries. 
AKA not both with erroneous keywords like "optimized".
```
"optimized" "C:/Program Files/OpenSSL-Win64/lib/VC/libssl64MD.lib" "debug" "C:/Program Files/OpenSSL-Win64/lib/VC/libssl64MDd.lib" "optimized" "C:/Program Files/OpenSSL-Win64/lib/VC/libcrypto64MD.lib" "debug" "C:/Program Files/OpenSSL-Win64/lib/VC/libcrypto64MDd.lib"
```

**system parameters**
* Is this a [cross build](https://mesonbuild.com/Cross-compilation.html) or just a plain native build (for the same computer)? **plain native**
* what operating system (e.g. MacOS Catalina, Windows 10, CentOS 8.0, Ubuntu 18.04, etc.) **Windows 10**
* what Python version are you using e.g. 3.8.0 **3.9.2**
* what `meson --version` **0.59.1**
* what `ninja --version` if it's a Ninja build **1.10.0.git.kitware.jobserver-1**
* what `cmake --version` **3.20.21032501-MSVC_2**
* what `cl` version **Microsoft (R) C/C++ Optimizing Compiler Version 19.29.30133 for x64**

This seems to be the cause of https://gitlab.freedesktop.org/libnice/libnice/-/issues/140



Discussion:
The best workaround, for now, should be to explicitly let meson know what [imported CMake target](https://github.com/Kitware/CMake/blob/master/Modules/FindOpenSSL.cmake#L594) to use with the [modules kwarg](https://mesonbuild.com/Dependencies.html#cmake).

So, `dependency('OpenSSL', modules: ['OpenSSL::SSL'])` should do the trick here.
Should be fixed with #9221.

Resolving patch:
diff --git a/ci/ciimage/build.py b/ci/ciimage/build.py
--- a/ci/ciimage/build.py
+++ b/ci/ciimage/build.py
@@ -184,10 +184,36 @@ def do_test(self, tty: bool = False) -> None:
             cleanup_cmd = [self.docker, 'rmi', '-f', 'meson_test_image']
             subprocess.run(cleanup_cmd).returncode
 
+class ImageTTY(BuilderBase):
+    def __init__(self, data_dir: Path, temp_dir: Path, ci_root: Path) -> None:
+        super().__init__(data_dir, temp_dir)
+        self.meson_root = ci_root.parent.parent.resolve()
+
+    def do_run(self) -> None:
+        try:
+            tty_cmd = [
+                self.docker, 'run',
+                '--name', 'meson_test_container', '-t', '-i', '-v', f'{self.meson_root.as_posix()}:/meson',
+                f'{image_namespace}/{self.data_dir.name}',
+                '/bin/bash', '-c', ''
+                    + 'cd meson;'
+                    + 'source /ci/env_vars.sh;'
+                    + f'echo -e "\\n\\nInteractive test shell in the {image_namespace}/{self.data_dir.name} container with the current meson tree";'
+                    + 'echo -e "The file ci/ciimage/user.sh will be sourced if it exists to enable user specific configurations";'
+                    + 'echo -e "Run the following command to run all CI tests: ./run_tests.py $CI_ARGS\\n\\n";'
+                    + '[ -f ci/ciimage/user.sh ] && exec /bin/bash --init-file ci/ciimage/user.sh;'
+                    + 'exec /bin/bash;'
+            ]
+            subprocess.run(tty_cmd).returncode != 0
+        finally:
+            cleanup_cmd = [self.docker, 'rm', '-f', 'meson_test_container']
+            subprocess.run(cleanup_cmd).returncode
+
+
 def main() -> None:
     parser = argparse.ArgumentParser(description='Meson CI image builder')
     parser.add_argument('what', type=str, help='Which image to build / test')
-    parser.add_argument('-t', '--type', choices=['build', 'test', 'testTTY'], help='What to do', required=True)
+    parser.add_argument('-t', '--type', choices=['build', 'test', 'testTTY', 'TTY'], help='What to do', required=True)
 
     args = parser.parse_args()
 
@@ -207,6 +233,9 @@ def main() -> None:
         elif args.type == 'testTTY':
             tester = ImageTester(ci_data, ci_build, ci_root)
             tester.do_test(tty=True)
+        elif args.type == 'TTY':
+            tester = ImageTTY(ci_data, ci_build, ci_root)
+            tester.do_run()
 
 if __name__ == '__main__':
     main()
diff --git a/mesonbuild/dependencies/cmake.py b/mesonbuild/dependencies/cmake.py
--- a/mesonbuild/dependencies/cmake.py
+++ b/mesonbuild/dependencies/cmake.py
@@ -15,7 +15,7 @@
 from .base import ExternalDependency, DependencyException, DependencyTypeName
 from ..mesonlib import is_windows, MesonException, OptionKey, PerMachine, stringlistify, extract_as_list
 from ..mesondata import mesondata
-from ..cmake import CMakeExecutor, CMakeTraceParser, CMakeException, CMakeToolchain, CMakeExecScope, check_cmake_args
+from ..cmake import CMakeExecutor, CMakeTraceParser, CMakeException, CMakeToolchain, CMakeExecScope, check_cmake_args, CMakeTarget
 from .. import mlog
 from pathlib import Path
 import functools
@@ -433,6 +433,18 @@ def _detect_dep(self, name: str, package_version: str, modules: T.List[T.Tuple[s
         modules = self._map_module_list(modules, components)
         autodetected_module_list = False
 
+        # Check if we need a DEBUG or RELEASE CMake dependencies
+        is_debug = False
+        if OptionKey('b_vscrt') in self.env.coredata.options:
+            is_debug = self.env.coredata.get_option(OptionKey('buildtype')) == 'debug'
+            if self.env.coredata.options[OptionKey('b_vscrt')].value in {'mdd', 'mtd'}:
+                is_debug = True
+        else:
+            # Don't directly assign to is_debug to make mypy happy
+            debug_opt = self.env.coredata.get_option(OptionKey('debug'))
+            assert isinstance(debug_opt, bool)
+            is_debug = debug_opt
+
         # Try guessing a CMake target if none is provided
         if len(modules) == 0:
             for i in self.traceparser.targets:
@@ -446,9 +458,59 @@ def _detect_dep(self, name: str, package_version: str, modules: T.List[T.Tuple[s
 
         # Failed to guess a target --> try the old-style method
         if len(modules) == 0:
+            # Warn when there might be matching imported targets but no automatic match was used
+            partial_modules: T.List[CMakeTarget] = []
+            for k, v in self.traceparser.targets.items():
+                tg = k.lower()
+                lname = name.lower()
+                if tg.startswith(f'{lname}::'):
+                    partial_modules += [v]
+            if partial_modules:
+                mlog.warning(textwrap.dedent(f'''\
+                    Could not find and exact match for the CMake dependency {name}.
+
+                    However, Meson found the following partial matches:
+
+                        {[x.name for x in partial_modules]}
+
+                    Using imported is recommended, since this approach is less error prone
+                    and better supported by Meson. Consider explicitly specifying one of
+                    these in the dependency call with:
+
+                        dependency('{name}', modules: ['{name}::<name>', ...])
+
+                    Meson will now continue to use the old-style {name}_LIBRARIES CMake
+                    variables to extract the dependency information since no explicit
+                    target is currently specified.
+
+                '''))
+                mlog.debug('More info for the partial match targets:')
+                for tgt in partial_modules:
+                    mlog.debug(tgt)
+
+
             incDirs = [x for x in self.traceparser.get_cmake_var('PACKAGE_INCLUDE_DIRS') if x]
             defs = [x for x in self.traceparser.get_cmake_var('PACKAGE_DEFINITIONS') if x]
-            libs = [x for x in self.traceparser.get_cmake_var('PACKAGE_LIBRARIES') if x]
+            libs_raw = [x for x in self.traceparser.get_cmake_var('PACKAGE_LIBRARIES') if x]
+
+            # CMake has a "fun" API, where certain keywords describing
+            # configurations can be in the *_LIBRARIES vraiables. See:
+            # - https://github.com/mesonbuild/meson/issues/9197
+            # - https://gitlab.freedesktop.org/libnice/libnice/-/issues/140
+            # - https://cmake.org/cmake/help/latest/command/target_link_libraries.html#overview  (the last point in the section)
+            libs: T.List[str] = []
+            cfg_matches = True
+            cm_tag_map = {'debug': is_debug, 'optimized': not is_debug, 'general': True}
+            for i in libs_raw:
+                if i.lower() in cm_tag_map:
+                    cfg_matches = cm_tag_map[i.lower()]
+                    continue
+                if cfg_matches:
+                    libs += [i]
+                # According to the CMake docs, a keyword only works for the
+                # directly the following item and all items without a keyword
+                # are implizitly `general`
+                cfg_matches = True
 
             # Try to use old style variables if no module is specified
             if len(libs) > 0:
@@ -514,15 +576,6 @@ def _detect_dep(self, name: str, package_version: str, modules: T.List[T.Tuple[s
                     cfgs = [x for x in tgt.properties['IMPORTED_CONFIGURATIONS'] if x]
                     cfg = cfgs[0]
 
-                if OptionKey('b_vscrt') in self.env.coredata.options:
-                    is_debug = self.env.coredata.get_option(OptionKey('buildtype')) == 'debug'
-                    if self.env.coredata.options[OptionKey('b_vscrt')].value in {'mdd', 'mtd'}:
-                        is_debug = True
-                else:
-                    # Don't directly assign to is_debug to make mypy happy
-                    debug_opt = self.env.coredata.get_option(OptionKey('debug'))
-                    assert isinstance(debug_opt, bool)
-                    is_debug = debug_opt
                 if is_debug:
                     if 'DEBUG' in cfgs:
                         cfg = 'DEBUG'


Test patch:
diff --git a/test cases/linuxlike/13 cmake dependency/cmake_fake1/cmMesonTestF1Config.cmake b/test cases/linuxlike/13 cmake dependency/cmake_fake1/cmMesonTestF1Config.cmake
--- a/test cases/linuxlike/13 cmake dependency/cmake_fake1/cmMesonTestF1Config.cmake	
+++ b/test cases/linuxlike/13 cmake dependency/cmake_fake1/cmMesonTestF1Config.cmake	
@@ -2,8 +2,10 @@ find_package(ZLIB)
 
 if(ZLIB_FOUND OR ZLIB_Found)
   set(cmMesonTestF1_FOUND        ON)
-  set(cmMesonTestF1_LIBRARIES    ${ZLIB_LIBRARY})
+  set(cmMesonTestF1_LIBRARIES    general ${ZLIB_LIBRARY})
   set(cmMesonTestF1_INCLUDE_DIRS ${ZLIB_INCLUDE_DIR})
+
+  add_library(CMMesonTESTf1::evil_non_standard_trget UNKNOWN IMPORTED)
 else()
   set(cmMesonTestF1_FOUND       OFF)
 endif()
diff --git a/test cases/linuxlike/13 cmake dependency/meson.build b/test cases/linuxlike/13 cmake dependency/meson.build
--- a/test cases/linuxlike/13 cmake dependency/meson.build	
+++ b/test cases/linuxlike/13 cmake dependency/meson.build	
@@ -48,6 +48,10 @@ depPrefEnv1 = dependency('cmMesonTestF1', required : true, method : 'cmake')
 depPrefEnv2 = dependency('cmMesonTestF2', required : true, method : 'cmake')
 depPrefEnv3 = dependency('cmMesonTestF3', required : true, method : 'cmake')
 
+# Try to actually link with depPrefEnv1, since we are doing "fun" stuff there
+exe3 = executable('zlibprog3', 'prog.c', dependencies : depPrefEnv1)
+test('zlibtest3', exe3)
+
 # Try to find a dependency with a custom CMake module
 
 depm1 = dependency('SomethingLikeZLIB', required : true, components : 'required_comp',   method : 'cmake', cmake_module_path : 'cmake')
diff --git a/test cases/linuxlike/13 cmake dependency/test.json b/test cases/linuxlike/13 cmake dependency/test.json
--- a/test cases/linuxlike/13 cmake dependency/test.json	
+++ b/test cases/linuxlike/13 cmake dependency/test.json	
@@ -2,5 +2,13 @@
   "env": {
     "CMAKE_PREFIX_PATH": "@ROOT@/cmake_fake1;@ROOT@/cmake_fake2:@ROOT@/cmake_pref_env",
     "PATH": "@ROOT@/cmake_fake3/bin:@PATH@"
-  }
+  },
+  "stdout": [
+    {
+      "line": "WARNING: Could not find and exact match for the CMake dependency cmMesonTestF1."
+    },
+    {
+      "line": "    ['CMMesonTESTf1::evil_non_standard_trget']"
+    }
+  ]
 }