Repository: mesonbuild/meson

Issue:
Broken linking on GCC + macOS if a framework is detected through CMake
**Describe the bug**

Officially, Meson does not support looking up frameworks through Homebrewed GCC. However, Meson can still "find" them through CMake's internal modules.

When such a module results in the dependency being found, Meson will import and use the `.framework`'s full path as the `dependency`'s library path, resulting in a failure at link time that looks like this:

```
❯ meson compile -C build2 
INFO: autodetecting backend as ninja
INFO: calculating backend command to run: /usr/local/bin/ninja -C /Volumes/Centricular/test-cmake-openal/build2
ninja: Entering directory `/Volumes/Centricular/test-cmake-openal/build2'
[2/2] Linking target tt
FAILED: tt 
g++-13  -o tt tt.p/main.cpp.o -Wl,-dead_strip_dylibs -Wl,-headerpad_max_install_names -Wl,-undefined,error /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX12.1.sdk/System/Library/Frameworks/OpenAL.framework
ld: can't map file, errno=22 file '/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX12.1.sdk/System/Library/Frameworks/OpenAL.framework' for architecture x86_64
collect2: error: ld returned 1 exit status
ninja: build stopped: subcommand failed.
```

**To Reproduce**

The following project includes equivalent CMake and Meson build systems: https://github.com/amyspark/meson-bug-cmake-gcc-framework

**Expected behavior**

Meson should either completely prohibit framework lookup on Apple platforms when AppleClang is not used, or at least validate if what's found is a framework or not.

I've not been able to parse how CMake performs the latter at buildsystem generation time, so I couldn't put a PR together with this.

**system parameters**
* Is this a [cross build](https://mesonbuild.com/Cross-compilation.html) or just a plain native build (for the same computer)? Native
* what operating system (e.g. MacOS Catalina, Windows 10, CentOS 8.0, Ubuntu 18.04, etc.) MacOS 11.7.9
* what Python version are you using e.g. 3.8.0 3.11.5
* what `meson --version` 1.2.1
* what `ninja --version` if it's a Ninja build 1.11.1



Discussion:

Resolving patch:
diff --git a/mesonbuild/cmake/tracetargets.py b/mesonbuild/cmake/tracetargets.py
--- a/mesonbuild/cmake/tracetargets.py
+++ b/mesonbuild/cmake/tracetargets.py
@@ -4,6 +4,7 @@
 
 from .common import cmake_is_debug
 from .. import mlog
+from ..mesonlib import Version
 
 from pathlib import Path
 import re
@@ -15,6 +16,28 @@
     from ..compilers import Compiler
     from ..dependencies import MissingCompiler
 
+# Small duplication of ExtraFramework to parse full
+# framework paths as exposed by CMake
+def _get_framework_latest_version(path: Path) -> str:
+    versions: list[Version] = []
+    for each in path.glob('Versions/*'):
+        # macOS filesystems are usually case-insensitive
+        if each.name.lower() == 'current':
+            continue
+        versions.append(Version(each.name))
+    if len(versions) == 0:
+        # most system frameworks do not have a 'Versions' directory
+        return 'Headers'
+    return 'Versions/{}/Headers'.format(sorted(versions)[-1]._s)
+
+def _get_framework_include_path(path: Path) -> T.Optional[str]:
+    trials = ('Headers', 'Versions/Current/Headers', _get_framework_latest_version(path))
+    for each in trials:
+        trial = path / each
+        if trial.is_dir():
+            return trial.as_posix()
+    return None
+
 class ResolvedTarget:
     def __init__(self) -> None:
         self.include_directories: T.List[str] = []
@@ -46,10 +69,25 @@ def resolve_cmake_trace_targets(target_name: str,
             continue
 
         if curr not in trace.targets:
+            curr_path = Path(curr)
             if reg_is_lib.match(curr):
                 res.libraries += [curr]
-            elif Path(curr).is_absolute() and Path(curr).exists():
-                res.libraries += [curr]
+            elif curr_path.is_absolute() and curr_path.exists():
+                if any(x.endswith('.framework') for x in curr_path.parts):
+                    # Frameworks detected by CMake are passed as absolute paths
+                    # Split into -F/path/to/ and -framework name
+                    path_to_framework = []
+                    # Try to slice off the `Versions/X/name.tbd`
+                    for x in curr_path.parts:
+                        path_to_framework.append(x)
+                        if x.endswith('.framework'):
+                            break
+                    curr_path = Path(*path_to_framework)
+                    framework_path = curr_path.parent
+                    framework_name = curr_path.stem
+                    res.libraries += [f'-F{framework_path}', '-framework', framework_name]
+                else:
+                    res.libraries += [curr]
             elif reg_is_maybe_bare_lib.match(curr) and clib_compiler:
                 # CMake library dependencies can be passed as bare library names,
                 # CMake brute-forces a combination of prefix/suffix combinations to find the
@@ -115,9 +153,9 @@ def resolve_cmake_trace_targets(target_name: str,
 
         processed_targets += [curr]
 
-    res.include_directories = sorted(set(res.include_directories))
-    res.link_flags = sorted(set(res.link_flags))
-    res.public_compile_opts = sorted(set(res.public_compile_opts))
-    res.libraries = sorted(set(res.libraries))
+    # Do not sort flags here -- this breaks
+    # semantics of eg. `-framework CoreAudio`
+    # or `-Lpath/to/root -llibrary`
+    # see eg. #11113
 
     return res
diff --git a/mesonbuild/dependencies/framework.py b/mesonbuild/dependencies/framework.py
--- a/mesonbuild/dependencies/framework.py
+++ b/mesonbuild/dependencies/framework.py
@@ -79,7 +79,7 @@ def _get_framework_path(self, path: str, name: str) -> T.Optional[Path]:
         return None
 
     def _get_framework_latest_version(self, path: Path) -> str:
-        versions = []
+        versions: T.List[Version] = []
         for each in path.glob('Versions/*'):
             # macOS filesystems are usually case-insensitive
             if each.name.lower() == 'current':


Test patch:
diff --git a/test cases/osx/9 framework recasting/main.cpp b/test cases/osx/9 framework recasting/main.cpp
new file mode 100644
--- /dev/null
+++ b/test cases/osx/9 framework recasting/main.cpp	
@@ -0,0 +1 @@
+int main() { return 0; }
diff --git a/test cases/osx/9 framework recasting/meson.build b/test cases/osx/9 framework recasting/meson.build
new file mode 100644
--- /dev/null
+++ b/test cases/osx/9 framework recasting/meson.build	
@@ -0,0 +1,5 @@
+project('framework recasting', 'c', 'cpp')
+
+x = dependency('openal')
+
+y = executable('tt', files('main.cpp'), dependencies: x)