Skip to content

Add MRBIND_LINK_CLANG_CPP_DYLIB opt-in for smaller mrbind on macOS#35

Open
Fedr wants to merge 4 commits into
masterfrom
ci/macos-link-clang-cpp-dylib
Open

Add MRBIND_LINK_CLANG_CPP_DYLIB opt-in for smaller mrbind on macOS#35
Fedr wants to merge 4 commits into
masterfrom
ci/macos-link-clang-cpp-dylib

Conversation

@Fedr
Copy link
Copy Markdown

@Fedr Fedr commented May 14, 2026

Why

mrbind's parser binary is currently ~30 MiB stripped on macOS / Ubuntu / Windows-MSYS2 because it statically links the granular clangTooling / friends. On Rocky Linux it's ~5 MiB stripped because the local LLVM cmake config doesn't expose clangTooling and we fall through to the clang-cpp + LLVM (dynamic) branch.

For consumers caching thirdparty/mrbind/build across CI runs (e.g. MeshLib/build-mrbind), the per-platform cache footprint is ~12 MiB compressed where the static path is used vs ~2 MiB on Rocky. Across the matrix that's the difference between ~90 MiB and ~20 MiB of cache.

What

Add a new CMake option, MRBIND_LINK_CLANG_CPP_DYLIB, off by default. When ON, link mrbind against libclang-cpp + libLLVM (single shared lib resolved at runtime) instead of clangTooling. The fallback for platforms where clangTooling isn't exposed (Arch, Rocky) is unchanged — they continue to take the dynamic path implicitly.

The OFF branch keeps the old clangTooling / clang-cpp + LLVM + LLVMSupport logic untouched. The ON branch is written separately and uses find_library to resolve to absolute library paths rather than linking through the imported targets clang-cpp / LLVM / LLVMSupport — see "find_library vs imported targets" below.

option(MRBIND_LINK_CLANG_CPP_DYLIB
    "Link mrbind against libclang-cpp + libLLVM dylibs instead of clangTooling. \
Smaller binary, but the dylibs must be present at the same paths at runtime."
    OFF)
if (MRBIND_LINK_CLANG_CPP_DYLIB)
    find_library(MRBIND_CLANG_CPP_LIB NAMES clang-cpp HINTS ${LLVM_LIBRARY_DIRS} REQUIRED)
    target_link_libraries(mrbind PRIVATE ${MRBIND_CLANG_CPP_LIB})

    find_library(MRBIND_LLVM_LIB NAMES LLVM HINTS ${LLVM_LIBRARY_DIRS})
    if (MRBIND_LLVM_LIB)
        target_link_libraries(mrbind PRIVATE ${MRBIND_LLVM_LIB})
    endif()

    find_library(MRBIND_LLVM_SUPPORT_LIB NAMES LLVMSupport HINTS ${LLVM_LIBRARY_DIRS})
    if (MRBIND_LLVM_SUPPORT_LIB)
        target_link_libraries(mrbind PRIVATE ${MRBIND_LLVM_SUPPORT_LIB})
    endif()
elseif (TARGET clangTooling)
    target_link_libraries(mrbind PRIVATE clangTooling)
else()
    # Arch / Rocky fallback (LLVM only needed on Rocky)
    target_link_libraries(mrbind PRIVATE clang-cpp LLVM LLVMSupport)
endif()

find_library vs imported targets

The naive shape target_link_libraries(mrbind PRIVATE clang-cpp LLVM LLVMSupport) linked through Clang's imported targets and propagated their INTERFACE_INCLUDE_DIRECTORIES into mrbind's compile commands. On Homebrew's llvm@N that includes /opt/homebrew/opt/llvm@N/include, which gets inserted before libc++'s wrapper headers and breaks the libc++ <cstddef> include chain (<cstddef> ends up pulling LLVM-bundled <stddef.h> instead of the libc++ wrapper).

find_library returns an absolute path, so passing that to target_link_libraries adds the lib to the link line without any usage-requirement propagation. mrbind already explicitly adds the include paths it needs above via LLVM_INCLUDE_DIR / CLANG_INCLUDE_DIR.

Why off by default

The existing comment in the file documents that on apt-clang/Ubuntu, the dynamic path crashed at startup:

LLVM ERROR: inconsistency in registered CommandLine options

This is duplicate cl::opt registrations between libclang-cpp.so and the granular libs that still slip into the link line on Debian-family LLVM packaging. Default-off preserves the current safe behaviour for everyone.

When to turn it on

