···33# leveva is a workspace member, so the whole workspace is copied in: cargo needs
44# every member manifest to resolve the lockfile, even though `-p leveva` only
55# compiles leveva and its path dependency (leveva-iauth). The build is pure Rust
66-# (rustls/ring for TLS, no system OpenSSL), so the runtime image needs nothing
77-# beyond glibc.
66+# (rustls/ring for TLS, no system OpenSSL), and we cross-compile to a fully
77+# static musl target with cargo-zigbuild, so the runtime image needs nothing.
88+#
99+# The build stage pins itself to the native BUILDPLATFORM (no QEMU emulation);
1010+# cargo-zigbuild then cross-compiles to whichever musl triple matches the
1111+# requested TARGETARCH (amd64 -> x86_64, arm64 -> aarch64). Both ARGs are
1212+# populated automatically by `docker buildx`.
81399-FROM rust:1-bookworm AS build
1414+FROM --platform=$BUILDPLATFORM rust:1-bookworm AS build
1015WORKDIR /src
11161717+# Install zig from ziglang.org rather than apt: Debian's package is far behind,
1818+# and leveva's cross-build path (cargo-zigbuild) wants a current toolchain.
1919+# The tarball is a self-contained static binary; just extract and symlink.
2020+ARG ZIG_VERSION=0.16.0
2121+RUN apt-get update \
2222+ && apt-get install -y --no-install-recommends curl xz-utils ca-certificates \
2323+ && rm -rf /var/lib/apt/lists/* \
2424+ && case "$(dpkg --print-architecture)" in \
2525+ amd64) ZIG_ARCH=x86_64 ;; \
2626+ arm64) ZIG_ARCH=aarch64 ;; \
2727+ riscv64) ZIG_ARCH=riscv64 ;; \
2828+ ppc64le) ZIG_ARCH=powerpc64le ;; \
2929+ *) echo "unsupported arch: $(dpkg --print-architecture)" >&2; exit 1 ;; \
3030+ esac \
3131+ && curl -fsSL "https://ziglang.org/download/${ZIG_VERSION}/zig-${ZIG_ARCH}-linux-${ZIG_VERSION}.tar.xz" \
3232+ | tar -xJ -C /opt \
3333+ && ln -s "/opt/zig-${ZIG_ARCH}-linux-${ZIG_VERSION}/zig" /usr/local/bin/zig \
3434+ && cargo install cargo-zigbuild
3535+3636+# Map the Docker TARGETARCH to a Rust musl triple, add the std component for it,
3737+# then cross-build. The artifact is copied to a stable path (/leveva) so the
3838+# runtime stage's COPY doesn't need to know the triple.
3939+ARG TARGETARCH
4040+1241# Copy the full workspace (path deps + lockfile) and build only leveva.
1342COPY . .
1414-RUN cargo build --release --locked -p leveva --bin leveva
4343+RUN set -eux; \
4444+ case "$TARGETARCH" in \
4545+ amd64) RUST_TARGET=x86_64-unknown-linux-musl ;; \
4646+ arm64) RUST_TARGET=aarch64-unknown-linux-musl ;; \
4747+ riscv64) RUST_TARGET=riscv64gc-unknown-linux-musl ;; \
4848+ ppc64le) RUST_TARGET=powerpc64le-unknown-linux-musl ;; \
4949+ ppc64) RUST_TARGET=powerpc64-unknown-linux-musl ;; \
5050+ *) echo "unsupported TARGETARCH: $TARGETARCH" >&2; exit 1 ;; \
5151+ esac; \
5252+ rustup target add "$RUST_TARGET"; \
5353+ cargo zigbuild --release --locked --target "$RUST_TARGET" -p leveva --bin leveva; \
5454+ cp "target/$RUST_TARGET/release/leveva" /leveva
15551656# ---
17571818-FROM debian:bookworm-slim AS runtime
1919-RUN useradd --system --user-group --home-dir /var/lib/leveva --create-home leveva
5858+# A throwaway stage that supplies the two pieces of userland the scratch runtime
5959+# needs: the CA bundle and the unprivileged service account. Both are pure data
6060+# (PEM files / passwd entries), so we pin this to the native BUILDPLATFORM — it
6161+# never has to match the target arch, and the alpine image only needs to exist
6262+# for the builder. This is what lets the runtime work on ppc64 (big endian),
6363+# which has no official alpine/debian image of its own.
6464+FROM --platform=$BUILDPLATFORM alpine:3.24 AS rootfs
6565+RUN apk add --no-cache ca-certificates \
6666+ && addgroup -g 10001 leveva \
6767+ && adduser -S -u 10001 -G leveva -h /var/lib/leveva leveva
20682121-COPY --from=build /src/target/release/leveva /usr/local/bin/leveva
6969+# ---
7070+7171+# The binary is a fully static musl build, so the runtime image needs nothing but
7272+# the binary itself plus the CA bundle and account skeleton from the rootfs stage.
7373+# scratch has no per-arch base image, so every target platform is satisfied.
7474+FROM scratch AS runtime
7575+7676+COPY --from=rootfs /etc/ssl/certs /etc/ssl/certs
7777+COPY --from=rootfs /etc/passwd /etc/passwd
7878+COPY --from=rootfs /etc/group /etc/group
7979+COPY --from=rootfs --chown=10001:10001 /var/lib/leveva /var/lib/leveva
8080+COPY --from=build /leveva /usr/local/bin/leveva
22812382# Config lives in /etc/leveva; runtime state (none today) under /var/lib/leveva.
2483WORKDIR /var/lib/leveva
···11+#!/usr/bin/env bash
22+#
33+# generate-release-assets.sh — build leveva for x86_64 + aarch64 and emit
44+# .deb and .rpm packages for each architecture.
55+#
66+# Cross-compilation is handled by cargo-zigbuild (zig as the linker/sysroot),
77+# so no per-arch C toolchain is required. Both targets are static musl builds.
88+# Packaging is pure-Rust: cargo-deb and cargo-generate-rpm read the
99+# [package.metadata.deb] / [package.metadata.generate-rpm] tables in
1010+# leveva/Cargo.toml. Passing --target to each rewrites the `target/release/`
1111+# asset prefix to `target/<triple>/release/`, so they pick up the cross binary.
1212+#
1313+# Outputs (relative to the workspace root):
1414+# target/<triple>/debian/leveva_<ver>_<arch>.deb
1515+# target/<triple>/generate-rpm/leveva-<ver>-1.<arch>.rpm
1616+# and a copy of every artifact collected under dist/release/.
1717+set -euo pipefail
1818+1919+# Resolve the workspace root (parent of this script's directory) so the script
2020+# works regardless of the caller's cwd; cargo-deb/-rpm resolve asset paths
2121+# relative to it.
2222+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
2323+ROOT_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
2424+cd "${ROOT_DIR}"
2525+2626+TARGETS=(
2727+ x86_64-unknown-linux-musl
2828+ aarch64-unknown-linux-musl
2929+ riscv64gc-unknown-linux-musl
3030+ powerpc64le-unknown-linux-musl
3131+ powerpc64-unknown-linux-musl
3232+)
3333+3434+OUT_DIR="${ROOT_DIR}/var/dist/release"
3535+3636+require() {
3737+ command -v "$1" >/dev/null 2>&1 || {
3838+ echo "error: required tool '$1' not found in PATH" >&2
3939+ echo " install with: $2" >&2
4040+ exit 1
4141+ }
4242+}
4343+4444+require cargo "https://rustup.rs"
4545+require zig "https://ziglang.org/download/ (or: brew install zig)"
4646+require cargo-zigbuild "cargo install cargo-zigbuild"
4747+require cargo-deb "cargo install cargo-deb"
4848+require cargo-generate-rpm "cargo install cargo-generate-rpm"
4949+5050+# Ensure the rustup std component is present for each target; zigbuild links the
5151+# objects but rustc still needs the precompiled std for the triple.
5252+for triple in "${TARGETS[@]}"; do
5353+ if ! rustup target list --installed 2>/dev/null | grep -qx "${triple}"; then
5454+ echo ">> adding rust target ${triple}"
5555+ rustup target add "${triple}"
5656+ fi
5757+done
5858+5959+mkdir -p "${OUT_DIR}"
6060+6161+for triple in "${TARGETS[@]}"; do
6262+ echo
6363+ echo "==> ${triple}"
6464+6565+ echo ">> cargo zigbuild --release"
6666+ cargo zigbuild --quiet --release --target "${triple}" -p leveva
6767+6868+ # --no-build: reuse the zigbuild artifact above (cargo-deb would otherwise
6969+ # invoke a plain `cargo build` that lacks the zig cross linker).
7070+ # --no-strip: the host `strip` can't process a foreign-arch binary; the
7171+ # musl release binary is already lean enough to ship unstripped.
7272+ echo ">> cargo deb"
7373+ cargo deb -p leveva --target "${triple}" --no-build --no-strip
7474+7575+ echo ">> cargo generate-rpm"
7676+ cargo generate-rpm -p leveva --target "${triple}"
7777+7878+ # Collect the artifacts into one place for convenience.
7979+ cp -v "target/${triple}/debian/"*.deb "${OUT_DIR}/"
8080+ cp -v "target/${triple}/generate-rpm/"*.rpm "${OUT_DIR}/"
8181+done
8282+8383+echo
8484+echo "==> release assets collected in ${OUT_DIR}:"
8585+ls -1 "${OUT_DIR}"