From 748b4e72a9a1e795e856edd890310756e2544a0b Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Thu, 4 Jun 2026 11:31:45 +1000 Subject: [PATCH] macOS: re-enable Python wrapper + stage IfcViewerMinimal.app bundle MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three coupled fixes that close the macOS bring-up loop: ## 1. ifcwrap: fix INSTALL_RPATH on Apple The ifcopenshell_wrapper Python module had `INSTALL_RPATH "$ORIGIN"` set for "NOT WIN32 AND NOT WASM_BUILD" — but `$ORIGIN` is a Linux ld.so placeholder, not a macOS dyld one. macOS dyld doesn't expand it; it bakes the literal string `$ORIGIN` into LC_RPATH, which resolves to nothing at runtime. The wrapper's hard-link `@rpath/ifcopenshell .document.rdb.dylib` then fails to load even though INSTALL(TARGETS … LIBRARY DESTINATION "${python_package_dir}/ifcopenshell") above had already dropped the plug-in dylib right next to the wrapper. Split the rpath assignment: `@loader_path` on Apple (the dyld equivalent of `$ORIGIN`), `$ORIGIN` elsewhere. This is what b0ef47819 (the build_osx IFCOS_BUILD_PYTHON_WRAPPER=off gate) was working around. The gate is removed below. ## 2. ifcviewer-minimal: stage IfcOpenShell + wgpu_native into the .app IfcViewerMinimal.app was building on macOS via the cmake `BUILD_BONSAIVIEWER → BUILD_BONSAIVIEWER_WGPU` promotion, but had no bundle staging — Contents/Frameworks/ only contained the Qt frameworks macdeployqt deposited, so the .app would refuse to start ("Library not loaded: @rpath/libwgpu_native.dylib"). Mirror what src/bonsaiviewer/CMakeLists.txt does for BonsaiViewer.app: * Set INSTALL_RPATH to `@executable_path/../Frameworks` so the exe knows where to look for @rpath/* deps. * install(FILES) libwgpu_native.dylib into the bundle's Frameworks/ (globbed from WGPU_NATIVE_LIB_DIR rather than hard-coded so it covers any future versioned name). * install(CODE) staging block that copies every `*.dylib` from /lib/ into the bundle's Frameworks/, excluding the geometry-writer plug-ins (same EXCLUDE regex as BonsaiViewer.app — viewer doesn't need OBJ/glTF/DAE/STP/IGS/SVG/TTL export converters). Same long-form rationale + caveats apply (macdeployqt doesn't follow non-Qt @rpath deps, lib-prefixed core libs vs ifcopenshell.* plug-in naming split, Linux's equivalent lives in build_rocky.yml workflow bash via patchelf + stage_runtime_payload). See src/bonsaiviewer/ CMakeLists.txt for the full version. ## 3. build_osx.yml: drop IFCOS_BUILD_PYTHON_WRAPPER=off With (1) fixed, the Python wrapper smoke test should pass again. The gate goes away; the comment block in build_osx.yml is replaced with a short note pointing at the ifcwrap rpath fix as the underlying change that re-enables this. Together, (1)+(2)+(3) close the standalone IfcViewerMinimal-on-macOS gap (task #43) and re-enable IfcOpenShell-Python on the macOS arm64 CI. Co-Authored-By: Claude Opus 4.7 --- .github/workflows/build_osx.yml | 12 ++++----- src/ifcviewer-minimal/CMakeLists.txt | 37 ++++++++++++++++++++++++++++ src/ifcwrap/CMakeLists.txt | 21 +++++++++++++--- 3 files changed, 61 insertions(+), 9 deletions(-) diff --git a/.github/workflows/build_osx.yml b/.github/workflows/build_osx.yml index f76964506e..5d4832e251 100644 --- a/.github/workflows/build_osx.yml +++ b/.github/workflows/build_osx.yml @@ -83,19 +83,19 @@ jobs: fi set -o pipefail export QT_DIR="$(brew --prefix qt)" - # IFCOS_BUILD_PYTHON_WRAPPER=off skips IfcOpenShell-Python on - # macOS until the plug-in refactor's CREATE_BUNDLE install rule - # actually drops ifcopenshell.document.rdb.dylib into - # site-packages/ifcopenshell/ (see the commit that added this - # gate for the full history + the rocksdb/macdeployqt write-up). # --shared mirrors build_rocky.yml after 27249770e: builds # IfcOpenShell as shared libs so each plug-in dylib references # libIfcParse / libIfcGeom via @rpath instead of statically # embedding them — the dominant size win for BonsaiViewer.app # (per-plugin libs go from ~30-50 MB to a few MB). + # + # IfcOpenShell-Python is back on after the ifcwrap rpath fix: + # INSTALL_RPATH "$ORIGIN" is a Linux-ism that macOS dyld bakes + # in as a literal string, so `@rpath/ifcopenshell.document.rdb + # .dylib` failed to resolve at import time. ifcwrap now sets + # INSTALL_RPATH to "@loader_path" on Apple. CXXFLAGS="-O3" CFLAGS="-O3 ${DARWIN_C_SOURCE}" ADD_COMMIT_SHA=1 BUILD_CFG=Release \ BUILD_BONSAIVIEWER=ON QT_DIR="${QT_DIR}" \ - IFCOS_BUILD_PYTHON_WRAPPER=off \ python3 ./nix/build-all.py -v --diskcleanup --shared ${MAC_INTEL} \ | tee build.log diff --git a/src/ifcviewer-minimal/CMakeLists.txt b/src/ifcviewer-minimal/CMakeLists.txt index 61ea4c9735..d5dfb06e73 100644 --- a/src/ifcviewer-minimal/CMakeLists.txt +++ b/src/ifcviewer-minimal/CMakeLists.txt @@ -45,6 +45,16 @@ if(UNIX AND NOT APPLE AND WGPU_NATIVE_LIB_DIR) ) endif() +# macOS install-time rpath — same as BonsaiViewer.app: dyld looks in +# IfcViewerMinimal.app/Contents/Frameworks for @rpath/* link deps +# (libwgpu_native.dylib, libIfcParse.dylib, libifcopenshell.geometry.dylib, +# …). The actual files get staged below via install(CODE). +if(APPLE) + set_target_properties(IfcViewerMinimal PROPERTIES + INSTALL_RPATH "@executable_path/../Frameworks" + ) +endif() + # BUNDLE DESTINATION must be "." (install-prefix root) on macOS — Qt's # deploy generator emits `macdeployqt IfcViewerMinimal.app` (no # path prefix), which resolves only when the .app sits at the install @@ -56,3 +66,30 @@ install(TARGETS IfcViewerMinimal BUNDLE DESTINATION . ) ifcopenshell_deploy_qt_runtime(IfcViewerMinimal) + +# Mirror BonsaiViewer.app's bundle staging on macOS — see +# src/bonsaiviewer/CMakeLists.txt for the long-form rationale (macdeployqt +# doesn't follow non-Qt @rpath deps, the lib-prefixed core libs vs +# ifcopenshell.* plug-in naming split, the geometry-writer EXCLUDE filter +# that keeps the bundle small). +if(APPLE) + # libwgpu_native.dylib lives in WGPU_NATIVE_LIB_DIR (set by + # src/ifcviewer/CMakeLists.txt). Glob it rather than hard-code the + # filename so we cover both libwgpu_native.dylib (and any future + # versioned name). + file(GLOB _wgpu_native_dylib "${WGPU_NATIVE_LIB_DIR}/libwgpu_native.*") + install(FILES ${_wgpu_native_dylib} + DESTINATION "IfcViewerMinimal.app/Contents/Frameworks") + + install(CODE [[ + set(_fw "${CMAKE_INSTALL_PREFIX}/IfcViewerMinimal.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 IfcViewerMinimal.app/Contents/Frameworks") + file(COPY ${_ifc_dylibs} DESTINATION "${_fw}") + else() + message(WARNING "No IfcOpenShell *.dylib found in lib/ — IfcViewerMinimal.app will fail to launch (missing @rpath linked deps) or at IFC load time (missing plug-ins)") + endif() + ]]) +endif() diff --git a/src/ifcwrap/CMakeLists.txt b/src/ifcwrap/CMakeLists.txt index 12c6f97999..14ccf82016 100644 --- a/src/ifcwrap/CMakeLists.txt +++ b/src/ifcwrap/CMakeLists.txt @@ -233,9 +233,24 @@ IF(Python_Interpreter_FOUND OR PYTHON_MODULE_INSTALL_DIR) endif() if(NOT WIN32 AND NOT WASM_BUILD) - set_target_properties(ifcopenshell_wrapper PROPERTIES - INSTALL_RPATH "$ORIGIN" - ) + # On Linux $ORIGIN expands to the directory of the loaded + # object (= site-packages/ifcopenshell/). macOS dyld doesn't + # expand $ORIGIN; the equivalent placeholder is + # @loader_path. Setting $ORIGIN on macOS bakes a literal + # "$ORIGIN" into LC_RPATH which resolves to nothing at + # runtime — the wrapper's `@rpath/ifcopenshell.document.rdb + # .dylib` LC_LOAD_DYLIB then fails to find the plug-in even + # though INSTALL(TARGETS … LIBRARY DESTINATION …/ifcopenshell) + # above has dropped it right next to the wrapper. + if(APPLE) + set_target_properties(ifcopenshell_wrapper PROPERTIES + INSTALL_RPATH "@loader_path" + ) + else() + set_target_properties(ifcopenshell_wrapper PROPERTIES + INSTALL_RPATH "$ORIGIN" + ) + endif() endif() endif() if (MSVC)