mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-28 15:53:00 +00:00
ci: build OCCT shared on Linux so plug-ins share one OCCT instance
`geom.tree().select()` silently returns zero results (or raises SWIG's "An unknown error occurred") in the Linux release packages, while the same commit built from source returns correct answers. `select_box()` agrees between both, and geometry conversion is bit-identical -- only operations that touch a stored TopoDS_Shape diverge. Cause is linkage, not code.8bdaa8c7cnarrowed the Rocky builds from `--shared` to `--ifcopenshell-shared`, which shares IfcOpenShell's own libraries but leaves every dependency static. OCCT is then compiled privately into each plug-in that uses it -- 15 of them, verified by their own copies of the BRepClass3d/BRepExtrema/Standard_Failure strings. That contradicts what the binaries already declare. tree.h:1748 casts a `conversion_result_shape*` to `open_cascade_shape*`, moves the TopoDS_Shape out of it and frees it; `open_cascade_shape` is defined once in ifcopenshell_geometry_kernel_opencascade.so and left undefined in ifcopenshell_geometry_tree_opencascade_brep.so for the loader to resolve. So the two plug-ins are designed to share one OCCT-based type system, yet static linking gives each its own Standard_Type registry and allocator. Shapes get read and released by a different OCCT instance than made them. Add `--opencascade-shared`, mirroring the existing `--ifcopenshell-shared` precedent, and use it on both Rocky workflows. It cannot be spelled `--occt-shared`: build-all.py parses any `occt-*` flag as a version override. BUILD_STATIC drives three things at once -- dependency link type, -fvisibility=hidden, and BUILD_SHARED_LIBS -- so making one dependency shared means overriding all three for it. Visibility is the subtle one: OCCT's Standard_EXPORT expands to nothing on Unix, so it relies on default visibility to export its API. Built shared under -fvisibility=hidden it exports almost nothing and its own libraries cannot resolve against each other (libTKMath.so fails to find NCollection_BaseAllocator::CommonBaseAllocator in libTKernel.so). Static archives are immune, which is why this surfaces only once OCCT goes shared. Compile OCCT with the pre-visibility flag set instead. Put the shared OCCT on LD_LIBRARY_PATH for the build itself. Nothing else points at it -- IfcOpenShell's libraries get INSTALL_RPATH=$ORIGIN and OCCT sits in its own dependency prefix -- so the post-build `import ifcopenshell` check fails with "libTKernel.so.7.8: cannot open shared object file". This is build-time only; the shipped packages get libTK*.so* staged beside the payload with an $ORIGIN RUNPATH instead. Suffix OCCT's install directory with `-shared` when it applies. Static and shared installs are not interchangeable, but `build_dependency` skips any dependency whose install dir already exists and cache_dependencies.py keys its tarballs purely on that directory name -- so the static `cache-occt-7.8.1.tar.gz` restored from the build-outputs repo silently satisfied the build and BUILD_LIBRARY_TYPE was never applied. This is the same cache stickiness8bdaa8c7cdescribed, pointing the other way. The suffix makes the key configuration-aware, so it self-invalidates and the static tarball stays valid for builds that still want static. Packaging is the other half, and is why8bdaa8c7cbacked the flag out -- `stage_runtime_payload` only copies from install/ifcopenshell, so OCCT in install/occt-* was never staged and `--shared` "worked by accident" off cached static outputs. Stage libTK*.so* alongside, then give every staged library an $ORIGIN RUNPATH: the core libs currently carry dead build-machine RPATHs and the plug-ins carry none, resolving only because the Python wrapper pulls them in by SONAME first. Shared OCCT has no such first loader, since it is reached through the dlopen'd plug-ins. This shrinks the packages. Duplicated OCCT is 113 MB of the 287 MB unpacked python payload (the eight geometry_writer_ifc* plug-ins alone are 4.6 MB each); one shared copy of the 29 linked libTK is around 67 MB, and the plug-ins collapse to source-build sizes -- kernel_opencascade 15.4 MB -> 1.2 MB, tree_opencascade_brep 9.6 MB -> 248 KB. Same argument asa91b1da28("Reduce Rocky package size") and402591e71. macOS and Windows are affected too but are not fixed here. Their packaging resolves via @loader_path install names and would need install_name_tool rewriting, which cannot be verified from Linux; adding the flag without that would ship a package that fails to load. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -80,7 +80,7 @@ jobs:
|
||||
set -o pipefail
|
||||
CXXFLAGS="-O3" CFLAGS="-O3" ADD_COMMIT_SHA=1 BUILD_CFG=Release BUILD_BONSAIVIEWER=ON \
|
||||
uv run --with aqtinstall ./nix/build-all.py \
|
||||
-v --diskcleanup --ifcopenshell-shared 2>&1 \
|
||||
-v --diskcleanup --ifcopenshell-shared --opencascade-shared 2>&1 \
|
||||
| tee build.log
|
||||
|
||||
- name: Upload Build Logs
|
||||
@@ -147,6 +147,22 @@ jobs:
|
||||
done
|
||||
}
|
||||
|
||||
# Copy the shared OCCT from the dependency prefix to the provided `$1`.
|
||||
# OCCT is built shared (`--opencascade-shared`) so that the opencascade kernel
|
||||
# and tree plug-ins share a single OCCT instance: `open_cascade_shape` objects
|
||||
# are created by the kernel plug-in and then have their `TopoDS_Shape` moved out
|
||||
# and freed by a tree plug-in. A private static OCCT per plug-in gives each its
|
||||
# own Standard_Type registry and allocator, which silently corrupts those shapes.
|
||||
# These libs live under `install/occt-*` rather than `install/ifcopenshell`,
|
||||
# so `stage_runtime_payload` does not pick them up on its own.
|
||||
stage_occt_runtime_payload() {
|
||||
dest="$1"
|
||||
for occt_lib_dir in "$(dirname "$install_root")"/occt-*/lib "$(dirname "$install_root")"/occt-*/lib64; do
|
||||
[ -d "$occt_lib_dir" ] || continue
|
||||
find "$occt_lib_dir" -maxdepth 1 \( -type f -o -type l \) -name "libTK*.so*" -exec cp -P {} "$dest/" \;
|
||||
done
|
||||
}
|
||||
|
||||
# Copy all libs from `install/ifcopenshell` to the provided `$1`.
|
||||
# Set `$2` to `0` to skip including geometry writers.
|
||||
stage_runtime_payload() {
|
||||
@@ -163,7 +179,14 @@ jobs:
|
||||
find "$runtime_dir" \( -type f -o -type l \) \( -name "*.so" -o -name "*.so.*" -o -name "*.dylib" -o -name "*.dll" \)
|
||||
done
|
||||
)
|
||||
stage_occt_runtime_payload "$dest"
|
||||
ensure_soname_links "$dest"
|
||||
# The core libs ship with dead build-machine RPATHs and the plug-ins have
|
||||
# none; today they resolve only because the Python wrapper ($ORIGIN) pulls
|
||||
# them in by SONAME before any plug-in is dlopen'd. Shared OCCT has no such
|
||||
# first loader -- it is reached through the plug-ins -- so give every staged
|
||||
# library an $ORIGIN of its own.
|
||||
find "$dest" -maxdepth 1 -type f -name "*.so*" -exec patchelf --set-rpath '$ORIGIN' {} \;
|
||||
}
|
||||
|
||||
# Copy all libs from `QT_DIR` to the provided `$2`.
|
||||
|
||||
@@ -92,7 +92,7 @@ jobs:
|
||||
set -o pipefail
|
||||
CXXFLAGS="-O3" CFLAGS="-O3" ADD_COMMIT_SHA=1 BUILD_CFG=Release BUILD_BONSAIVIEWER=ON \
|
||||
uv run --with aqtinstall ./nix/build-all.py \
|
||||
-v --diskcleanup --ifcopenshell-shared 2>&1 \
|
||||
-v --diskcleanup --ifcopenshell-shared --opencascade-shared 2>&1 \
|
||||
| tee build.log
|
||||
|
||||
- name: Upload Build Logs
|
||||
@@ -156,6 +156,22 @@ jobs:
|
||||
done
|
||||
}
|
||||
|
||||
# Copy the shared OCCT from the dependency prefix to the provided `$1`.
|
||||
# OCCT is built shared (`--opencascade-shared`) so that the opencascade kernel
|
||||
# and tree plug-ins share a single OCCT instance: `open_cascade_shape` objects
|
||||
# are created by the kernel plug-in and then have their `TopoDS_Shape` moved out
|
||||
# and freed by a tree plug-in. A private static OCCT per plug-in gives each its
|
||||
# own Standard_Type registry and allocator, which silently corrupts those shapes.
|
||||
# These libs live under `install/occt-*` rather than `install/ifcopenshell`,
|
||||
# so `stage_runtime_payload` does not pick them up on its own.
|
||||
stage_occt_runtime_payload() {
|
||||
dest="$1"
|
||||
for occt_lib_dir in "$(dirname "$install_root")"/occt-*/lib "$(dirname "$install_root")"/occt-*/lib64; do
|
||||
[ -d "$occt_lib_dir" ] || continue
|
||||
find "$occt_lib_dir" -maxdepth 1 \( -type f -o -type l \) -name "libTK*.so*" -exec cp -P {} "$dest/" \;
|
||||
done
|
||||
}
|
||||
|
||||
stage_runtime_payload() {
|
||||
dest="$1"
|
||||
include_geometry_writers="${2:-1}"
|
||||
@@ -170,7 +186,14 @@ jobs:
|
||||
find "$runtime_dir" \( -type f -o -type l \) \( -name "*.so" -o -name "*.so.*" -o -name "*.dylib" -o -name "*.dll" \)
|
||||
done
|
||||
)
|
||||
stage_occt_runtime_payload "$dest"
|
||||
ensure_soname_links "$dest"
|
||||
# The core libs ship with dead build-machine RPATHs and the plug-ins have
|
||||
# none; today they resolve only because the Python wrapper ($ORIGIN) pulls
|
||||
# them in by SONAME before any plug-in is dlopen'd. Shared OCCT has no such
|
||||
# first loader -- it is reached through the plug-ins -- so give every staged
|
||||
# library an $ORIGIN of its own.
|
||||
find "$dest" -maxdepth 1 -type f -name "*.so*" -exec patchelf --set-rpath '$ORIGIN' {} \;
|
||||
}
|
||||
|
||||
stage_qt_runtime_payload() {
|
||||
|
||||
Reference in New Issue
Block a user