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 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 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 ) ensure_soname_links "$dest" } # 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