diff --git a/crates/Cross.toml b/crates/Cross.toml index fe5ae7d4a..9016752b7 100644 --- a/crates/Cross.toml +++ b/crates/Cross.toml @@ -1,6 +1,8 @@ # cross (https://github.com/cross-rs/cross) config for cross-compiling the -# aarch64 `qbz` binary from an x86-64 Linux host. Used by -# scripts/build-aarch64-linux.sh (CROSS mode). +# aarch64 `qbzd` daemon from an x86-64 Linux host. Used by +# scripts/build-aarch64-qbzd.sh (CROSS mode). The Qt desktop binary is NOT +# cross-built (cxx-qt needs the target Qt's qmake at build time; see +# scripts/build-aarch64-linux.sh) — build it natively on an ARM box. # # `image` pins a newer Ubuntu (cross 0.2.5's default aarch64 image is Ubuntu # 16.04 "xenial" — EOL, archived repos, OpenSSL 1.0.2 — which breaks modern @@ -20,7 +22,7 @@ image = "ghcr.io/cross-rs/aarch64-unknown-linux-gnu:main" pre-build = [ "dpkg --add-architecture arm64", "apt-get update", - "apt-get install -y --no-install-recommends pkg-config cmake clang libclang-dev nasm libasound2-dev:arm64 libjack-jackd2-dev:arm64 libfontconfig1-dev:arm64 libfreetype6-dev:arm64 libxkbcommon-dev:arm64 libwayland-dev:arm64 libxcb1-dev:arm64 libgl1-mesa-dev:arm64 libegl1-mesa-dev:arm64 libdbus-1-dev:arm64 libssl-dev:arm64", + "apt-get install -y --no-install-recommends pkg-config cmake clang libclang-dev nasm libasound2-dev:arm64 libjack-jackd2-dev:arm64 libdbus-1-dev:arm64 libssl-dev:arm64", ] [target.aarch64-unknown-linux-gnu.env] diff --git a/scripts/build-aarch64-linux.sh b/scripts/build-aarch64-linux.sh index f1982427a..47d741fc3 100755 --- a/scripts/build-aarch64-linux.sh +++ b/scripts/build-aarch64-linux.sh @@ -1,94 +1,124 @@ #!/usr/bin/env bash # -# build-aarch64-linux.sh — build the Linux aarch64 (ARM64) `qbz` binary, e.g. -# for a Raspberry Pi 4 / other ARM single-board boxes. +# build-aarch64-linux.sh — build the Linux aarch64 (ARM64) `qbz` binary (the +# Qt frontend, package `qbz-qt`), e.g. for a Raspberry Pi 4 / other ARM boxes. # -# ── READ FIRST: where can this run? ────────────────────────────────────────── -# The generated Slint crate `qbz_ui` compiles as ONE ~1.6M-line module whose -# single `rustc` needs ~30 GB RAM (RSS+swap). Consequences: -# • An 8 GB Mac or a 4 GB Pi CANNOT build it natively — it OOMs. Don't try. -# • Build on a box with >= ~16 GB RAM + swap (the wall is survivable at -# ~14 GB RAM + swap, proven on the dev box). +# Rewritten 2026-08-25 for the Slint -> Qt frontend switch. There is no memory +# wall any more: `qbz-qt` never pulls the 30 GB `qbz-ui` unit, so a 4 GB Pi +# CAN build it natively (slowly). What the Qt build needs instead is a +# **Qt >= 6.8 with private headers** — the cxx-qt RHI items include +# — and the QtQuick.Effects / Qt.labs.qmlmodels QML modules at +# run time. That is the distro gate: +# Debian 13+ (6.8), Ubuntu 25.04+ (6.8), Fedora 41+ (6.8), Arch/Gentoo: OK +# Ubuntu 24.04 (6.4, no Effects module), Debian 12, Fedora 40: NOT buildable +# with distro Qt — use the CI AppImage (Qt bundled) or aqt on a bigger box. # -# This script has two modes, auto-selected by the host arch: +# Modes, auto-selected by the host arch: # -# 1. NATIVE (running ON aarch64 Linux: an ARM runner, an ARM VM, etc.) -# -> apt-installs the build deps and runs `cargo build --release -p qbz`. -# This is exactly what a future Slint aarch64 CI job runs — treat the -# dep list + build line below as the workflow's source of truth. +# 1. NATIVE (running ON aarch64 Linux: a Pi, an ARM VM, the ARM CI runner) +# -> apt-installs the build deps (Qt from the distro), installs rustup +# if there is no cargo, runs `cargo build --release -p qbz-qt`, then +# prints the glibc floor and boots the binary offscreen (same criterion +# as CI: >= 10 log lines, zero QML complaints, QbzCore initialized). +# This mirrors .github/workflows/build-qt-linux.yml's aarch64 job, with +# distro Qt instead of aqt. # -# 2. CROSS (running on x86-64 Linux with Docker, e.g. the dev box, 30 GB) -# -> uses `cross` (https://github.com/cross-rs/cross): a container with -# the aarch64 sysroot + the arm64 dev libs (see crates/Cross.toml) does -# the compile. Needs Docker running and ~30 GB RAM available to it. +# 2. CROSS (on x86-64 Linux with Docker) — NOT SUPPORTED for the Qt binary. +# cxx-qt-build runs the TARGET Qt's `qmake -query` during the build, +# which needs qemu-user + binfmt_misc on the host, and cross-rs's +# images are Ubuntu 20.04 (Qt 5). A trixie-based cross image is +# possible but untested; until someone builds and proves it, this mode +# exits with that explanation instead of pretending. The qbzd script +# keeps its CROSS mode — the daemon links no Qt. # -# macOS is intentionally NOT a build host here: cross from macOS to -# linux-gnu means re-creating a full Linux sysroot for every native dep -# (ALSA, dbus, fontconfig, …). Run the CROSS mode on a Linux box instead. +# Memory: with <= 4 GB RAM the script forces CARGO_BUILD_JOBS=1 (one rustc at +# a time). Expect 1-3 h on a Pi 4 cold. Override with JOBS=. # # Output: dist/qbz-aarch64-linux (an aarch64 ELF — verify with `file`). # ───────────────────────────────────────────────────────────────────────────── set -euo pipefail -cd "$(dirname "$0")/.." # repo root (qbz-nix) +cd "$(dirname "$0")/.." # repo root REPO="$(pwd)" -TARGET="aarch64-unknown-linux-gnu" OUT="$REPO/dist/qbz-aarch64-linux" +say() { printf '[aarch64-build] %s\n' "$*"; } +die() { say "ERROR: $*" >&2; exit 1; } -# The Linux build deps for the Slint `qbz` binary (kept in sync with the CI -# workflows). PipeWire is reached through its ALSA/JACK compat shims, so only -# libasound2-dev + libjack-jackd2-dev are needed. +# Build deps for the Qt `qbz` binary — the same list as build-qt-linux.yml +# minus what aqt provides there (here the distro's Qt provides it). DEPS=( - build-essential pkg-config cmake clang libclang-dev mold nasm - libasound2-dev libjack-jackd2-dev - libfontconfig1-dev libfreetype-dev - libxkbcommon-dev libwayland-dev libxcb1-dev - libgl1-mesa-dev libegl1-mesa-dev - libdbus-1-dev libssl-dev + build-essential pkg-config cmake clang libclang-dev nasm curl + libasound2-dev libjack-jackd2-dev libdbus-1-dev libssl-dev + libgl1-mesa-dev libegl1-mesa-dev libxkbcommon-dev + qt6-base-dev qt6-base-private-dev + qt6-declarative-dev qt6-declarative-private-dev + qt6-shadertools-dev + qml6-module-qtquick qml6-module-qtquick-controls qml6-module-qtquick-effects + qml6-module-qtquick-window qml6-module-qtqml-models qml6-module-qt-labs-qmlmodels + qt6-wayland qt6-qpa-plugins ) arch="$(uname -m)" case "$arch" in aarch64 | arm64) - echo "[aarch64-build] NATIVE build on $arch" + say "NATIVE build on $arch ($(. /etc/os-release; echo "$PRETTY_NAME"))" if command -v apt-get >/dev/null; then + # The distro gate, checked before installing anything: a Qt below 6.8 + # fails much later with a confusing "non-existent property" at runtime. + qtver="$(apt-cache policy qt6-base-dev | awk '/Candidate/ {print $2}' | grep -oE '^[0-9]+\.[0-9]+' || true)" + [[ -n "$qtver" ]] || die "no qt6-base-dev candidate — this distro has no Qt 6 (see header)" + if [[ "$(printf '%s\n' "$qtver" "6.8" | sort -V | head -1)" != "6.8" ]]; then + die "distro Qt is $qtver; the tree needs >= 6.8 (see header for the distro table)" + fi + say "distro Qt $qtver — OK" sudo apt-get update sudo apt-get install -y --no-install-recommends "${DEPS[@]}" else - echo "[aarch64-build] non-apt distro: install the equivalents of: ${DEPS[*]}" >&2 + say "non-apt distro: install the equivalents of: ${DEPS[*]}" >&2 fi - ( cd crates && cargo build --release -p qbz ) + if ! command -v cargo >/dev/null && [[ ! -x "$HOME/.cargo/bin/cargo" ]]; then + say "no cargo — installing rustup (stable, minimal)" + curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal --default-toolchain stable + fi + export PATH="$HOME/.cargo/bin:$PATH" + rustc -V; cargo -V + + mem_mb="$(awk '/MemTotal/ {print int($2/1024)}' /proc/meminfo)" + if [[ -n "${JOBS:-}" ]]; then jobs="$JOBS" + elif (( mem_mb <= 4500 )); then jobs=1 + else jobs="$(nproc)"; fi + say "RAM ${mem_mb} MB -> CARGO_BUILD_JOBS=${jobs}" + # No RUSTFLAGS, no mold, stable: same cache rule as qt-run.sh. + ( cd crates && CARGO_BUILD_JOBS="$jobs" CARGO_INCREMENTAL=0 cargo build --release -p qbz-qt ) install -Dm755 "crates/target/release/qbz" "$OUT" ;; x86_64 | amd64) - echo "[aarch64-build] CROSS-compile from $arch via cross (Docker)" - if ! docker info >/dev/null 2>&1; then - echo "[aarch64-build] ERROR: Docker is not running. cross needs it." >&2 - exit 1 - fi - command -v cross >/dev/null || cargo install cross --locked - # Cap the container's RAM so a runaway qbz_ui rustc gets OOM-killed - # (losing the build) instead of swap-thrashing the host into a hard freeze - # — the dev box has no hibernation and dies under memory pressure. Override - # via CROSS_CONTAINER_OPTS if your box has more headroom. - # qbz-ui's Slint build imports fonts from the REPO ROOT (../../../../static/ - # fonts/*.ttf) — outside the `crates/` workspace that cross mounts. Mount - # static/ at its host path so those relative imports resolve in the - # container (cross 0.2.x mounts the project at the host path). - # qbz_ui's single rustc demands ~30-33 GB (RSS+swap). --memory-swap must - # exceed that or Docker OOM-kills the build; 48g leaves headroom yet still - # caps a runaway well under the host's 30G RAM + 46G swap (no freeze). RAM - # cap 24g keeps ~6g for the desktop (overflow swaps, doesn't exhaust). - export CROSS_CONTAINER_OPTS="${CROSS_CONTAINER_OPTS:---memory=24g --memory-swap=48g} --volume $REPO/static:$REPO/static:ro" - # Cross.toml (workspace root) injects the arm64 dev libs into the image. - ( cd crates && cross build --release --target "$TARGET" -p qbz ) - install -Dm755 "crates/target/$TARGET/release/qbz" "$OUT" + die "CROSS mode is not supported for the Qt binary (see the header: cxx-qt \ +needs the target Qt's qmake at build time -> qemu-user + a Qt>=6.8 arm64 \ +sysroot). Build natively on an ARM box, or dispatch release-linux-aarch64.yml." ;; *) - echo "[aarch64-build] ERROR: unsupported build host arch: $arch" >&2 - exit 1 + die "unsupported build host arch: $arch" ;; esac -echo "[aarch64-build] done -> $OUT" +say "done -> $OUT" file "$OUT" || true +say "glibc floor: $(objdump -T "$OUT" | grep -oE 'GLIBC_[0-9]+\.[0-9]+' | sort -Vu | tail -1)" + +# The smoke, same criterion as CI. Distro Qt is on the default paths, so no +# env is needed beyond the offscreen platform. +if [[ "${SMOKE:-1}" == 1 ]]; then + log="$(mktemp "${TMPDIR:-/tmp}/qbz-aarch64-smoke-XXXXXX")" + say "offscreen smoke (60 s) -> $log" + QT_QPA_PLATFORM=offscreen RUST_LOG=info timeout 60 "$OUT" > "$log" 2>&1 || true + lines="$(wc -l < "$log")" + (( lines >= 10 )) || { cat "$log"; die "smoke: the app did not start ($lines lines)"; } + errs="$(grep -av 'propertyCache' "$log" | grep -aciE 'is not a type|unavailable|ReferenceError|TypeError|Cannot read|Unable to assign|Cannot open|no such method|non-existent property|failed to load component|is not installed' || true)" + if (( errs > 0 )); then + grep -av 'propertyCache' "$log" | grep -aiE 'is not a type|unavailable|ReferenceError|TypeError|Cannot read|Unable to assign|Cannot open|no such method|non-existent property|failed to load component|is not installed' | head -20 >&2 + die "smoke: $errs QML complaint(s)" + fi + grep -aq 'QbzCore initialized' "$log" || { tail -20 "$log" >&2; die "smoke: never reached QbzCore init"; } + say "smoke OK (0 QML complaints, core initialized)" +fi diff --git a/scripts/build-aarch64-qbzd.sh b/scripts/build-aarch64-qbzd.sh index ddc0227aa..590724d3b 100755 --- a/scripts/build-aarch64-qbzd.sh +++ b/scripts/build-aarch64-qbzd.sh @@ -4,13 +4,14 @@ # e.g. for a Raspberry Pi 4 + HiFiBerry/USB DAC streamer. # # ── Why this is NOT build-aarch64-linux.sh ─────────────────────────────────── -# That script builds the Slint desktop `qbz`, whose generated `qbz_ui` crate is -# ONE ~1.6M-line module needing ~30 GB for a single rustc. `qbzd` is the -# slint-free column of the workspace: no UI crate, no fonts, no GPU libs. It -# builds in minutes and fits on modest hardware, so: -# • a 4 GB Pi CAN build it natively (unlike the desktop binary); -# • the container caps here are small — no 48 GB swap headroom needed; -# • the `static/` fonts mount the desktop script needs is irrelevant. +# That script builds the Qt desktop `qbz`, which needs a Qt >= 6.8 with private +# headers on the build host and cannot be cross-compiled with cross-rs (the +# target Qt's qmake has to run during the build). `qbzd` is the Qt-free and +# Slint-free column of the workspace: no UI crate, no fonts, no GPU libs. It +# builds in minutes on modest hardware and cross-compiles cleanly, so: +# • a 4 GB Pi CAN build it natively; +# • CROSS mode via cross-rs + crates/Cross.toml still works here; +# • the container caps are small. # # Two modes, auto-selected by host arch (same shape as the desktop script): # @@ -62,9 +63,8 @@ case "$arch" in # the same as the desktop script's — a runaway gets killed instead of # swap-thrashing this box (30 GB, no hibernation) into a hard freeze. export CROSS_CONTAINER_OPTS="${CROSS_CONTAINER_OPTS:---memory=8g --memory-swap=12g}" - # crates/Cross.toml injects the arm64 dev libs into the image. It installs - # the desktop's superset (GUI libs included) because it is keyed by TARGET, - # not by crate — harmless here, and not worth forking a second image. + # crates/Cross.toml injects the arm64 dev libs into the image (the daemon's + # set; the Qt desktop is not cross-built, see build-aarch64-linux.sh). ( cd crates && cross build --release --target "$TARGET" -p qbzd ) install -Dm755 "crates/target/$TARGET/release/qbzd" "$OUT" ;;