mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-28 07:49:59 +00:00
7845f8e8cd
`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. 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 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. 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 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>
311 lines
14 KiB
YAML
311 lines
14 KiB
YAML
name: Build IfcOpenShell Linux
|
|
|
|
on:
|
|
workflow_dispatch:
|
|
|
|
jobs:
|
|
build_ifcopenshell:
|
|
runs-on: ubuntu-22.04
|
|
container: rockylinux:9
|
|
|
|
steps:
|
|
- name: Set up uv
|
|
uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0
|
|
|
|
- name: Install Python
|
|
# Installs latest Python version so it's preferred by uv over Rocky's system Python.
|
|
run: uv python install
|
|
|
|
- name: Install Dependencies
|
|
run: |
|
|
dnf update -y
|
|
dnf install -y epel-release
|
|
# --enablerepo=crb: libstdc++-static (libsupc++.a, needed by the
|
|
# bundled FLTK link) lives in Rocky's CodeReady Builder repo, which
|
|
# is disabled by default.
|
|
dnf install -y --enablerepo=crb gcc gcc-c++ git autoconf automake bison make zip cmake \
|
|
bzip2 patch mesa-libGL-devel libffi-devel fontconfig-devel \
|
|
sqlite-devel bzip2-devel zlib-devel openssl-devel xz-devel \
|
|
readline-devel ncurses-devel libffi-devel libuuid-devel git-lfs \
|
|
findutils xz byacc patchelf libxkbcommon-devel \
|
|
dbus-devel \
|
|
libXext-devel libXinerama-devel libXcursor-devel libXrender-devel \
|
|
libXfixes-devel libXft-devel pango-devel cairo-devel libstdc++-static
|
|
git config --global --add safe.directory '*'
|
|
|
|
- name: Install Rust
|
|
# The bonsaiviewer-autodesk connector is a Rust crate; the "Package
|
|
# .zip archives" step below runs `cargo build --release` via
|
|
# packaging/build.py. Match the dedicated connector workflow's stable
|
|
# toolchain (dtolnay/rust-toolchain@stable).
|
|
run: |
|
|
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal --default-toolchain stable
|
|
echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
|
|
|
|
- name: Install aws cli
|
|
run: |
|
|
curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip"
|
|
unzip awscliv2.zip
|
|
./aws/install
|
|
rm -rf awscliv2.zip aws
|
|
aws --version
|
|
|
|
- name: Checkout Repository
|
|
uses: actions/checkout@v7
|
|
with:
|
|
submodules: recursive
|
|
|
|
- name: Checkout Build Repository
|
|
uses: actions/checkout@v7
|
|
with:
|
|
repository: IfcOpenShell/build-outputs
|
|
path: ./build
|
|
ref: rockylinux9-x64
|
|
lfs: true
|
|
token: ${{ secrets.BUILD_REPO_TOKEN }}
|
|
|
|
- name: Unpack Dependencies
|
|
run: |
|
|
cd build
|
|
uv run ../nix/cache_dependencies.py unpack
|
|
|
|
- name: ccache
|
|
uses: hendrikmuhs/ccache-action@v1.2.23
|
|
with:
|
|
key: ubuntu-22.04-${{ runner.arch }}-rockylinux9
|
|
|
|
- name: Run Build Script
|
|
shell: bash
|
|
run: |
|
|
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 --opencascade-shared 2>&1 \
|
|
| tee build.log
|
|
|
|
- name: Upload Build Logs
|
|
if: always()
|
|
uses: actions/upload-artifact@v7
|
|
with:
|
|
name: build-logs-rocky
|
|
path: |
|
|
build.log
|
|
build/*/*/logs/*.log
|
|
retention-days: 30
|
|
|
|
- name: Pack Dependencies
|
|
run: |
|
|
cd build
|
|
uv run ../nix/cache_dependencies.py pack
|
|
|
|
- name: Commit and Push Changes to Build Repository
|
|
run: |
|
|
cd build
|
|
git config user.name "IfcOpenBot"
|
|
git config user.email "ifcopenbot@ifcopenshell.org"
|
|
git add "$(find . -maxdepth 4 -name install)/*.tar.gz"
|
|
git commit -m "Update build artifacts [skip ci]" || echo "No changes to commit"
|
|
git push || true
|
|
|
|
- name: Package .zip archives
|
|
shell: bash
|
|
run: |
|
|
VERSION=v`cat VERSION`
|
|
# bonsaiviewer-autodesk is now a Rust connector. packaging/build.py
|
|
# invokes `cargo build --release` and stages the binary +
|
|
# connector.json into dist/autodesk/. Same on-disk shape as the
|
|
# old PyInstaller flow so the symlink + zip steps below
|
|
# continue to work unchanged.
|
|
uv run src/bonsaiviewer-autodesk/packaging/build.py
|
|
autodesk_connector_dir="$PWD/src/bonsaiviewer-autodesk/dist/autodesk"
|
|
test -d "$autodesk_connector_dir"
|
|
|
|
cd ./build/`uname`/*/install/ifcopenshell
|
|
mkdir -p ~/output
|
|
install_root="$PWD"
|
|
QT6_VERSION="${QT6_VERSION:-6.8.3}"
|
|
|
|
if [ -z "${QT_DIR:-}" ]; then
|
|
for qt_candidate in "$(dirname "$install_root")"/qt6-${QT6_VERSION}-*/${QT6_VERSION}/*; do
|
|
if [ -d "$qt_candidate/lib" ]; then
|
|
QT_DIR="$qt_candidate"
|
|
break
|
|
fi
|
|
done
|
|
fi
|
|
|
|
# Ensure that all shared libraries in provided dest `$1`
|
|
# are present using their SONAMEs (at least as symlinks).
|
|
ensure_soname_links() {
|
|
dest="$1"
|
|
find "$dest" -maxdepth 1 -type f -name "*.so*" | while IFS= read -r shared_object; do
|
|
# TODO: actual pattern is "Library soname" instead of "Shared library"?
|
|
soname=$(readelf -d "$shared_object" 2>/dev/null | sed -n 's/.*(SONAME).*Shared library: \[\(.*\)\].*/\1/p' | head -n 1)
|
|
[ -n "$soname" ] || continue
|
|
[ -e "$dest/$soname" ] && continue
|
|
ln -s "$(basename "$shared_object")" "$dest/$soname"
|
|
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() {
|
|
dest="$1"
|
|
include_geometry_writers="${2:-1}"
|
|
while IFS= read -r runtime_file; do
|
|
if [ "$include_geometry_writers" != "1" ] && [[ "$(basename "$runtime_file")" == ifcopenshell.geometry.writer.* ]]; then
|
|
continue
|
|
fi
|
|
cp -P "$runtime_file" "$dest/"
|
|
done < <(
|
|
for runtime_dir in "$install_root/bin" "$install_root/lib" "$install_root/lib64"; do
|
|
[ -d "$runtime_dir" ] || continue
|
|
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`.
|
|
stage_qt_runtime_payload() {
|
|
exe_path="$1"
|
|
dest="$2"
|
|
[ -n "${QT_DIR:-}" ] && [ -d "$QT_DIR/lib" ] || return 0
|
|
|
|
# Skip executables that don't depend on QT (don't have `libQt6` referenced).
|
|
if ! LD_LIBRARY_PATH="$QT_DIR/lib:${LD_LIBRARY_PATH:-}" ldd "$exe_path" 2>/dev/null | grep -q "libQt6"; then
|
|
return 0
|
|
fi
|
|
|
|
# Copy all QT libs to `dest`.
|
|
find "$QT_DIR/lib" -maxdepth 1 \( -type f -o -type l \) -name "*.so*" -exec cp -P {} "$dest/" \;
|
|
ensure_soname_links "$dest"
|
|
|
|
# Copy QT plugins.
|
|
if [ -d "$QT_DIR/plugins" ]; then
|
|
pushd "$QT_DIR/plugins" > /dev/null
|
|
find . \( -type f -o -type l \) -name "*.so*" | while IFS= read -r plugin_file; do
|
|
mkdir -p "$dest/plugins/$(dirname "$plugin_file")"
|
|
cp -P "$plugin_file" "$dest/plugins/$plugin_file"
|
|
done
|
|
popd > /dev/null
|
|
# Point plugins rpath to `$dest`.
|
|
if [ -d "$dest/plugins" ]; then
|
|
find "$dest/plugins" -type f -name "*.so*" -exec patchelf --set-rpath '$ORIGIN/../..:$ORIGIN' {} \;
|
|
fi
|
|
fi
|
|
|
|
find "$dest" -maxdepth 1 -type f -name "*.so*" -exec patchelf --set-rpath '$ORIGIN' {} \;
|
|
|
|
printf "[Paths]\nPrefix = .\n" > "$dest/qt.conf"
|
|
}
|
|
|
|
# Check all binaries in the dest `$1`
|
|
# and report if they're still missing dependencies or are static.
|
|
check_runtime_dependencies() {
|
|
package_dir="$1"
|
|
missing=0
|
|
# Iterate over all .so files.
|
|
while IFS= read -r binary_file; do
|
|
# Skip non-binaries.
|
|
readelf -h "$binary_file" >/dev/null 2>&1 || continue
|
|
# Report non-dynamic binaries.
|
|
if ! env -u LD_LIBRARY_PATH ldd "$binary_file" > "$package_dir/.ldd.out" 2>&1; then
|
|
echo "ldd failed for $binary_file"
|
|
cat "$package_dir/.ldd.out"
|
|
missing=1
|
|
continue
|
|
fi
|
|
# Report missing dependencies.
|
|
if grep -q "not found" "$package_dir/.ldd.out"; then
|
|
echo "Missing runtime dependencies for $binary_file"
|
|
grep "not found" "$package_dir/.ldd.out"
|
|
missing=1
|
|
fi
|
|
done < <(find "$package_dir" -type f \( -perm /111 -o -name "*.so" -o -name "*.so.*" \))
|
|
rm -f "$package_dir/.ldd.out"
|
|
# TODO: should error?
|
|
if [ "$missing" -ne 0 ]; then
|
|
echo "Runtime dependency check found issues; continuing packaging."
|
|
fi
|
|
return 0
|
|
}
|
|
|
|
# Iterate over all built Python wrappers in `install/ifcopenshell/python-x.y.z`.
|
|
# and zip them, bundling all dynamic libs from `lib`.
|
|
ls -d python-* | while read py_version; do
|
|
postfix=`echo ${py_version: -1} | sed s/[0-9]//`
|
|
numbers=`echo $py_version | grep -oE '[0-9]+\.[0-9]+' | tr -d '.'`
|
|
py_version_major=python-${numbers}$postfix
|
|
pushd . > /dev/null
|
|
cd $py_version
|
|
if [ ! -d ifcopenshell ]; then
|
|
mkdir ../ifcopenshell_
|
|
mv * ../ifcopenshell_
|
|
mv ../ifcopenshell_ ifcopenshell
|
|
fi
|
|
[ -d ifcopenshell/__pycache__ ] && rm -rf ifcopenshell/__pycache__
|
|
find ifcopenshell -name "*.pyc" -delete
|
|
# TODO: packs qt libs also?
|
|
stage_runtime_payload ifcopenshell
|
|
zip -y -r -qq ifcopenshell-${py_version_major}-${VERSION}-${GITHUB_SHA:0:7}-linux64.zip ifcopenshell
|
|
mv *.zip ~/output
|
|
popd > /dev/null
|
|
done
|
|
|
|
# Iterate over all executables in `install/ifcopenshell/bin` and zip them.
|
|
# Each zip bundles dynamic libs from `lib` and also qt libs.
|
|
find "$install_root/bin" -maxdepth 1 -type f -perm /111 ! -name "*.zip" ! -name "*.so" ! -name "*.so.*" ! -name "*.dylib" ! -name "*.dll" | while read exe_path; do
|
|
exe=`basename "$exe_path"`
|
|
package_dir="$install_root/.package-${exe}"
|
|
rm -rf "$package_dir"
|
|
mkdir -p "$package_dir"
|
|
cp "$exe_path" "$package_dir/"
|
|
patchelf --set-rpath '$ORIGIN' "$package_dir/$exe"
|
|
stage_runtime_payload "$package_dir" 0
|
|
stage_qt_runtime_payload "$exe_path" "$package_dir"
|
|
if [ "$exe" = "BonsaiViewer" ]; then
|
|
mkdir -p "$package_dir/connectors"
|
|
cp -a "$autodesk_connector_dir" "$package_dir/connectors/"
|
|
fi
|
|
check_runtime_dependencies "$package_dir"
|
|
pushd "$package_dir" > /dev/null
|
|
zip -y -qq -r "$HOME/output/${exe}-${VERSION}-${GITHUB_SHA:0:7}-linux64.zip" .
|
|
popd > /dev/null
|
|
rm -rf "$package_dir"
|
|
done
|
|
|
|
- name: Configure AWS credentials
|
|
uses: aws-actions/configure-aws-credentials@v6
|
|
with:
|
|
aws-access-key-id: ${{ secrets.AWS_UPLOAD_ACCESS_KEY_ID }}
|
|
aws-secret-access-key: ${{ secrets.AWS_UPLOAD_SECRET_ACCESS_KEY }}
|
|
aws-region: us-east-1
|
|
|
|
- name: Upload .zip archives to S3
|
|
run: |
|
|
aws s3 cp ~/output s3://ifcopenshell-builds/ --recursive
|