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%."