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<mapping/kernel>.dylib)
* non-prefixed plug-ins (ifcopenshell.parse.schema.ifcXxX.dylib,
ifcopenshell.geometry.mapping.ifcXxX.dylib, etc.)
<prefix>/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 <noreply@anthropic.com>
Mirrors the Rocky workflow's two-part size reduction (27249770e
"Reduce Rocky package size") on macOS:
1. Pass `--shared` to nix/build-all.py. The default builds
IfcOpenShell as static libs, which means every plug-in dylib
(schemas × 8, kernels × 3, mappings × 8, writers × 8, document
serializers × ~4, linework processing) statically embeds a full
copy of libIfcParse + libIfcGeom. With --shared the plug-ins
reference @rpath/libIfcParse.dylib + @rpath/libIfcGeom.dylib and
the per-plug-in dylib drops from ~30-50 MB to a few MB each.
Dominant size win.
2. Filter `ifcopenshell.geometry.writer.*.dylib` out of BonsaiViewer's
plug-in staging step in src/bonsaiviewer/CMakeLists.txt. These are
the per-schema OBJ / glTF / DAE / STP / IGS / SVG / TTL export
converters — heavy because each one inlines the full schema, and
BonsaiViewer is a viewer, never an exporter, so they're pure
deadweight inside the bundle. Additive on top of --shared.
Bundle went 300 MB → expected ~100 MB, in line with Linux (~100 MB)
and Windows (~80 MB).
The IFCOPENSHELL_BUILD_PYTHON_WRAPPER=off gate is unchanged for now
— once we confirm BonsaiViewer.app size + functionality look sane,
we can ungate the Python wrapper and see if shared-builds-on-macOS
shake out its install issues too.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
# What
Two upstream-shaped fixes to ifcopenshell's runtime plug-in discovery
so the schema/serializer plug-ins are findable in deployment layouts
other than a flat \`<prefix>/lib/\` (specifically: macOS .app bundles).
## (1) Stable anchor variable instead of a function pointer
\`schema_plugin_directory()\` used \`&load_schema_plugins\` as the
anchor whose containing module \`dladdr\` is asked to resolve. Function
addresses are not reliably equal to a single canonical location across
toolchains — on macOS arm64 with BonsaiViewer.app, \`&load_schema_plugins\`
took the address of a PLT/stub inside the consumer binary rather than
the actual symbol inside \`libIfcParse.dylib\`. \`dladdr\` then dutifully
returned the consumer's path and the loader started searching
\`BonsaiViewer.app/Contents/MacOS/\` for plug-ins that were never
installed there.
A variable doesn't suffer from this — it has exactly one canonical
address inside its defining dylib. Add \`ifcopenshell_libifcparse_anchor\`
(exported via IFC_PARSE_API) and use \`&that\` instead. Standard pattern
used by Boost.DLL, GStreamer, \`_dyld_get_image_*\`, etc.
## (2) Bundle-aware fallback search paths
The primary search path is \`dirname(libIfcParse)\`. That works for
flat installs (Linux \`lib/\`, Windows \`bin/\`) where plug-ins are
siblings of libIfcParse. macOS app bundles split the layout:
\`macdeployqt\` puts non-Qt @rpath deps in \`Contents/Frameworks/\`,
Apple convention asks for \`Contents/PlugIns/\`, and some install rules
co-locate libIfcParse with the exe in \`Contents/MacOS/\`. Plug-ins
typically end up in a sibling directory, not the same one.
In \`add_search_paths_or_default\`, after registering the primary path,
also register \`<parent>/PlugIns\`, \`<parent>/Frameworks\`, and
\`<parent>/MacOS\` on Apple platforms. \`discover_exact\` short-circuits
on the first hit so duplicates and missing directories are harmless.
# What this does NOT do
The plug-in dylibs still need to actually be inside the app bundle
somewhere for these fallbacks to find them — the upstream install
rules (\`install(TARGETS …)\` in \`src/ifcparse/CMakeLists.txt\`,
\`src/serializers/CMakeLists.txt\`, etc.) put them in \`<prefix>/lib/\`
which lives outside \`BonsaiViewer.app\`. That side of the fix is a
follow-up — either an explicit bundle-aware install destination on
the plug-in targets, or an install(CODE) sweep that mirrors them
into the bundle.
# What this also un-does
Reverts the BonsaiViewer-only \`install(CODE)\` hack that was about to
copy \`ifcopenshell.*.dylib\` from \`lib/\` into
\`BonsaiViewer.app/Contents/MacOS/\` — superseded by the loader-side
fix above, which lets us put the plug-ins anywhere sane inside the
bundle without further consumer-side stitching.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
BonsaiViewer.app crashed at launch on a fresh macOS arm64 mac with:
Library not loaded: @rpath/libwgpu_native.dylib
Referenced from: /Applications/BonsaiViewer.app/Contents/MacOS/BonsaiViewer
Reason: no LC_RPATH's found
The exe had \`LC_LOAD_DYLIB @rpath/libwgpu_native.dylib\` (CMake baked
that in from the upstream dylib's install_name), but zero \`LC_RPATH\`
entries, so dyld had nowhere to look — and macdeployqt hadn't pulled
the dylib in either, since it sat at \`<install_root>/lib/\` rather
than inside the .app.
Two changes:
- \`src/ifcviewer-wgpu/CMakeLists.txt\`: on macOS, when
BUILD_BONSAIVIEWER is on, install libwgpu_native.dylib straight
into \`BonsaiViewer.app/Contents/Frameworks/\` instead of
\`<prefix>/lib/\`. That matches the standard macOS bundle layout.
- \`src/bonsaiviewer/CMakeLists.txt\`: set
\`INSTALL_RPATH "@executable_path/../Frameworks"\` on the
BonsaiViewer target on Apple. That's where dyld looks at launch,
and where the dylib now lives.
Together the @rpath load resolves at launch without depending on
macdeployqt to follow non-Qt @rpath references (it usually only
chases Qt frameworks).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Linux (Rocky manylinux, GCC 11):
- WgpuAreaMeasurement.h: include <cstddef> directly. GCC 11 does not
transitively pull `size_t` through <vector>, so triangleCount()'s
return type fails to parse.
- WgpuViewportWindow.cpp:meshLocalToGlobal: use static_cast<double>(...)
instead of double(mesh_local[N]) when constructing the Eigen::Vector4d.
The latter triggers GCC 11's most-vexing-parse: it reads
`Vector4d local(double(mesh_local[0]), double(mesh_local[1]), ...)`
as a function declaration of `local` taking parameters
`double mesh_local[0]` etc., colliding with the outer `mesh_local`
parameter and failing with "redefinition of double* mesh_local".
macOS arm64:
- IfcViewerWgpuMinimal + BonsaiViewer install rules: change
`BUNDLE DESTINATION bin` → `BUNDLE DESTINATION .`. Qt's deploy
generator emits `macdeployqt <Target>.app` with no path prefix, which
only resolves when the bundle sits at the install-prefix root.
`BUNDLE DESTINATION bin` put it at `<prefix>/bin/Target.app` and the
install/strip step failed with "Could not find app bundle".
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Bundles four portability fixes uncovered by manually firing the platform
workflows against this branch:
- WgpuOverlayRenderer.cpp: GCC 11 (Rocky manylinux runner) does not parse
a multi-line raw string inside `#define`. Converted
THICK_LINE_HELPERS_WGSL from a `#define` to a `static const char*` and
switched AXIS_WGSL / SECTION_WGSL / MARQUEE_WGSL to `std::string` so
they can concatenate at static-init time. Three call sites now pass
`.c_str()` to svFromCStr.
- bonsaiviewer/CMakeLists.txt: added BUNDLE DESTINATION to the install
rule (same fix already applied to IfcViewerWgpuMinimal). MACOSX_BUNDLE
targets fail at configure on macOS without it even when nobody runs
`make install`.
- build_osx.yml: dropped the x64 (Intel cross-compile) matrix row. The
runner is arm64 so `brew --prefix qt` returns the arm64 prefix; we'd
need a separate x86_64 Qt install under /usr/local to cross-build
BonsaiViewer. Revisit if Intel-Mac demand resurfaces.
- build_win.yml: dropped the ARM64 matrix row. wgpu-native does not ship
a Windows-ARM64 binary, so IfcViewerWgpu's link step fails with ~60
unresolved wgpu* externs. Re-enable when upstream publishes that
target.
Cherry-pick this commit to v0.8.0 so the workflow_dispatch buttons see
the dropped rows.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Bonsai now drives the wgpu viewport for both sidecar and direct-IFC
loads. The GL viewer and its supporting state classes are gone.
SceneLoader rewire:
- Takes WgpuViewportWindow* instead of ViewportWindow*.
- Sidecar path reads metadata only (readSidecarMetadataOnly) and hands
the StreamingSidecar off to the new applyCachedModel. Field accesses
inside applySidecarData go through .meta.
- Direct-IFC path uses the wgpu A-path (upload{Mesh,Instance}Chunk +
finalizeModel). The applyLodExtension call is dropped — wgpu has no
live LOD1 splice; LOD1 still lands in the on-disk sidecar for the
next open.
Bonsai migration:
- ViewportWindow → WgpuViewportWindow across MainWindow, Measurement,
SessionState, and every modules/*/{Commands,Panel,View}.{h,cpp} —
116 sites total. Same s/OverlayRenderer::/WgpuOverlayRenderer::/
rename, 12 sites.
- Includes flipped from ../ifcviewer/ViewportWindow.h to
../ifcviewer-wgpu/WgpuViewportWindow.h. OverlayRenderer.h include
dropped (transitively reached via the viewport header).
- BonsaiViewer links IfcViewerWgpu in addition to IfcViewer for the
duration of the migration; the GL-side IfcViewer also publicly links
IfcViewerWgpu so SceneLoader can resolve WgpuViewportWindow.
GL backend deletion:
- src/ifcviewer/ViewportWindow.{cpp,h}, BvhAccel.*, OverlayRenderer.*,
Selection.*, Visibility.* all gone.
- src/ifcviewer-minimal/ removed entirely (MinimalWindow drove the GL
viewport).
- src/ifcviewer/tests: test_bvh_accel, test_selection, test_visibility
removed. The first has no replacement (wgpu doesn't use a per-instance
BVH); the latter two are ported separately. test_lod_builder,
test_sidecar_cache, test_instanced_geometry, test_federation remain
(backend-agnostic).
- IfcViewer's CMakeLists drops OpenGL, Qt::OpenGL, Qt::Widgets — none
of the surviving translation units reach for them.
Build flag plumbing:
- BUILD_BONSAIVIEWER now auto-enables BUILD_BONSAIVIEWER_WGPU since
SceneLoader requires the wgpu lib for its WgpuViewportWindow* arg.
- The wgpu subprojects add_subdirectory ahead of the GL one so
IfcViewerWgpu exists when IfcViewer's link evaluates.
- src/ifcviewer-minimal subdir reference removed from cmake/CMakeLists.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Compile the Bonsai Viewer as part of the Linux and Windows binary builds,
and ship the Autodesk connector alongside the viewer executable.
Qt6 dependencies:
- The viewer links Qt6::Svg for runtime icon tinting. Svg is a separate
base-Qt archive, so aqt now installs "qtbase qtsvg" (plus icu on Linux)
rather than qtbase alone, on both Linux and Windows.
- Qt6::CorePrivate is exposed differently across Qt versions: Qt 6.8 ships
the target inside Qt6Core, while Qt 6.10 provides it only as a separate
CorePrivate config package. The viewer CMakeLists requests it via
OPTIONAL_COMPONENTS so it resolves on both.
- When cross-compiling Windows ARM64, windeployqt runs from the host x64
Qt, so qtsvg is installed into the host Qt as well.
Windows build:
- build-all-win.py passed -DBUILD_IFCVIEWER, a flag since renamed to
BUILD_BONSAIVIEWER, so the Windows build compiled no viewer at all. It
now passes -DBUILD_BONSAIVIEWER.
- The Autodesk connector is bundled under connectors/ next to
BonsaiViewer.exe in the packaged archive, mirroring the Linux builds.
- The Windows workflow builds the connector (PyInstaller) before the main
build so it is available to bundle.
Connector bundling:
- The Linux rocky workflows build the connector and bundle it into the
BonsaiViewer archive; the Windows build now does the same.
Generated with the assistance of an AI coding tool.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Replace the "Open Recent coming soon" placeholder with a working
most-recently-used project list. RecentProjects persists .ifcfed paths
via QSettings, capped and pruned to existing files. The Open Recent
ribbon button now shows a popup menu of recent projects; every
successful open or save (local or cloud) records an entry.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Directory src/ifcviewer-full -> src/bonsaiviewer, CMake target
IfcViewerFull -> BonsaiViewer, namespace ifcviewerfull -> bonsaiviewer,
QApplication / window titles / connector path now use the new brand.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>