FazBrowse GitHub Viewer | Trending |
URL:
| Home
Tools: [Download Repo ZIP]   [Original HTTPS Page]

ci: build OCCT shared on Linux so plug-ins share one OCCT instance by Moult · Pull Request #9341 · IfcOpenShell/IfcOpenShell · GitHub

ci: build OCCT shared on Linux so plug-ins share one OCCT instance - #9341

Open
Moult wants to merge 1 commit into
v0.9.0from
fix/occt-shared-linkage
Open

ci: build OCCT shared on Linux so plug-ins share one OCCT instance#9341
Moult wants to merge 1 commit into
v0.9.0from
fix/occt-shared-linkage

Conversation

Moult commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

AI verdict below, out of my knowledge area, but I can confirm the bug and fix. I think @Andrej730 encountered something similar too.


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. 8bdaa8c narrowed 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 stickiness 8bdaa8c described, 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 why 8bdaa8c backed 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 as a91b1da ("Reduce Rocky package size") and 402591e.

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.

Moult force-pushed the fix/occt-shared-linkage branch from 35f261b to 4bfbf6b Compare August 21, 2026 13:56
`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. 8bdaa8c narrowed 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.

Link the OCCT set with --as-needed. FindOpenCASCADE.cmake's config branch
uses OCCT's *complete* module list, Visualization included, which against a
static OCCT costs nothing -- an unreferenced module contributes no objects.
Against a shared OCCT all 47 become hard DT_NEEDED entries, and libTKV3d
pulls libGL.so.1 + libEGL.so.1, so `import ifcopenshell` fails on any
headless machine with "libEGL.so.1: cannot open shared object file" even
though nothing ever opens a window. Measured through the real find_package
path with the LINK_GROUP workaround composed: 67 DT_NEEDED without the
flag, 3 with it, TKV3d and TKOpenGl gone.

The flag is deliberately left open rather than closed with
-Wl,--no-as-needed. CMake emits the imported targets'
INTERFACE_LINK_LIBRARIES -- where OCCT lists libGL/libEGL -- after that
item, so closing the bracket switches the flag off immediately before the
libraries it exists to exclude. Verified against the shipped artifact:
closed, the kernel plug-in fell from 47 DT_NEEDED libTK entries to 14 and
lost TKV3d, yet still carried a direct libEGL.so.1 and still failed to
import on a headless server; open, the same 14 remain and libGL/libEGL are
gone. None of the 14 retained modules depends on GL.

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 the same way. 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 stickiness 8bdaa8c described, 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 why 8bdaa8c backed 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.

The packages shrink: the python zip goes from 109.4 MB to 85.6 MB, because
the duplicated OCCT was 113 MB of the 287 MB unpacked payload (the eight
geometry_writer_ifc* plug-ins alone were 4.6 MB each) against ~67 MB for
one shared copy. Same argument as a91b1da ("Reduce Rocky package size")
and 402591e.

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>
Moult force-pushed the fix/occt-shared-linkage branch from 4bfbf6b to 7845f8e Compare August 21, 2026 22:19

aothms commented Aug 22, 2026
edited
Loading

Copy link
Copy Markdown
Member

I had a similar suggestion to Andrej, but if it's broken we don't need to retain the old behavior. I'd say, just remove all flags related to static/shared building from the build script and default to the new behavior. (Leave cmake as is, no need to remove options there). Also, I don't think these problems (multiple static libs compiled into multiple dynamic libraries loaded simultaneously) are unique to opencascade, just more apparent on our side because we have wider use across different dlls. Everything always shared seems to be the only sane default.

I think we even saw variations of this already currently with CGAL > Boost. (I think because we have cgal-simple and cgal each linking to their own boost symbols, all as static libraries and then indeed conflicting). So we use /FORCE there on MSVC to ignore the duplications. Since we have plug-in dlls now anyway: probably shared libs for the deps is not unlogical, but not sure about the performance implications in case of granular API usage (e.g SVG serializer) due to no inlining and no link-time optimization. But anyway SVG serializer performance is bad and we need to do our own.

Moult commented Aug 24, 2026

Copy link
Copy Markdown
Contributor Author

@Andrej730 I noticed you recently added the --occt-shared flag, what do you think about @aothms ' suggestion to go full shared on everything?

Does this also mean that there will be a bazillion sofiles?

aothms commented Aug 25, 2026
edited
Loading

Copy link
Copy Markdown
Member

Does this also mean that there will be a bazillion sofiles?

I don't think it's a different order of magnitude. Worst offenders are definitely ifcopenshell itself (plugins) and opencascade.

Copy link
Copy Markdown
Contributor

About --occt-shared - I was trying to test if this is enough to resolve all the issues I've met with OCCT recently, can confirm that it is. The reasoning in the first post is correct - each binary with static OCCT has its own Standard_Type registry, so e.g. if one binary is creating STANDARD_TYPE(Geom_Line), then only this binary would be able to recognize it as Geom_Line, any other would think it's a unknown entity.

But agree, moving to full shared build seems to be the most straightforward way to resolve this, since we already have plugins, ifcopenshell libs as shared + OCCT needs it too, + potentially Boost, so we might as well reuse the infrastructure for managing and bundling shared libs for all other dependencies too, maybe it will save us from some side effects or reduce binaries size in some cases. And most .so files are coming from OCCT and ifcopenshell either way, as Thomas mentioned.

Folder Total .so Symlinks Regular files
occt-shared-7.8.1 144 96 48
qt6-6.8.3-gcc_64 105 42 63
ifcopenshell 87 8 79
boost-1.86.0 16 8 8
pcre-8.41 9 6 3
mpfr-3.1.6 3 2 1
manifold-3.2.1 3 2 1
libxml2-2.13.8 3 2 1
gmp-6.3.0 3 2 1
OpenCOLLADA 10 0 10

This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
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.

3 participants


Back | FazBrowse Home | New Git URL