From 94a94a9848b2f10291087618fe2f36c867d4666e Mon Sep 17 00:00:00 2001 From: Callan Barrett Date: Fri, 3 Jul 2026 09:30:35 +0800 Subject: [PATCH 1/2] Add ARM64 cross-build support --- AGENTS.md | 1 + Dockerfile.arm64 | 70 +++++++ Dockerfile.toolchain.arm64 | 311 +++++++++++++++++++++++++++++++ cmake/ZaparooRust.cmake | 42 ++++- docs/building.md | 47 ++++- justfile | 3 + rust/.cargo/config.toml | 4 + scripts/build-arm64.sh | 93 +++++++++ scripts/build-toolchain-arm64.sh | 53 ++++++ scripts/toolchain-arm64/VERSION | 1 + src/app/main.cpp | 6 + 11 files changed, 619 insertions(+), 12 deletions(-) create mode 100644 Dockerfile.arm64 create mode 100644 Dockerfile.toolchain.arm64 create mode 100755 scripts/build-arm64.sh create mode 100755 scripts/build-toolchain-arm64.sh create mode 100644 scripts/toolchain-arm64/VERSION diff --git a/AGENTS.md b/AGENTS.md index c7abf086..ad04787b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -25,6 +25,7 @@ raw cargo as the default path; the justfile carries the expected environment. | Rust lint only | `just lint-rust` | | Format | `just fmt` | | MiSTer ARM32 build | `just arm32` | +| ARM64 build | `just arm64` | | Deploy to MiSTer | `just deploy-mister` | `just --list` is the source of truth. `CMakePresets.json` and diff --git a/Dockerfile.arm64 b/Dockerfile.arm64 new file mode 100644 index 00000000..90affa00 --- /dev/null +++ b/Dockerfile.arm64 @@ -0,0 +1,70 @@ +# syntax=docker/dockerfile:1.6 +# Zaparoo Frontend +# Copyright (c) 2026 Wizzo Pty Ltd and the Zaparoo Project contributors. +# SPDX-License-Identifier: LicenseRef-PolyForm-Noncommercial-1.0.0 +# +# Cross-compiles the zaparoo-frontend application for Linux arm64. +# The toolchain image is built by Dockerfile.toolchain.arm64 and contains Qt, +# the aarch64 cross compiler, Rust, and cargo-chef. + +ARG TOOLCHAIN_IMAGE=zaparoo/qt6-arm64-toolchain:unset + +FROM ${TOOLCHAIN_IMAGE} AS chef +WORKDIR /src + +FROM chef AS planner +COPY rust-toolchain.toml ./ +COPY rust/ rust/ +RUN cd rust && cargo chef prepare --recipe-path /recipe.json + +FROM chef AS app-build + +ARG ZAPAROO_OFFICIAL_BUILD= +ARG ZAPAROO_BUILD_COMMIT= +ARG ZAPAROO_BUILD_DATE= + +COPY rust-toolchain.toml ./ +COPY --from=planner /recipe.json /recipe.json +RUN mkdir -p rust && cd rust && \ + target_hash="$(printf '%s' "/src/rust/Cargo.toml" | sha1sum | cut -c1-5)" && \ + env QMAKE=/opt/qt6-arm64/bin/qmake6 \ + CORROSION_BUILD_DIR=/src/build \ + "CXX_aarch64_unknown_linux_gnu=/usr/bin/aarch64-linux-gnu-g++" \ + "CARGO_TARGET_DIR=/src/build/cargo/rust_${target_hash}" \ + cargo chef cook --release \ + --target aarch64-unknown-linux-gnu \ + --package zaparoo-frontend-rs \ + --no-default-features \ + --recipe-path /recipe.json + +COPY CMakeLists.txt ./ +COPY cmake/ cmake/ +COPY src/ src/ +COPY resources/ resources/ +COPY rust/ rust/ + +RUN mkdir -p build && cd build && \ + if [ -n "${ZAPAROO_OFFICIAL_BUILD}" ]; then \ + export ZAPAROO_OFFICIAL_BUILD="${ZAPAROO_OFFICIAL_BUILD}"; \ + fi && \ + if [ -n "${ZAPAROO_BUILD_COMMIT}" ]; then \ + export ZAPAROO_BUILD_COMMIT="${ZAPAROO_BUILD_COMMIT}"; \ + fi && \ + if [ -n "${ZAPAROO_BUILD_DATE}" ]; then \ + export ZAPAROO_BUILD_DATE="${ZAPAROO_BUILD_DATE}"; \ + fi && \ + if [ ! -f build.ninja ]; then \ + /opt/qt6-arm64/bin/qt-cmake .. \ + -G Ninja \ + -DCMAKE_BUILD_TYPE=Release \ + -DBUILD_SHARED_LIBS=OFF \ + -DZAPAROO_BUILD_TESTS=OFF \ + -DZAPAROO_EMBEDDED=ON \ + -DZAPAROO_WITH_UPDATE=OFF \ + -DQt6LinguistTools_DIR=/opt/qt6-host/lib/cmake/Qt6LinguistTools; \ + fi \ + && ninja \ + && cp bin/frontend /src/frontend-arm64-built + +FROM scratch AS export +COPY --from=app-build /src/frontend-arm64-built /frontend-arm64 diff --git a/Dockerfile.toolchain.arm64 b/Dockerfile.toolchain.arm64 new file mode 100644 index 00000000..d8d17021 --- /dev/null +++ b/Dockerfile.toolchain.arm64 @@ -0,0 +1,311 @@ +# syntax=docker/dockerfile:1.6 +# Zaparoo Frontend +# Copyright (c) 2026 Wizzo Pty Ltd and the Zaparoo Project contributors. +# SPDX-License-Identifier: LicenseRef-PolyForm-Noncommercial-1.0.0 +# +# Builds the reusable Qt + Linux arm64 toolchain base image. +# This mirrors the MiSTer ARM32 toolchain flow, but targets aarch64 Linux +# with EGLFS/GBM + linuxfb so the same binary can be smoke-tested on KMS/GBM +# or software framebuffer paths. +# +# Qt version pinned here — bump this line when updating Qt: +ARG QT_VERSION=6.10.3 + +FROM debian:bookworm-slim AS builder + +ENV DEBIAN_FRONTEND=noninteractive + +RUN dpkg --add-architecture arm64 && apt-get update && apt-get install -y --no-install-recommends \ + build-essential \ + ca-certificates \ + cmake \ + g++-aarch64-linux-gnu \ + gcc-aarch64-linux-gnu \ + git \ + ninja-build \ + perl \ + pkg-config \ + python3 \ + wget \ + xz-utils \ + libfontconfig1-dev \ + libfreetype-dev \ + libx11-dev \ + libx11-xcb-dev \ + libxext-dev \ + libxfixes-dev \ + libxi-dev \ + libxrender-dev \ + libxcb1-dev \ + libxcb-cursor-dev \ + libxcb-glx0-dev \ + libxcb-keysyms1-dev \ + libxcb-image0-dev \ + libxcb-shm0-dev \ + libxcb-icccm4-dev \ + libxcb-sync-dev \ + libxcb-xfixes0-dev \ + libxcb-shape0-dev \ + libxcb-randr0-dev \ + libxcb-render-util0-dev \ + libxcb-util-dev \ + libxcb-xinerama0-dev \ + libxcb-xkb-dev \ + libxkbcommon-dev \ + libxkbcommon-x11-dev \ + libgl1-mesa-dev \ + libgles2-mesa-dev \ + libegl1-mesa-dev \ + libssl-dev \ + libdbus-1-dev \ + libglib2.0-dev \ + libicu-dev \ + libpcre2-dev \ + zlib1g-dev \ + libjpeg-dev \ + libpng-dev \ + libdrm-dev:arm64 \ + libegl-dev:arm64 \ + libfontconfig1-dev:arm64 \ + libfreetype-dev:arm64 \ + libgbm-dev:arm64 \ + libgl-dev:arm64 \ + libgles-dev:arm64 \ + libjpeg-dev:arm64 \ + libpng-dev:arm64 \ + libwebp-dev:arm64 \ + libxkbcommon-dev:arm64 \ + zlib1g-dev:arm64 \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /build + +ARG QT_VERSION +RUN git clone --depth 1 --branch v${QT_VERSION} https://github.com/qt/qtbase.git && \ + git clone --depth 1 --branch v${QT_VERSION} https://github.com/qt/qtshadertools.git && \ + git clone --depth 1 --branch v${QT_VERSION} https://github.com/qt/qtdeclarative.git && \ + git clone --depth 1 --branch v${QT_VERSION} https://github.com/qt/qttools.git && \ + git clone --depth 1 --branch v${QT_VERSION} https://github.com/qt/qtwebsockets.git && \ + git clone --depth 1 --branch v${QT_VERSION} https://github.com/qt/qtsvg.git && \ + git clone --depth 1 --branch v${QT_VERSION} https://github.com/qt/qtimageformats.git + +# Host Qt build. Provides moc/rcc/qmlcachegen/lupdate/lrelease for the cross build. +RUN mkdir qtbase-host-build && cd qtbase-host-build && \ + ../qtbase/configure \ + -release \ + -prefix /opt/qt6-host \ + -nomake examples \ + -nomake tests \ + -no-opengl \ + -no-openssl \ + -no-dbus \ + -no-feature-accessibility \ + -no-feature-sql \ + -qt-zlib \ + -qt-libpng \ + -qt-libjpeg \ + -qt-freetype \ + -qt-pcre \ + -qt-harfbuzz \ + && cmake --build . --parallel $(nproc) \ + && cmake --install . + +RUN mkdir qtshadertools-host-build && cd qtshadertools-host-build && \ + /opt/qt6-host/bin/qt-configure-module ../qtshadertools \ + && cmake --build . --parallel $(nproc) \ + && cmake --install . + +RUN mkdir qtdeclarative-host-build && cd qtdeclarative-host-build && \ + /opt/qt6-host/bin/qt-configure-module ../qtdeclarative \ + && cmake --build . --parallel $(nproc) \ + && cmake --install . + +RUN mkdir qttools-host-build && cd qttools-host-build && \ + /opt/qt6-host/bin/qt-configure-module ../qttools \ + -- \ + -DFEATURE_linguist=ON \ + -DFEATURE_assistant=OFF \ + -DFEATURE_designer=OFF \ + -DFEATURE_qtattributionsscanner=OFF \ + -DFEATURE_qdbus=OFF \ + -DFEATURE_qtdiag=OFF \ + -DFEATURE_qtplugininfo=OFF \ + && cmake --build . --parallel $(nproc) \ + && cmake --install . + +RUN mkdir qtwebsockets-host-build && cd qtwebsockets-host-build && \ + /opt/qt6-host/bin/qt-configure-module ../qtwebsockets \ + && cmake --build . --parallel $(nproc) \ + && cmake --install . + +RUN mkdir qtsvg-host-build && cd qtsvg-host-build && \ + /opt/qt6-host/bin/qt-configure-module ../qtsvg \ + && cmake --build . --parallel $(nproc) \ + && cmake --install . + +RUN mkdir qtimageformats-host-build && cd qtimageformats-host-build && \ + /opt/qt6-host/bin/qt-configure-module ../qtimageformats \ + -feature-webp \ + -no-feature-jasper \ + -no-feature-mng \ + -no-feature-tiff \ + && cmake --build . --parallel $(nproc) \ + && cmake --install . + +# Debian multiarch aarch64 toolchain. Use compiler default sysroot and target pkg-config dirs. +COPY <<'EOF' /build/arm64-toolchain.cmake +set(CMAKE_SYSTEM_NAME Linux) +set(CMAKE_SYSTEM_PROCESSOR aarch64) + +set(CMAKE_C_COMPILER aarch64-linux-gnu-gcc) +set(CMAKE_CXX_COMPILER aarch64-linux-gnu-g++) +set(CMAKE_AR aarch64-linux-gnu-ar) +set(CMAKE_RANLIB aarch64-linux-gnu-ranlib) +set(CMAKE_STRIP aarch64-linux-gnu-strip) + +set(CMAKE_FIND_ROOT_PATH /usr/aarch64-linux-gnu /usr) +set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) +set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY BOTH) +set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE BOTH) +set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE BOTH) + +set(CMAKE_C_FLAGS_INIT "-mcpu=cortex-a53") +set(CMAKE_CXX_FLAGS_INIT "-mcpu=cortex-a53") +EOF + +ENV PKG_CONFIG_LIBDIR=/usr/lib/aarch64-linux-gnu/pkgconfig:/usr/share/pkgconfig +ENV PKG_CONFIG_SYSROOT_DIR=/ + +# Target Qt for ARM64 Linux: static Qt, dynamic system EGL/GLES/GBM/libdrm. +RUN mkdir qtbase-arm64-build && cd qtbase-arm64-build && \ + ../qtbase/configure \ + -release \ + -static \ + -prefix /opt/qt6-arm64 \ + -qt-host-path /opt/qt6-host \ + -nomake examples \ + -nomake tests \ + -opengl es2 \ + -eglfs \ + -linuxfb \ + -no-openssl \ + -no-dbus \ + -no-icu \ + -no-feature-accessibility \ + -no-feature-printsupport \ + -no-feature-sql \ + -no-feature-concurrent \ + -no-feature-brotli \ + -no-feature-libinput \ + -no-feature-libudev \ + -qt-zlib \ + -qt-libpng \ + -qt-libjpeg \ + -qt-freetype \ + -qt-pcre \ + -qt-harfbuzz \ + -- -DCMAKE_TOOLCHAIN_FILE=/build/arm64-toolchain.cmake \ + -DFEATURE_eglfs=ON \ + -DFEATURE_eglfs_gbm=ON \ + -DFEATURE_eglfs_kms=ON \ + && cmake --build . --parallel $(nproc) \ + && cmake --install . + +RUN mkdir qtshadertools-arm64-build && cd qtshadertools-arm64-build && \ + /opt/qt6-arm64/bin/qt-configure-module ../qtshadertools \ + && cmake --build . --parallel $(nproc) \ + && cmake --install . + +RUN mkdir qtdeclarative-arm64-build && cd qtdeclarative-arm64-build && \ + /opt/qt6-arm64/bin/qt-configure-module ../qtdeclarative \ + && cmake --build . --parallel $(nproc) \ + && cmake --install . + +RUN mkdir qtwebsockets-arm64-build && cd qtwebsockets-arm64-build && \ + /opt/qt6-arm64/bin/qt-configure-module ../qtwebsockets \ + && cmake --build . --parallel $(nproc) \ + && cmake --install . + +RUN mkdir qtsvg-arm64-build && cd qtsvg-arm64-build && \ + /opt/qt6-arm64/bin/qt-configure-module ../qtsvg \ + && cmake --build . --parallel $(nproc) \ + && cmake --install . + +RUN mkdir qtimageformats-arm64-build && cd qtimageformats-arm64-build && \ + /opt/qt6-arm64/bin/qt-configure-module ../qtimageformats \ + -feature-webp \ + -no-feature-jasper \ + -no-feature-mng \ + -no-feature-tiff \ + && cmake --build . --parallel $(nproc) \ + && cmake --install . + +# Rust toolchain for cross-compiling frontend-rs to arm64. +RUN wget -qO- https://sh.rustup.rs \ + | sh -s -- -y --default-toolchain stable --profile minimal \ + && /root/.cargo/bin/rustup target add aarch64-unknown-linux-gnu + +ENV PATH="/root/.cargo/bin:${PATH}" + +RUN printf '[target.aarch64-unknown-linux-gnu]\n\ +linker = "aarch64-linux-gnu-gcc"\n\ +\n\ +[env]\n\ +CC_aarch64_unknown_linux_gnu = "aarch64-linux-gnu-gcc"\n\ +CXX_aarch64_unknown_linux_gnu = "aarch64-linux-gnu-g++"\n\ +AR_aarch64_unknown_linux_gnu = "aarch64-linux-gnu-ar"\n\ +' > /root/.cargo/config.toml + +RUN rustup toolchain install 1.96.0 --profile minimal \ + && rustup target add --toolchain 1.96.0 aarch64-unknown-linux-gnu + +RUN cargo install cargo-chef@0.1.77 --locked + +RUN find /opt/qt6-arm64 -name '*.a' \ + -exec aarch64-linux-gnu-strip --strip-debug {} + 2>/dev/null || true +RUN find /opt/qt6-host \( -name '*.a' -o -name '*.so*' \) \ + -exec strip --strip-debug {} + 2>/dev/null || true \ + && find /opt/qt6-host/bin -type f -executable \ + -exec strip --strip-unneeded {} + 2>/dev/null || true + +FROM debian:bookworm-slim AS toolchain-base + +ENV DEBIAN_FRONTEND=noninteractive + +RUN dpkg --add-architecture arm64 && apt-get update && apt-get install -y --no-install-recommends \ + build-essential \ + ca-certificates \ + cmake \ + g++-aarch64-linux-gnu \ + gcc-aarch64-linux-gnu \ + git \ + libglib2.0-0 \ + ninja-build \ + pkg-config \ + python3 \ + xz-utils \ + libdrm-dev:arm64 \ + libegl-dev:arm64 \ + libfontconfig1-dev:arm64 \ + libfreetype-dev:arm64 \ + libgbm-dev:arm64 \ + libgl-dev:arm64 \ + libgles-dev:arm64 \ + libjpeg-dev:arm64 \ + libpng-dev:arm64 \ + libwebp-dev:arm64 \ + libxkbcommon-dev:arm64 \ + zlib1g-dev:arm64 \ + && rm -rf /var/lib/apt/lists/* + +COPY --from=builder /opt/qt6-host /opt/qt6-host +COPY --from=builder /opt/qt6-arm64 /opt/qt6-arm64 +COPY --from=builder /root/.cargo /root/.cargo +COPY --from=builder /root/.rustup /root/.rustup +COPY --from=builder /build/arm64-toolchain.cmake /build/arm64-toolchain.cmake + +ENV PATH="/root/.cargo/bin:${PATH}" +ENV PKG_CONFIG_LIBDIR=/usr/lib/aarch64-linux-gnu/pkgconfig:/usr/share/pkgconfig +ENV PKG_CONFIG_SYSROOT_DIR=/ + +WORKDIR /build diff --git a/cmake/ZaparooRust.cmake b/cmake/ZaparooRust.cmake index 0b421473..17195c26 100644 --- a/cmake/ZaparooRust.cmake +++ b/cmake/ZaparooRust.cmake @@ -12,15 +12,20 @@ include_guard(GLOBAL) include(FetchContent) -# When cross-compiling for MiSTer ARM32, tell Corrosion the Rust target triple explicitly. -# Corrosion's mapping from CMAKE_SYSTEM_PROCESSOR="arm" is ambiguous; MiSTer is ARMv7 hard-float -# (armv7-unknown-linux-gnueabihf). -if(CMAKE_CROSSCOMPILING AND CMAKE_SYSTEM_PROCESSOR STREQUAL "arm") - if(NOT Rust_CARGO_TARGET) +# Tell Corrosion the Rust target triple explicitly for cross-builds. CMake's +# processor names are not enough for Rust target selection: MiSTer is ARMv7 +# hard-float, while ARM64 builds use aarch64 Linux. +if(CMAKE_CROSSCOMPILING AND NOT Rust_CARGO_TARGET) + if(CMAKE_SYSTEM_PROCESSOR STREQUAL "arm") set(Rust_CARGO_TARGET "armv7-unknown-linux-gnueabihf" CACHE STRING "Cargo target triple for ARM32 cross-build" FORCE ) + elseif(CMAKE_SYSTEM_PROCESSOR STREQUAL "aarch64") + set(Rust_CARGO_TARGET + "aarch64-unknown-linux-gnu" + CACHE STRING "Cargo target triple for arm64 cross-build" FORCE + ) endif() endif() @@ -43,17 +48,21 @@ if(ZAPAROO_WITH_UPDATE) endif() # ── Environment variables for cxx_qt_build's build.rs ─────────────────────── QMAKE: cxx_qt_build -# (via qt-build-utils) uses qmake to locate Qt headers and libraries. For ARM32 cross-builds the -# system qmake points to x86_64 Qt; override with the cross-compiled qmake. +# (via qt-build-utils) uses qmake to locate Qt headers and libraries. For static +# cross-builds the system qmake points to x86_64 Qt; override with the target qmake. get_target_property(_rs_qt6_core_type Qt6::Core TYPE) if(_rs_qt6_core_type STREQUAL "STATIC_LIBRARY") - set(_rs_qmake "/opt/qt6-arm32/bin/qmake6") + if(CMAKE_SYSTEM_PROCESSOR STREQUAL "aarch64") + set(_rs_qmake "/opt/qt6-arm64/bin/qmake6") + else() + set(_rs_qmake "/opt/qt6-arm32/bin/qmake6") + endif() else() find_program(_rs_qmake NAMES qmake6 qmake REQUIRED) endif() corrosion_set_env_vars(zaparoo_frontend_rs "QMAKE=${_rs_qmake}") -if(_rs_qt6_core_type STREQUAL "STATIC_LIBRARY") +if(_rs_qt6_core_type STREQUAL "STATIC_LIBRARY" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "arm") corrosion_set_env_vars(zaparoo_frontend_rs "ZAPAROO_RUNTIME=mister") endif() if(ZAPAROO_DEV) @@ -104,6 +113,8 @@ if(_rs_qt6_core_type STREQUAL "STATIC_LIBRARY") include("${_rs_config}" OPTIONAL) endforeach() include("${_rs_qt_prefix}/lib/cmake/Qt6Gui/Qt6QLinuxFbIntegrationPluginConfig.cmake" OPTIONAL) + include("${_rs_qt_prefix}/lib/cmake/Qt6Gui/Qt6QEglFSIntegrationPluginConfig.cmake" OPTIONAL) + include("${_rs_qt_prefix}/lib/cmake/Qt6Gui/Qt6QEglFSKmsGbmIntegrationPluginConfig.cmake" OPTIONAL) include("${_rs_qt_prefix}/lib/cmake/Qt6Gui/Qt6QWebpPluginConfig.cmake" OPTIONAL) foreach(_rs_qml_plugin IN ITEMS qtquickcontrols2plugin qtquickcontrols2basicstyleplugin qtquickcontrols2implplugin qtquicktemplates2plugin quickwindow @@ -155,6 +166,19 @@ if(_rs_qt6_core_type STREQUAL "STATIC_LIBRARY") frontend PRIVATE Qt6::QLinuxFbIntegrationPlugin Qt6::QLinuxFbIntegrationPlugin_init ) endif() + if(TARGET Qt6::QEglFSIntegrationPlugin) + target_link_libraries( + frontend PRIVATE Qt6::QEglFSIntegrationPlugin Qt6::QEglFSIntegrationPlugin_init + ) + target_compile_definitions(frontend PRIVATE ZAPAROO_STATIC_EGLFS_PLUGIN) + endif() + if(TARGET Qt6::QEglFSKmsGbmIntegrationPlugin) + target_link_libraries( + frontend PRIVATE Qt6::QEglFSKmsGbmIntegrationPlugin + Qt6::QEglFSKmsGbmIntegrationPlugin_init + ) + target_compile_definitions(frontend PRIVATE ZAPAROO_STATIC_EGLFS_KMS_GBM_PLUGIN) + endif() if(TARGET Qt6::QSvgPlugin) target_link_libraries(frontend PRIVATE Qt6::QSvgPlugin Qt6::QSvgPlugin_init) endif() diff --git a/docs/building.md b/docs/building.md index ea9c887d..80f5e558 100644 --- a/docs/building.md +++ b/docs/building.md @@ -52,13 +52,13 @@ just install-tools If `just` isn't packaged for your distro, install it the same way: `cargo install --locked just`. -### MiSTer ARM32 cross-build +### MiSTer ARM32 / ARM64 cross-builds - Docker with Buildx (Docker Desktop includes it) - x86_64 Linux Docker platform (`linux/amd64`) -- ~5 GB disk space for the toolchain image +- ~5 GB disk space for each toolchain image -The toolchain Docker image provides the ARM build environment. Cargo still gets +The toolchain Docker images provide the ARM build environments. Cargo still gets its target and linker settings from `rust/.cargo/config.toml`; the desktop `mold` linker setting lives there too. You should not need to edit Cargo config by hand. @@ -146,6 +146,47 @@ file output/frontend # Should report: ELF 32-bit LSB executable, ARM, EABI5 ... ``` +## ARM64 cross-build + +The ARM64 target mirrors the MiSTer Docker flow but uses a separate Qt + +aarch64 toolchain image with static Qt, EGLFS/GBM, and linuxfb enabled: + +```bash +./scripts/build-arm64.sh +# or +just arm64 +``` + +This pulls +`ghcr.io/zaparooproject/qt6-arm64-toolchain:` if it is +not cached locally, builds the application in Docker, and writes the arm64 binary to +`output/frontend-arm64`. It does not require host Qt, CMake, Rust, or an aarch64 toolchain. + +If the official image is unavailable, build and use the local toolchain image: + +```bash +USE_LOCAL_TOOLCHAIN=1 ./scripts/build-arm64.sh +``` + +The arm64 build sets `ZAPAROO_WITH_UPDATE=OFF`. Platform-specific update mechanisms +own updates, so the optional zaparoo-update integration is intentionally not bundled into +this artifact. + +At runtime, set the Qt platform from the service/wrapper, for example: + +```bash +QT_QPA_PLATFORM=eglfs +QT_QPA_EGLFS_INTEGRATION=eglfs_kms +QT_QUICK_BACKEND=software # fallback only; omit when using Qt Quick via GLES +``` + +Check the ARM64 binary: + +```bash +file output/frontend-arm64 +# Should report: ELF 64-bit LSB executable, ARM aarch64 ... +``` + ## Tests ```bash diff --git a/justfile b/justfile index 75738850..1124b04d 100644 --- a/justfile +++ b/justfile @@ -54,6 +54,9 @@ build-san: arm32: ./scripts/build-arm32.sh +arm64: + ./scripts/build-arm64.sh + # --- run --- run *args: build ./build/bin/frontend {{args}} diff --git a/rust/.cargo/config.toml b/rust/.cargo/config.toml index 93a7d419..398ce739 100644 --- a/rust/.cargo/config.toml +++ b/rust/.cargo/config.toml @@ -14,3 +14,7 @@ rustflags = ["-C", "link-arg=-fuse-ld=mold"] # CMAKE_TOOLCHAIN_FILE points to arm32-toolchain.cmake. [target.armv7-unknown-linux-gnueabihf] linker = "arm-none-linux-gnueabihf-gcc" + +# ARM64: used inside the Docker toolchain image. +[target.aarch64-unknown-linux-gnu] +linker = "aarch64-linux-gnu-gcc" diff --git a/scripts/build-arm64.sh b/scripts/build-arm64.sh new file mode 100755 index 00000000..4a0a503d --- /dev/null +++ b/scripts/build-arm64.sh @@ -0,0 +1,93 @@ +#!/bin/bash +# Zaparoo Frontend +# Copyright (c) 2026 Wizzo Pty Ltd and the Zaparoo Project contributors. +# SPDX-License-Identifier: LicenseRef-PolyForm-Noncommercial-1.0.0 +# +# Cross-compiles the frontend for Linux arm64 using Docker. +# Uses a pinned Qt + aarch64 cross toolchain image. The build writes +# output/frontend-arm64. +# +# Set USE_LOCAL_TOOLCHAIN=1 to build/use the local toolchain image from +# Dockerfile.toolchain.arm64 if the published image is not available. + +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(dirname "$SCRIPT_DIR")" +OUTPUT_DIR="${PROJECT_ROOT}/output" +VERSION_FILE="${PROJECT_ROOT}/scripts/toolchain-arm64/VERSION" +DOCKER_PLATFORM="${DOCKER_PLATFORM:-linux/amd64}" +if [ ! -f "${VERSION_FILE}" ]; then + echo "Error: arm64 toolchain version file not found at ${VERSION_FILE}" >&2 + echo " (PROJECT_ROOT=${PROJECT_ROOT})" >&2 + exit 1 +fi +TOOLCHAIN_VERSION="$(tr -d '[:space:]' < "${VERSION_FILE}")" +if ! printf '%s' "${TOOLCHAIN_VERSION}" | grep -Eq '^[A-Za-z0-9_][A-Za-z0-9_.-]{0,127}$'; then + echo "Error: invalid arm64 toolchain version in ${VERSION_FILE}" >&2 + echo " raw value: '${TOOLCHAIN_VERSION}'" >&2 + echo " expected: Docker tag [A-Za-z0-9_][A-Za-z0-9_.-]{0,127}" >&2 + exit 1 +fi +if ! docker buildx version > /dev/null 2>&1; then + echo "Error: Docker Buildx is required for the arm64 application build." >&2 + echo " Docker Desktop includes Buildx." >&2 + exit 1 +fi +LOCAL_TOOLCHAIN_IMAGE="zaparoo/qt6-arm64-toolchain:${TOOLCHAIN_VERSION}" +OFFICIAL_TOOLCHAIN_IMAGE="ghcr.io/zaparooproject/qt6-arm64-toolchain:${TOOLCHAIN_VERSION}" + +if [ -z "${TOOLCHAIN_IMAGE:-}" ]; then + if [ "${USE_LOCAL_TOOLCHAIN:-0}" = "1" ]; then + TOOLCHAIN_IMAGE="${LOCAL_TOOLCHAIN_IMAGE}" + else + TOOLCHAIN_IMAGE="${OFFICIAL_TOOLCHAIN_IMAGE}" + fi +fi + +if [[ "${TOOLCHAIN_IMAGE}" == "${OFFICIAL_TOOLCHAIN_IMAGE}" ]] \ + && ! docker image inspect "${TOOLCHAIN_IMAGE}" > /dev/null 2>&1 \ + && ! docker manifest inspect "${TOOLCHAIN_IMAGE}" > /dev/null 2>&1; then + echo "Error: official arm64 toolchain image is not available: ${TOOLCHAIN_IMAGE}" >&2 + echo " If GHCR requires auth, run:" >&2 + echo " gh auth refresh -h github.com -s read:packages" >&2 + echo " gh auth token | docker login ghcr.io -u --password-stdin" >&2 + echo " To build the toolchain locally instead, run:" >&2 + echo " USE_LOCAL_TOOLCHAIN=1 ./scripts/build-arm64.sh" >&2 + exit 1 +fi + +if [[ "${TOOLCHAIN_IMAGE}" == "${LOCAL_TOOLCHAIN_IMAGE}" ]] \ + && ! docker image inspect "${TOOLCHAIN_IMAGE}" > /dev/null 2>&1; then + echo "Toolchain image '${TOOLCHAIN_IMAGE}' not found locally." + echo "Building it now..." + "${SCRIPT_DIR}/build-toolchain-arm64.sh" +fi + +echo "=== Cross-compiling frontend for Linux arm64 ===" +echo "Using toolchain image: ${TOOLCHAIN_IMAGE}" +echo "Docker platform: ${DOCKER_PLATFORM}" +mkdir -p "${OUTPUT_DIR}" + +ZAPAROO_BUILD_COMMIT="${ZAPAROO_BUILD_COMMIT:-$(git -C "${PROJECT_ROOT}" rev-parse --short=7 HEAD 2>/dev/null || true)}" +ZAPAROO_BUILD_DATE="${ZAPAROO_BUILD_DATE:-$(date -u +%Y-%m-%d)}" + +docker buildx build \ + --platform "${DOCKER_PLATFORM}" \ + -f "${PROJECT_ROOT}/Dockerfile.arm64" \ + --build-arg "TOOLCHAIN_IMAGE=${TOOLCHAIN_IMAGE}" \ + --build-arg "ZAPAROO_OFFICIAL_BUILD=${ZAPAROO_OFFICIAL_BUILD:-}" \ + --build-arg "ZAPAROO_BUILD_COMMIT=${ZAPAROO_BUILD_COMMIT}" \ + --build-arg "ZAPAROO_BUILD_DATE=${ZAPAROO_BUILD_DATE}" \ + --output "type=local,dest=${OUTPUT_DIR}" \ + --target export \ + "${PROJECT_ROOT}" + +if [ -f "${OUTPUT_DIR}/frontend-arm64" ]; then + echo "" + echo "=== arm64 build successful! ===" + file "${OUTPUT_DIR}/frontend-arm64" +else + echo "Build failed — binary not found in ${OUTPUT_DIR}" + exit 1 +fi diff --git a/scripts/build-toolchain-arm64.sh b/scripts/build-toolchain-arm64.sh new file mode 100755 index 00000000..c70964c8 --- /dev/null +++ b/scripts/build-toolchain-arm64.sh @@ -0,0 +1,53 @@ +#!/bin/bash +# Zaparoo Frontend +# Copyright (c) 2026 Wizzo Pty Ltd and the Zaparoo Project contributors. +# SPDX-License-Identifier: LicenseRef-PolyForm-Noncommercial-1.0.0 +# +# Build the Qt + Linux arm64 toolchain base image. Qt upstream version is pinned +# in Dockerfile.toolchain.arm64; the image tag is read from +# scripts/toolchain-arm64/VERSION so CI/local builds share one source of truth. +# Run this only when the arm64 toolchain image is missing or needs a version bump. + +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(dirname "$SCRIPT_DIR")" +VERSION_FILE="${PROJECT_ROOT}/scripts/toolchain-arm64/VERSION" +DOCKER_PLATFORM="${DOCKER_PLATFORM:-linux/amd64}" +if [ ! -f "${VERSION_FILE}" ]; then + echo "Error: arm64 toolchain version file not found at ${VERSION_FILE}" >&2 + echo " (PROJECT_ROOT=${PROJECT_ROOT})" >&2 + exit 1 +fi +TOOLCHAIN_VERSION="$(tr -d '[:space:]' < "${VERSION_FILE}")" +if ! printf '%s' "${TOOLCHAIN_VERSION}" | grep -Eq '^[A-Za-z0-9_][A-Za-z0-9_.-]{0,127}$'; then + echo "Error: invalid arm64 toolchain version in ${VERSION_FILE}" >&2 + echo " raw value: '${TOOLCHAIN_VERSION}'" >&2 + echo " expected: Docker tag [A-Za-z0-9_][A-Za-z0-9_.-]{0,127}" >&2 + exit 1 +fi +if ! docker buildx version > /dev/null 2>&1; then + echo "Error: Docker Buildx is required for the arm64 toolchain build." >&2 + echo " Docker Desktop includes Buildx." >&2 + exit 1 +fi +IMAGE_TAG="zaparoo/qt6-arm64-toolchain:${TOOLCHAIN_VERSION}" + +echo "=== Building Qt 6.10.3 arm64 toolchain image (${TOOLCHAIN_VERSION}) ===" +echo "Tag: ${IMAGE_TAG}" +echo "Docker platform: ${DOCKER_PLATFORM}" +echo "This can take a while on first run." +echo "" + +docker buildx build \ + --platform "${DOCKER_PLATFORM}" \ + -f "${PROJECT_ROOT}/Dockerfile.toolchain.arm64" \ + -t "${IMAGE_TAG}" \ + --load \ + "${PROJECT_ROOT}" + +echo "" +echo "=== arm64 toolchain image built successfully ===" +echo "Tag: ${IMAGE_TAG}" +echo "" +echo "You can now run ./scripts/build-arm64.sh to build the application." diff --git a/scripts/toolchain-arm64/VERSION b/scripts/toolchain-arm64/VERSION new file mode 100644 index 00000000..31c021db --- /dev/null +++ b/scripts/toolchain-arm64/VERSION @@ -0,0 +1 @@ +6.10.3-2 diff --git a/src/app/main.cpp b/src/app/main.cpp index 70fbc649..f59292c4 100644 --- a/src/app/main.cpp +++ b/src/app/main.cpp @@ -89,6 +89,12 @@ Q_IMPORT_QML_PLUGIN(QtQuickControls2ImplPlugin) Q_IMPORT_QML_PLUGIN(QtQuickTemplates2Plugin) Q_IMPORT_QML_PLUGIN(QtQuick_WindowPlugin) Q_IMPORT_PLUGIN(QLinuxFbIntegrationPlugin) +#ifdef ZAPAROO_STATIC_EGLFS_PLUGIN +Q_IMPORT_PLUGIN(QEglFSIntegrationPlugin) +#endif +#ifdef ZAPAROO_STATIC_EGLFS_KMS_GBM_PLUGIN +Q_IMPORT_PLUGIN(QEglFSKmsGbmIntegrationPlugin) +#endif Q_IMPORT_PLUGIN(QSvgPlugin) #endif From c9510e49b8cc525aa495bebc252817f895ac2e10 Mon Sep 17 00:00:00 2001 From: Callan Barrett Date: Fri, 3 Jul 2026 11:27:31 +0800 Subject: [PATCH 2/2] Address ARM64 build review feedback --- Dockerfile.arm64 | 18 ++++++++---------- Dockerfile.toolchain.arm64 | 5 +++-- cmake/ZaparooRust.cmake | 8 ++++++++ 3 files changed, 19 insertions(+), 12 deletions(-) diff --git a/Dockerfile.arm64 b/Dockerfile.arm64 index 90affa00..4092c529 100644 --- a/Dockerfile.arm64 +++ b/Dockerfile.arm64 @@ -53,16 +53,14 @@ RUN mkdir -p build && cd build && \ if [ -n "${ZAPAROO_BUILD_DATE}" ]; then \ export ZAPAROO_BUILD_DATE="${ZAPAROO_BUILD_DATE}"; \ fi && \ - if [ ! -f build.ninja ]; then \ - /opt/qt6-arm64/bin/qt-cmake .. \ - -G Ninja \ - -DCMAKE_BUILD_TYPE=Release \ - -DBUILD_SHARED_LIBS=OFF \ - -DZAPAROO_BUILD_TESTS=OFF \ - -DZAPAROO_EMBEDDED=ON \ - -DZAPAROO_WITH_UPDATE=OFF \ - -DQt6LinguistTools_DIR=/opt/qt6-host/lib/cmake/Qt6LinguistTools; \ - fi \ + /opt/qt6-arm64/bin/qt-cmake .. \ + -G Ninja \ + -DCMAKE_BUILD_TYPE=Release \ + -DBUILD_SHARED_LIBS=OFF \ + -DZAPAROO_BUILD_TESTS=OFF \ + -DZAPAROO_EMBEDDED=ON \ + -DZAPAROO_WITH_UPDATE=OFF \ + -DQt6LinguistTools_DIR=/opt/qt6-host/lib/cmake/Qt6LinguistTools \ && ninja \ && cp bin/frontend /src/frontend-arm64-built diff --git a/Dockerfile.toolchain.arm64 b/Dockerfile.toolchain.arm64 index d8d17021..2b258d56 100644 --- a/Dockerfile.toolchain.arm64 +++ b/Dockerfile.toolchain.arm64 @@ -242,8 +242,7 @@ RUN mkdir qtimageformats-arm64-build && cd qtimageformats-arm64-build && \ # Rust toolchain for cross-compiling frontend-rs to arm64. RUN wget -qO- https://sh.rustup.rs \ - | sh -s -- -y --default-toolchain stable --profile minimal \ - && /root/.cargo/bin/rustup target add aarch64-unknown-linux-gnu + | sh -s -- -y --default-toolchain none --profile minimal ENV PATH="/root/.cargo/bin:${PATH}" @@ -257,6 +256,7 @@ AR_aarch64_unknown_linux_gnu = "aarch64-linux-gnu-ar"\n\ ' > /root/.cargo/config.toml RUN rustup toolchain install 1.96.0 --profile minimal \ + && rustup default 1.96.0 \ && rustup target add --toolchain 1.96.0 aarch64-unknown-linux-gnu RUN cargo install cargo-chef@0.1.77 --locked @@ -280,6 +280,7 @@ RUN dpkg --add-architecture arm64 && apt-get update && apt-get install -y --no-i gcc-aarch64-linux-gnu \ git \ libglib2.0-0 \ + libicu72 \ ninja-build \ pkg-config \ python3 \ diff --git a/cmake/ZaparooRust.cmake b/cmake/ZaparooRust.cmake index 17195c26..a6125f35 100644 --- a/cmake/ZaparooRust.cmake +++ b/cmake/ZaparooRust.cmake @@ -57,6 +57,14 @@ if(_rs_qt6_core_type STREQUAL "STATIC_LIBRARY") else() set(_rs_qmake "/opt/qt6-arm32/bin/qmake6") endif() + if(NOT EXISTS "${_rs_qmake}") + message( + FATAL_ERROR + "Target qmake not found at ${_rs_qmake}. " + "Expected /opt/qt6-arm64/bin/qmake6 or /opt/qt6-arm32/bin/qmake6 " + "in static Qt cross-build images." + ) + endif() else() find_program(_rs_qmake NAMES qmake6 qmake REQUIRED) endif()