diff options
| -rw-r--r-- | .dockerignore | 1 | ||||
| -rw-r--r-- | Dockerfile | 19 | ||||
| -rw-r--r-- | deploy/Dockerfile.cross | 13 | ||||
| -rw-r--r-- | deploy/cross-build.sh | 16 | ||||
| -rw-r--r-- | docs/DEPLOY.md | 21 | ||||
| -rw-r--r-- | docs/STATE.md | 12 | ||||
| -rwxr-xr-x | scripts/deploy.sh | 52 |
7 files changed, 100 insertions, 34 deletions
diff --git a/.dockerignore b/.dockerignore index 6e6f325..110fe6f 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,6 +1,5 @@ .git .github -.prebuilt/ build/ var/ .env @@ -7,14 +7,23 @@ RUN apk add --no-cache build-base openssl-dev WORKDIR /src COPY . . ARG VERSION=0.1.0-dev -RUN make -j"$(nproc)" backend VERSION="$VERSION" \ - && make test-core VERSION="$VERSION" \ +# With .prebuilt/ the static aarch64 binaries are cross-compiled on the +# development machine (deploy/Dockerfile.cross); otherwise build and test from +# source on this host. +RUN if [ -x .prebuilt/bokfd ]; then \ + mkdir -p build && \ + cp .prebuilt/bokfd .prebuilt/bokfctl build/; \ + else \ + make -j"$(nproc)" backend VERSION="$VERSION" \ + && make test-core VERSION="$VERSION"; \ + fi \ && strip build/bokfd build/bokfctl -# The runtime image carries the daemon and bokfctl only. The ncurses TUI is a -# frontend: build it from source on the machine you sit at. +# The runtime image carries the daemon and bokfctl only, statically linked +# against OpenSSL and the C library; the ncurses TUI is a frontend built on the +# machine you sit at. FROM alpine:3.24 AS runtime -RUN apk add --no-cache libssl3 ca-certificates util-linux \ +RUN apk add --no-cache ca-certificates util-linux \ && addgroup -S bokfd \ && adduser -S -D -H -u 10001 -G bokfd -s /sbin/nologin bokfd \ && install -d -o bokfd -g bokfd /var/lib/bokfd /var/lib/bokfd/backup \ diff --git a/deploy/Dockerfile.cross b/deploy/Dockerfile.cross new file mode 100644 index 0000000..ab0504c --- /dev/null +++ b/deploy/Dockerfile.cross @@ -0,0 +1,13 @@ +# Cross-compile the static arm64 backend binaries on an amd64 host. The +# runtime image is assembled later on the target host from the produced +# binaries; there is no TUI in the runtime image. +FROM debian:bookworm +RUN dpkg --add-architecture arm64 \ + && apt-get update \ + && apt-get install -y --no-install-recommends \ + make ca-certificates gcc-aarch64-linux-gnu \ + libc6-dev:arm64 libssl-dev:arm64 \ + && rm -rf /var/lib/apt/lists/* +COPY cross-build.sh /usr/local/bin/cross-build +RUN chmod 0755 /usr/local/bin/cross-build +ENTRYPOINT ["/usr/local/bin/cross-build"] diff --git a/deploy/cross-build.sh b/deploy/cross-build.sh new file mode 100644 index 0000000..19d07b7 --- /dev/null +++ b/deploy/cross-build.sh @@ -0,0 +1,16 @@ +#!/bin/sh +# Cross-compile the static aarch64 backend binaries (bokfd, bokfctl). +# Expects the source read-only at /src, writes the binaries to /out and takes +# the version from $VERSION. Run through deploy/Dockerfile.cross. OpenSSL and +# glibc are linked statically, so the Alpine runtime image needs no shared +# libraries (the kernel ABI is all that matters). +set -eu + +make -C /src -j"$(nproc)" BUILD=/tmp/build backend \ + CC=aarch64-linux-gnu-gcc \ + CFLAGS="-O2 -g -static -L/usr/lib/aarch64-linux-gnu" \ + SSL_LIBS="-l:libssl.a -l:libcrypto.a -ldl -lpthread" \ + VERSION="${VERSION:-0.1.0-dev}" + +mkdir -p /out +cp /tmp/build/bokfd /tmp/build/bokfctl /out/ diff --git a/docs/DEPLOY.md b/docs/DEPLOY.md index 7385a8f..6fe6a53 100644 --- a/docs/DEPLOY.md +++ b/docs/DEPLOY.md @@ -56,6 +56,8 @@ Normal deploys build an image and recreate the container. While developing, `scripts/deploy.sh --dev` skips the image entirely: - the gate (`make` + `make test`) still runs locally, +- on an architecture mismatch the static aarch64 binaries are + cross-compiled here (about 20 s, `deploy/Dockerfile.cross`), - the binaries are copied into the running container with `docker cp`, - the daemon is reloaded with `SIGHUP`, which re-execs the binary in place (in-memory sessions are lost, clients reconnect), @@ -63,9 +65,7 @@ Normal deploys build an image and recreate the container. While developing, No image is built and the container is not recreated; a later normal deploy replaces the copied binaries. Use a descriptive tag, e.g. -`scripts/deploy.sh --dev v0.2.0-rc1`. Hot reload requires the host to share -the dev machine's architecture; on a mismatch `--dev` falls back to a full -remote build. +`scripts/deploy.sh --dev v0.2.0-rc1`. ## Deploying upgrades @@ -80,7 +80,8 @@ The script: 1. `make` + `make test` on the dev machine, 2. builds `bokf:<tag>`: locally and ships it with `docker save | gzip | ssh docker load`, or — when the host runs a different CPU architecture — - builds it natively on the host from a source tar, + cross-compiles the backend here and assembles the image on the host from + a source tar, 3. copies `compose.yaml` and writes `BOKF_IMAGE`/`BOKF_TAG` into the host's `.env` (other keys are preserved), 4. `docker compose up -d --no-build`, then polls the container healthcheck, @@ -88,11 +89,13 @@ The script: that is still loaded on the host. Architecture mismatches are automatic: `uname -m` is compared over SSH and a -mismatch switches to a remote build of the same Alpine image (compiled inside -the host's Docker, so the host still needs no toolchain). Override with -`BOKF_BUILD=local` or `BOKF_BUILD=remote` (also settable in `.env`). The -runtime image is Alpine and carries `bokfd` + `bokfctl` only; the ncurses TUI -is a frontend and never shipped. +mismatch runs `deploy/Dockerfile.cross`, which links `bokfd`/`bokfctl` +statically for aarch64 (glibc + OpenSSL archives; it runs on the Alpine +runtime directly, DNS included). The image is then assembled in the host's +Docker — no compilation there. Override with `BOKF_BUILD=local` or +`BOKF_BUILD=remote` (also settable in `.env`). The runtime image is Alpine, +carries `bokfd` + `bokfctl` only (no `libssl3`; the binaries are static) and +never ships the ncurses TUI, which is a frontend built on the client. Tags are `git describe` output unless passed. Tag releases (`v*`) so rollback and support have meaningful versions. The rollback image must still exist on diff --git a/docs/STATE.md b/docs/STATE.md index 21995c6..b6c5d4f 100644 --- a/docs/STATE.md +++ b/docs/STATE.md @@ -80,11 +80,13 @@ check. get accounts/roles/tokens, never VPN access. The runtime image is **Alpine + backend only** (`bokfd`, `bokfctl`; the ncurses TUI is a frontend built on the client machine). `scripts/deploy.sh` builds locally - and ships over SSH, or builds the same image natively on the host when - architectures differ (no cross-compilation — a musl cross toolchain is - not trusted yet). `scripts/deploy.sh --dev` hot-reloads the binaries in - the running container (SIGHUP re-exec via `docker cp`) when the - architecture matches, otherwise it falls back to a full remote build. + and ships over SSH, or — when the architectures differ — + cross-compiles the backend on the dev machine as **static aarch64 + (glibc + OpenSSL archives, runs directly on Alpine, DNS verified) and + assembles the image in the host's Docker** (`deploy/Dockerfile.cross`, + ~20 s; the image carries no `libssl3`). `scripts/deploy.sh --dev` + hot-reloads the binaries in the running container (SIGHUP re-exec via + `docker cp`), cross-compiling first when the architecture differs. 15. **Reports in the TUI**: rendered as fixed-width Swedish tables that mirror the Kapitas PDF exports (Saldobalans, Resultatrapport with previous-year column and 89xx bokfört/ej bokfört, Balansrapport with Ing balans/Ing diff --git a/scripts/deploy.sh b/scripts/deploy.sh index ad5350c..b625ac5 100755 --- a/scripts/deploy.sh +++ b/scripts/deploy.sh @@ -3,10 +3,12 @@ # # scripts/deploy.sh [--dev] [tag] # -# Normal mode builds an image and recreates the container, building natively on -# the host when the target architecture differs. --dev (same architecture only) -# skips the image: it copies the fresh binaries into the running container and -# reloads the daemon with SIGHUP, which re-execs them in place. +# Normal mode cross-compiles the static aarch64 backend binaries on this +# machine and assembles the image on the host when the architectures differ +# (otherwise it builds and ships the image locally). --dev skips the image: it +# copies fresh binaries into the running container and reloads the daemon with +# SIGHUP, which re-execs them in place; the binaries are cross-compiled when +# the host runs a different architecture. # # BOKF_HOST (user@host) and BOKF_REMOTE_DIR are read from the environment or # from .env in the repository root. The host needs only Docker and Compose. @@ -61,6 +63,21 @@ wait_healthy() { return 1 } +cross_build() { + echo "deploy: cross-compiling static $host_arch binaries on this machine" + rm -rf .prebuilt + mkdir -p .prebuilt + docker build -q -f deploy/Dockerfile.cross -t bokf-cross deploy/ >/dev/null + docker run --rm -e "VERSION=$TAG" -v "$PWD":/src:ro \ + -v "$PWD/.prebuilt":/out bokf-cross + if command -v file >/dev/null 2>&1 && + ! file .prebuilt/bokfd | grep -q aarch64; then + echo "deploy: cross-build produced no arm64 binary" >&2 + rm -rf .prebuilt + exit 1 + fi +} + echo "deploy: gate: build + tests" make -j"$(nproc)" make test @@ -73,7 +90,7 @@ case "$BOKF_BUILD" in if [ "$(uname -m)" = "$host_arch" ]; then BOKF_BUILD=local else - echo "deploy: host is $host_arch, building the image on the host" + echo "deploy: host is $host_arch, cross-compiling here" BOKF_BUILD=remote fi ;; @@ -86,20 +103,30 @@ case "$BOKF_BUILD" in ;; esac -if [ "$DEV" = 1 ] && [ "$BOKF_BUILD" = local ]; then +if [ "$DEV" = 1 ]; then if ! remote "test -f '$BOKF_REMOTE_DIR/var/db/bokfd.db'"; then echo "deploy: no database on $BOKF_HOST; run a normal deploy first" >&2 exit 1 fi - echo "deploy: dev: copying the locally built binaries into the running container" + if [ "$BOKF_BUILD" = local ]; then + prebin="build" + echo "deploy: dev: using the locally built binaries" + else + cross_build + prebin=".prebuilt" + fi + echo "deploy: dev: copying the binaries into the running container" remote "mkdir -p /tmp/bokf-dev" - scp -q build/bokfd build/bokfctl "$BOKF_HOST:/tmp/bokf-dev/" + scp -q "$prebin/bokfd" "$prebin/bokfctl" "$BOKF_HOST:/tmp/bokf-dev/" remote "cd '$BOKF_REMOTE_DIR' cid=\$(docker compose ps -q bokfd) test -n \"\$cid\" || { echo 'deploy: bokfd is not running' >&2; exit 1; } docker cp /tmp/bokf-dev/bokfd \"\$cid:/usr/local/bin/bokfd\" docker cp /tmp/bokf-dev/bokfctl \"\$cid:/usr/local/bin/bokfctl\" docker kill --signal=HUP \"\$cid\"" + if [ "$prebin" = ".prebuilt" ]; then + rm -rf .prebuilt + fi ver="" for _ in $(seq 1 15); do ver="$(remote "cd '$BOKF_REMOTE_DIR' && docker exec \$(docker compose ps -q bokfd) bokfctl meta 2>/dev/null" | @@ -116,23 +143,20 @@ if [ "$DEV" = 1 ] && [ "$BOKF_BUILD" = local ]; then exit 1 fi -if [ "$DEV" = 1 ]; then - echo "deploy: dev: host is $host_arch, hot reload needs a matching" \ - "architecture; doing a full deploy" >&2 -fi - if [ "$BOKF_BUILD" = local ]; then echo "deploy: building image bokf:$TAG locally" docker build --build-arg "VERSION=$TAG" -t "bokf:$TAG" . echo "deploy: shipping image to $BOKF_HOST" docker save "bokf:$TAG" | gzip | "${SSH[@]}" 'gunzip | docker load' else - echo "deploy: assembling image bokf:$TAG natively on $BOKF_HOST" + cross_build + echo "deploy: assembling image bokf:$TAG on $BOKF_HOST" tar -cf - \ --exclude=./.git --exclude=./build --exclude=./var --exclude=./.env \ --exclude='./*.se' --exclude='./*.db' --exclude='./*.db-wal' \ --exclude='./*.db-shm' --exclude=./docs . | "${SSH[@]}" "docker build --build-arg 'VERSION=$TAG' -t 'bokf:$TAG' -" + rm -rf .prebuilt fi remote "mkdir -p '$BOKF_REMOTE_DIR'" |
