Files
qbz/scripts/build-aarch64-linux.sh
T
vicrodh e8f4dd4aa3 build(qt): each worktree builds, and keeps its binary, in its own target
This workstation configures ONE Cargo target outside the checkouts for every
worktree below the project directory. A build in any worktree therefore
replaced the `qbz` another worktree had just built: a parallel session's
release build in its own worktree replaced the binary under test in the
main one.

scripts/qt-target.py now decides where builds go. When the target Cargo
resolves lies outside the worktree and nothing pins it (CARGO_TARGET_DIR or
CARGO_BUILD_TARGET_DIR), the worktree builds in its own sibling directory,
`<target>-worktrees/<worktree>`; rebuilding the same worktree still replaces
its own binary. Cargo's default target inside the checkout (macOS, CI,
Windows) and explicit overrides are untouched, and no machine path is
committed. Two checkouts that share a directory name get a stable suffix
instead of one target.

qt-run.sh, the runtime gate and direct qt-cargo.py builds and tests all use
it. qt-run.sh prints the target and the binary path, and re-points
crates/target after each successful build, never over a real directory.
The aarch64 scripts build into and install from the same target, and the
qbzd acceptance script names the build command that lands its binary there.

The first build of a profile in a new worktree target is seeded from the
shared target with copy-on-write clones, skipping incremental caches:
Cargo rewrites moved build-script paths through root-output
(custom_build.rs, "rewrite paths if the target directory has been moved"),
so unchanged crates are not compiled again and no data is duplicated.
Without clone support nothing is copied and Cargo builds from scratch.

The resolution and shortcut handling start from the uncommitted draft in
the build-storage-layout worktree. Verified on the main worktree: seeded
from the shared release target, Cargo found everything fresh in two
seconds, and crates/target now points at the worktree's own target. 33
build-gate tests cover derivation, overrides, name collisions, linking,
seeding and the runner end to end.

cc
2026-09-14 11:47:07 -06:00

128 lines
6.6 KiB
Bash
Executable File

#!/usr/bin/env bash
#
# 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.
#
# 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
# <rhi/qrhi.h> — 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.
#
# Modes, auto-selected by the host arch:
#
# 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 (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.
#
# 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
REPO="$(pwd)"
OUT="$REPO/dist/qbz-aarch64-linux"
say() { printf '[aarch64-build] %s\n' "$*"; }
die() { say "ERROR: $*" >&2; exit 1; }
# 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 nasm curl python3
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)
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
say "non-apt distro: install the equivalents of: ${DEPS[*]}" >&2
fi
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.
# The target qt-cargo.py builds in (scripts/qt-target.py), resolved once.
CARGO_TARGET_DIR="$(python3 scripts/qt-target.py resolve)"
export CARGO_TARGET_DIR
CARGO_BUILD_JOBS="$jobs" CARGO_INCREMENTAL=0 python3 scripts/qt-cargo.py build --release --manifest-path crates/Cargo.toml -p qbz-qt
install -Dm755 "$CARGO_TARGET_DIR/release/qbz" "$OUT"
;;
x86_64 | amd64)
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."
;;
*)
die "unsupported build host arch: $arch"
;;
esac
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