From 2b91e41fc4b7f60fdb1a6c4d315acb4d6a34094a Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Wed, 3 Jun 2026 15:29:59 +1000 Subject: [PATCH] bonsaiviewer: stage all IfcOpenShell dylibs (core + plug-ins) on macOS MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The .app bundle's Frameworks/ staging rule was only globbing ifcopenshell.*.dylib (the dlopen-only plug-ins) on the assumption that macdeployqt would follow BonsaiViewer's link-time @rpath deps for the lib-prefixed core shared libs. In practice it doesn't — non-Qt @rpath deps whose source path is outside the standard system / Qt prefixes get skipped silently. In a static build this didn't matter: libifcopenshell.geometry, libIfcParse, libIfcViewer, etc. were statically embedded in BonsaiViewer.exe, so there was no runtime dep. With --shared (added in ddee88bed for the bundle-size win) they're separate dylibs that must physically live in Frameworks/, or the binary won't even start: dyld: Library not loaded: @rpath/libifcopenshell.geometry.dylib Reason: tried '…/BonsaiViewer.app/Contents/Frameworks/ libifcopenshell.geometry.dylib' (no such file) Broaden the install(CODE) glob to *.dylib so both flavours land: * lib-prefixed linked core libs (libifcopenshell.geometry.dylib, libIfcParse.dylib, libIfcViewer.dylib, lib.dylib) * non-prefixed plug-ins (ifcopenshell.parse.schema.ifcXxX.dylib, ifcopenshell.geometry.mapping.ifcXxX.dylib, etc.) /lib/ is IfcOpenShell-exclusive (Qt / boost / eigen live in their own brew / build prefixes), so the broad glob doesn't risk sweeping in unrelated dylibs. The geometry-writer EXCLUDE regex is preserved so the size win from the writer-skip side of ddee88bed stays in place. In a static build the lib/ directory simply has no *.dylib files that match, so the rule no-ops cleanly — same code path is safe for both --shared and the (unused but possible) default static config. Linux didn't need a counterpart: build_rocky.yml already does the equivalent in workflow bash (`patchelf --set-rpath '$ORIGIN'` + `stage_runtime_payload`), and local Linux dev uses CMake's BUILD_RPATH which auto-resolves to the build subdirectories. macOS .app bundles are treated as opaque by the packaging step (no stage_runtime_payload against Contents/), so the staging has to happen at CMake install time when the bundle is being assembled. Co-Authored-By: Claude Opus 4.7 --- src/bonsaiviewer/CMakeLists.txt | 65 +++++++++++++++++++-------------- 1 file changed, 37 insertions(+), 28 deletions(-) diff --git a/src/bonsaiviewer/CMakeLists.txt b/src/bonsaiviewer/CMakeLists.txt index 0dac4dd59d..401b6455f5 100644 --- a/src/bonsaiviewer/CMakeLists.txt +++ b/src/bonsaiviewer/CMakeLists.txt @@ -156,21 +156,37 @@ install(TARGETS BonsaiViewer ) ifcopenshell_deploy_qt_runtime(BonsaiViewer) -# Stage IfcOpenShell plug-ins (schemas, kernels, mappings, serializers — -# anything named ifcopenshell.*.dylib) into the .app bundle's -# Frameworks/ directory on macOS. These are dlopen-only deps so -# macdeployqt does not follow them automatically. The upstream -# install(TARGETS …) rules place them at /lib/ (outside the -# bundle). +# Stage IfcOpenShell dylibs into the .app bundle's Frameworks/ directory. # -# Frameworks/ is the standard macOS app-bundle location for shared -# libraries the app pulls in at runtime — it's where macdeployqt -# already deposited libIfcParse/libIfcGeom/etc. as linked deps, and -# the BonsaiViewer exe's INSTALL_RPATH (@executable_path/../Frameworks) -# points there. With the plug-in loader's primary search path being -# dirname(libIfcParse) (= Frameworks/ inside the bundle), placing the -# plug-ins alongside libIfcParse means they're found on the first -# probe — no fallback search needed. +# Two flavours sit alongside each other in /lib/ after install: +# +# 1. Linked core libs (lib*.dylib) — IfcParse, IfcGeom (output-named +# libifcopenshell.geometry.dylib), IfcViewer, plug-in / mapping / +# kernel shared libs. With --shared these are runtime @rpath deps +# of BonsaiViewer.exe. macdeployqt is *supposed* to follow them +# but in practice misses non-Qt @rpath deps when the source lib +# lives outside the standard system / Qt prefixes, so we stage +# them explicitly. (In a static build these are absent from lib/ +# and the glob just no-ops, so this rule is safe in both modes.) +# +# 2. Plug-ins (ifcopenshell.*.dylib, no `lib` prefix) — dlopen-only +# deps the plug-in loader resolves at runtime. macdeployqt has +# no way to know about these. +# +# Both kinds get a flat copy into Contents/Frameworks/. The plug-in +# loader's primary search path is dirname(libIfcParse) (= Frameworks/ +# inside the bundle), so plug-ins and core libs both find each other +# on the first probe. +# +# The geometry-writer filter drops ifcopenshell.geometry.writer.*.dylib +# (the per-schema OBJ / glTF / DAE / STP / IGS / SVG / TTL export +# converters — heavy, viewer-irrelevant). Mirrors the Rocky workflow's +# filter in `stage_runtime_payload` (see 27249770e). +# +# /lib/ is IfcOpenShell-exclusive — Qt / boost / eigen live in +# their own brew / build prefixes — so a broad *.dylib glob is safe +# here and automatically picks up any future shared libs without +# needing to maintain an explicit name list. # # Subdirectory order in cmake/CMakeLists.txt guarantees that ifcparse/ # / ifcgeom/ / serializers/ are add_subdirectory'd before bonsaiviewer/, @@ -178,21 +194,14 @@ ifcopenshell_deploy_qt_runtime(BonsaiViewer) # on disk under /lib/. if(APPLE) install(CODE [[ - file(GLOB _ifc_plugins - "${CMAKE_INSTALL_PREFIX}/lib/ifcopenshell.*.dylib") - # Skip ifcopenshell.geometry.writer.*.dylib — those are heavy - # schema-specific OBJ / glTF / DAE / STP / IGS / SVG / TTL export - # converters. BonsaiViewer is a viewer; it never writes geometry - # out, so they're pure deadweight. Mirrors the Rocky workflow's - # filter in `stage_runtime_payload` (see 27249770e). Cuts the - # macOS .app by ~100-150 MB. - list(FILTER _ifc_plugins EXCLUDE REGEX "ifcopenshell\\.geometry\\.writer\\.") - if(_ifc_plugins) - message(STATUS "Staging IfcOpenShell plug-ins into BonsaiViewer.app/Contents/Frameworks") - file(COPY ${_ifc_plugins} - DESTINATION "${CMAKE_INSTALL_PREFIX}/BonsaiViewer.app/Contents/Frameworks") + set(_fw "${CMAKE_INSTALL_PREFIX}/BonsaiViewer.app/Contents/Frameworks") + file(GLOB _ifc_dylibs "${CMAKE_INSTALL_PREFIX}/lib/*.dylib") + list(FILTER _ifc_dylibs EXCLUDE REGEX "ifcopenshell\\.geometry\\.writer\\.") + if(_ifc_dylibs) + message(STATUS "Staging IfcOpenShell dylibs (linked core + plug-ins) into BonsaiViewer.app/Contents/Frameworks") + file(COPY ${_ifc_dylibs} DESTINATION "${_fw}") else() - message(WARNING "No ifcopenshell.*.dylib plug-ins found in lib/ — BonsaiViewer.app will fail at the first IFC load") + message(WARNING "No IfcOpenShell *.dylib found in lib/ — BonsaiViewer.app will fail to launch (missing @rpath linked deps) or at IFC load time (missing plug-ins)") endif() ]]) endif()