From de7520418b7208bba31272bccc72831ffc63e1c2 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Thu, 21 May 2026 13:23:27 +1000 Subject: [PATCH] Build the Bonsai Viewer in CI with the Autodesk connector bundled 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 --- .github/workflows/build_rocky.yml | 13 ++++++++++++- .github/workflows/build_rocky_arm.yml | 13 ++++++++++++- .github/workflows/build_win.yml | 18 ++++++++++++++++++ nix/build-all.py | 9 +++++++-- src/bonsaiviewer/CMakeLists.txt | 10 +++++++++- win/build-all-win.py | 25 ++++++++++++++++++++++++- win/build-deps.cmd | 18 ++++++++++++++---- 7 files changed, 96 insertions(+), 10 deletions(-) diff --git a/.github/workflows/build_rocky.yml b/.github/workflows/build_rocky.yml index 0fd59ce3fd..da518d25bc 100644 --- a/.github/workflows/build_rocky.yml +++ b/.github/workflows/build_rocky.yml @@ -17,9 +17,11 @@ jobs: 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 + findutils xz byacc patchelf libxkbcommon-devel \ + python3.11 python3.11-pip python3.11-tkinter python3 -m pip install typing_extensions aqtinstall git config --global --add safe.directory '*' + python3.11 -c "import tkinter; print('Tk', tkinter.TkVersion)" - name: Install aws cli run: | @@ -87,6 +89,11 @@ jobs: shell: bash run: | VERSION=v`cat VERSION` + python3.11 -m pip install "src/bonsaiviewer-autodesk[build]" + python3.11 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" @@ -205,6 +212,10 @@ jobs: patchelf --set-rpath '$ORIGIN' "$package_dir/$exe" stage_runtime_payload "$package_dir" 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" . diff --git a/.github/workflows/build_rocky_arm.yml b/.github/workflows/build_rocky_arm.yml index 43fe0c72bd..ca1a451cae 100644 --- a/.github/workflows/build_rocky_arm.yml +++ b/.github/workflows/build_rocky_arm.yml @@ -17,9 +17,11 @@ jobs: 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 + findutils xz byacc patchelf libxkbcommon-devel \ + python3.11 python3.11-pip python3.11-tkinter python3 -m pip install typing_extensions aqtinstall git config --global --add safe.directory '*' + python3.11 -c "import tkinter; print('Tk', tkinter.TkVersion)" - name: Install aws cli run: | @@ -87,6 +89,11 @@ jobs: shell: bash run: | VERSION=v`cat VERSION` + python3.11 -m pip install "src/bonsaiviewer-autodesk[build]" + python3.11 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" @@ -206,6 +213,10 @@ jobs: patchelf --set-rpath '$ORIGIN' "$package_dir/$exe" stage_runtime_payload "$package_dir" 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}-linuxarm64.zip" . diff --git a/.github/workflows/build_win.yml b/.github/workflows/build_win.yml index 2e27938bf4..695f726ee3 100644 --- a/.github/workflows/build_win.yml +++ b/.github/workflows/build_win.yml @@ -63,6 +63,24 @@ jobs: # and with default 500MB some cache gets deleted, leading to misses. max-size: 5000MB + - name: Set up Python for connector build + uses: actions/setup-python@v6 + with: + python-version: '3.12' + + # The connector's PyInstaller bundle embeds a tkinter GUI; verify Tk is + # present so a missing-tk regression fails here, not inside the build. + - name: Verify tkinter is available + run: python -c "import tkinter; print('Tk', tkinter.TkVersion)" + + # Build the Autodesk connector before the C++ build: build-all-win.py + # bundles it next to BonsaiViewer.exe while archiving the executables. + - name: Build Autodesk connector + working-directory: src/bonsaiviewer-autodesk + run: | + python -m pip install ".[build]" + python packaging/build.py + - name: Run Build Script And Pack .zip Archives shell: cmd env: diff --git a/nix/build-all.py b/nix/build-all.py index 6d1ab4d7db..f4c093bc60 100644 --- a/nix/build-all.py +++ b/nix/build-all.py @@ -805,7 +805,8 @@ def install_qt6() -> str: qt_config = qt_dir / "lib" / "cmake" / "Qt6" / "Qt6Config.cmake" qt_core = qt_dir / "lib" / "libQt6Core.so.6" - if qt_config.exists() and qt_core.exists(): + qt_svg = qt_dir / "lib" / "cmake" / "Qt6Svg" / "Qt6SvgConfig.cmake" + if qt_config.exists() and qt_core.exists() and qt_svg.exists(): logger.info(f"Found existing Qt6 at {qt_dir}, skipping") return str(qt_dir) @@ -822,13 +823,17 @@ def install_qt6() -> str: qt_arch, "-O", str(qt_install_root), + # Keep the install lean by filtering archives: qtbase provides + # Core/Gui/Widgets (and the Qt6::CorePrivate target), qtsvg provides + # Qt6::Svg. Both are base-Qt archives, not add-on modules. "--archives", "icu", "qtbase", + "qtsvg", ] ) - if not qt_config.exists() or not qt_core.exists(): + if not (qt_config.exists() and qt_core.exists() and qt_svg.exists()): raise RuntimeError(f"Qt6 installation did not produce a usable Qt at {qt_dir}.") return str(qt_dir) diff --git a/src/bonsaiviewer/CMakeLists.txt b/src/bonsaiviewer/CMakeLists.txt index 280c339c01..8ec1b648c5 100644 --- a/src/bonsaiviewer/CMakeLists.txt +++ b/src/bonsaiviewer/CMakeLists.txt @@ -21,7 +21,15 @@ message("Running CMakeLists.txt in /src/bonsaiviewer") set(QT_VERSION 6 CACHE STRING "Qt version") -find_package(Qt${QT_VERSION} COMPONENTS Core CorePrivate Gui Widgets Svg REQUIRED PATHS ${QT_DIR}) +# Qt6::CorePrivate is exposed differently across Qt versions: Qt 6.8 ships the +# target inside Qt6Core (no CorePrivate config package), while Qt 6.10 provides +# it only as a separate CorePrivate package. OPTIONAL_COMPONENTS finds that +# package where it exists without failing where it does not; the Qt6::CorePrivate +# target ends up available either way for the link step below. +find_package(Qt${QT_VERSION} REQUIRED + COMPONENTS Core Gui Widgets Svg + OPTIONAL_COMPONENTS CorePrivate + PATHS ${QT_DIR}) set(BONSAIVIEWER_FILES ${CMAKE_CURRENT_SOURCE_DIR}/main.cpp diff --git a/win/build-all-win.py b/win/build-all-win.py index 5a04134153..165a8b40b4 100644 --- a/win/build-all-win.py +++ b/win/build-all-win.py @@ -32,6 +32,10 @@ assert Path.cwd() == Path(__file__).parent, "Run this script from the 'win' dire PYTHON_VERSIONS = ["3.10.3", "3.11.8", "3.12.1", "3.13.0", "3.14.0"] REPO_PATH = Path(__file__).parent.parent REPO_WIN = REPO_PATH / "win" +# Prebuilt Autodesk connector folder, produced by the connector's packaging +# build.py. The CI workflow builds it before invoking this script; it gets +# bundled next to BonsaiViewer.exe so the viewer can discover it at runtime. +CONNECTOR_DIR = REPO_PATH / "src" / "bonsaiviewer-autodesk" / "dist" / "autodesk" VERSION = (REPO_PATH / "VERSION").read_text().strip() if "GITHUB_SHA" in os.environ: SHA = os.environ["GITHUB_SHA"][:7] @@ -163,6 +167,21 @@ def collect_qt_deployment_files(install_dir: Path) -> dict[str, Path]: return files +def collect_connector_files() -> dict[str, Path]: + """Map the prebuilt Autodesk connector to ``connectors/autodesk/`` arcnames.""" + if not CONNECTOR_DIR.is_dir(): + raise RuntimeError( + f"Autodesk connector not found at {CONNECTOR_DIR}. Build it first with: " + "python src/bonsaiviewer-autodesk/packaging/build.py" + ) + files: dict[str, Path] = {} + for file in CONNECTOR_DIR.rglob("*"): + if file.is_file(): + arcname = Path("connectors") / "autodesk" / file.relative_to(CONNECTOR_DIR) + files[arcname.as_posix()] = file + return files + + def write_zip(zip_path: Path, files: dict[str, Path], generated_files: dict[str, str] | None = None) -> None: zip_path.parent.mkdir(exist_ok=True) with ZipFile(zip_path, "w", compression=zipfile.ZIP_DEFLATED) as zipf: @@ -190,7 +209,7 @@ def build() -> None: "-DENABLE_BUILD_OPTIMIZATIONS=ON", "-DGLTF_SUPPORT=ON", "-DBUILD_EXAMPLES=OFF", - "-DBUILD_IFCVIEWER=ON", + "-DBUILD_BONSAIVIEWER=ON", ] ) restore_env(*OLD_ADD_COMMIT_SHA) @@ -232,6 +251,10 @@ def archive_executables() -> None: files[arcname] = dependency generated_files["qt.conf"] = QT_CONF + # Bundle the Autodesk connector next to the Bonsai Viewer executable. + if file.stem == "BonsaiViewer": + files.update(collect_connector_files()) + zip_name = ZIP_TEMPLATE.format(package_name=file.stem) write_zip(OUTPUT_DIR / zip_name, files, generated_files) print(f"{file} -> {zip_name}") diff --git a/win/build-deps.cmd b/win/build-deps.cmd index fa34b3803d..1e9121f600 100644 --- a/win/build-deps.cmd +++ b/win/build-deps.cmd @@ -804,11 +804,11 @@ IF DEFINED QT6_HOST_INSTALL_DIR ( ) set QT6_TARGET_INSTALLED=FALSE -IF EXIST "%QT6_INSTALL_DIR%\lib\cmake\Qt6\Qt6Config.cmake" IF EXIST "%QT6_INSTALL_DIR%\bin\%QT6_CONFIG_DLL%" set QT6_TARGET_INSTALLED=TRUE +IF EXIST "%QT6_INSTALL_DIR%\lib\cmake\Qt6\Qt6Config.cmake" IF EXIST "%QT6_INSTALL_DIR%\bin\%QT6_CONFIG_DLL%" IF EXIST "%QT6_INSTALL_DIR%\lib\cmake\Qt6Svg\Qt6SvgConfig.cmake" set QT6_TARGET_INSTALLED=TRUE set QT6_HOST_INSTALLED=TRUE IF DEFINED QT6_HOST_INSTALL_DIR ( set QT6_HOST_INSTALLED=FALSE - IF EXIST "%QT6_HOST_INSTALL_DIR%\lib\cmake\Qt6\Qt6Config.cmake" IF EXIST "%QT6_HOST_INSTALL_DIR%\bin\moc.exe" IF EXIST "%QT6_HOST_INSTALL_DIR%\bin\rcc.exe" set QT6_HOST_INSTALLED=TRUE + IF EXIST "%QT6_HOST_INSTALL_DIR%\lib\cmake\Qt6\Qt6Config.cmake" IF EXIST "%QT6_HOST_INSTALL_DIR%\bin\moc.exe" IF EXIST "%QT6_HOST_INSTALL_DIR%\bin\rcc.exe" IF EXIST "%QT6_HOST_INSTALL_DIR%\lib\cmake\Qt6Svg\Qt6SvgConfig.cmake" set QT6_HOST_INSTALLED=TRUE ) IF "%QT6_TARGET_INSTALLED%"=="TRUE" IF "%QT6_HOST_INSTALLED%"=="TRUE" ( @@ -825,13 +825,18 @@ IF "%IFCOS_INSTALL_PYTHON%"=="TRUE" IF EXIST "%PYTHONHOME%\python.exe" set AQT_P IF NOT %ERRORLEVEL%==0 GOTO :Error IF NOT "%QT6_TARGET_INSTALLED%"=="TRUE" ( - %AQT_PYTHON% -m aqt install-qt windows desktop %QT6_VERSION% %QT6_ARCH% -O "%QT6_AQT_OUTPUT_DIR%" --archives qtbase + REM Keep the install lean by filtering archives: qtbase provides + REM Core/Gui/Widgets (and the Qt6::CorePrivate target), qtsvg provides + REM Qt6::Svg. Both are base-Qt archives, not add-on modules. + %AQT_PYTHON% -m aqt install-qt windows desktop %QT6_VERSION% %QT6_ARCH% -O "%QT6_AQT_OUTPUT_DIR%" --archives qtbase qtsvg IF ERRORLEVEL 1 GOTO :Error ) IF DEFINED QT6_HOST_INSTALL_DIR ( IF NOT "%QT6_HOST_INSTALLED%"=="TRUE" ( - %AQT_PYTHON% -m aqt install-qt windows desktop %QT6_VERSION% %QT6_HOST_ARCH% -O "%QT6_HOST_AQT_OUTPUT_DIR%" --archives qtbase + REM windeployqt runs from the host Qt when cross-compiling ARM64, so the + REM host Qt needs qtsvg too to deploy the Bonsai Viewer's Qt6Svg dependency. + %AQT_PYTHON% -m aqt install-qt windows desktop %QT6_VERSION% %QT6_HOST_ARCH% -O "%QT6_HOST_AQT_OUTPUT_DIR%" --archives qtbase qtsvg IF ERRORLEVEL 1 GOTO :Error ) ) @@ -846,6 +851,11 @@ IF NOT EXIST "%QT6_INSTALL_DIR%\bin\%QT6_CONFIG_DLL%" ( GOTO :Error ) +IF NOT EXIST "%QT6_INSTALL_DIR%\lib\cmake\Qt6Svg\Qt6SvgConfig.cmake" ( + call cecho.cmd 0 12 "Qt6 installation did not produce the Qt6 Svg module at %QT6_INSTALL_DIR%." + GOTO :Error +) + IF DEFINED QT6_HOST_INSTALL_DIR ( IF NOT EXIST "%QT6_HOST_INSTALL_DIR%\lib\cmake\Qt6\Qt6Config.cmake" ( call cecho.cmd 0 12 "Qt6 host installation did not produce Qt6Config.cmake at %QT6_HOST_INSTALL_DIR%."