Consumers who know their LLVM was built with LLVM_LINK_LLVM_DYLIB=ON (Homebrew's llvm@N on macOS is a confirmed case) can pass -DMRBIND_LINK_CLANG_CPP_DYLIB=ON to their cmake invocation. The resulting binary depends on libclang-cpp.dylib / libLLVM.dylib being resolvable at the same path at runtime, which is usually fine for CI but worth being explicit about.

Test plan

  • mrbind's own CI passes — default-off path is unchanged, so this is a no-op for everyone not opting in.
  • Downstream verified on MeshLibMeshInspector/MeshLib#6101 passes this flag from install_mrbind_macos.sh and goes green on all three macOS jobs (x64 Release, arm64 Release, arm64 Debug). Build MRBind, Generate C bindings, and Generate and build Python bindings all succeed; no inconsistency in registered CommandLine options and no libc++ <cstddef> failures.
  • Cache footprint drops as predicted — all three macOS runners (two GitHub-hosted, one self-hosted) drop from ~12–13 MiB to ~2 MiB compressed, matching Rocky's footprint.

Adds a new CMake option, off by default. When ON, mrbind links against
`libclang-cpp.dylib` + `libLLVM.dylib` instead of pulling clang/LLVM in
statically via `clangTooling`. The resulting binary is much smaller --
single shared lib resolved at runtime vs. the entire frontend in the
exe -- which is useful for CI caches.

Not enabled by default because the existing comment documents that on
apt-clang/Ubuntu the dynamic path crashes at startup with
`LLVM ERROR: inconsistency in registered CommandLine options`
(duplicate `cl::opt` registrations between libclang-cpp.so and the
granular libs that still slip into the link line).

Homebrew's `llvm@N` on macOS is built with `LLVM_LINK_LLVM_DYLIB=ON`
and doesn't have that problem, so consumers building on macOS can pass
`-DMRBIND_LINK_CLANG_CPP_DYLIB=ON` to opt in.

The fallback for platforms where `clangTooling` isn't exposed (Arch,
Rocky) is unchanged -- they continue to take the dynamic path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Fedr and others added 3 commits May 14, 2026 21:09
…'t leak

Linking `mrbind` against `clang-cpp` on macOS (Homebrew llvm@N) broke
`<cstddef>` at compile time:

  /opt/homebrew/opt/llvm@N/.../c++/v1/cstddef:46: error: <cstddef> tried
  including <stddef.h> but didn't find libc++'s <stddef.h> header.

Cause: the `clang-cpp` CMake target's `INTERFACE_INCLUDE_DIRECTORIES`
contains `/opt/homebrew/opt/llvm@N/include`, and `target_link_libraries`
propagates that as a `-I` to every `mrbind` source file. That dir
shadows libc++'s wrapper for `<stddef.h>` and the include chain breaks.

Wrap the dynamic-link targets in `$<LINK_ONLY:...>` so they appear on
the link line but their interface properties (include dirs, compile
defs, etc.) are not propagated to `mrbind`. The explicit
`target_include_directories(mrbind PRIVATE ${LLVM_INCLUDE_DIR}
${CLANG_INCLUDE_DIR})` above already provides the include paths the
parser actually needs, so dropping the propagation is safe.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…agation

The previous attempt wrapped `clang-cpp` / `LLVM` / `LLVMSupport` in
`$<LINK_ONLY:...>` hoping that would strip their usage requirements,
but CI still failed at libc++ `<cstddef>`: the imported targets'
INTERFACE properties (and/or some equivalent compile-time hook) are
still leaking into mrbind's compile commands somehow.

Sidestep the whole machinery: `find_library` the libs by name and link
to the absolute paths. CMake adds raw-path link libraries to the link
line directly, with no INTERFACE_* propagation, no usage requirements.
mrbind already explicitly adds the include paths it needs via
`target_include_directories`, so dropping any reliance on the imported
targets here is safe.

`LLVM` and `LLVMSupport` are now both optional (the comment above
already documented "`LLVM` part is only needed on Rocky"). They're
linked only when found, matching the existing platform-specific shape.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Restructure so the OFF branch (the default) preserves the original
`if (TARGET clangTooling) ... else() clang-cpp LLVM LLVMSupport`
shape verbatim, including its original Ubuntu-CommandLine-error
comment. The new find_library-based dynamic path is now reachable
only via the explicit `MRBIND_LINK_CLANG_CPP_DYLIB=ON` opt-in.

No behaviour change for existing consumers (no change when the option
is OFF, no change to Arch/Rocky who hit the fall-through implicitly).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant