#!/usr/bin/env bash set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" DOCKERFILE="${DOCKERFILE:-$REPO_ROOT/docker/validator-runtime.Dockerfile}" TARGET_ARCH="" IMAGE_TAG="" OUT_DIR="${OUT_DIR:-$REPO_ROOT/docker-out}" ALLOW_DIRTY=0 SAVE_IMAGE=1 LOAD_IMAGE=1 INSTALL_BINFMT=1 BUILDER_IMAGE="${BUILDER_IMAGE:-rust:1-bookworm}" RUNTIME_IMAGE="${RUNTIME_IMAGE:-debian:bookworm-slim}" usage() { cat <<'USAGE' Usage: scripts/docker/build_image.sh --arch amd64|arm64 [options] Options: --arch Target architecture (required) --image Image tag (default: panda-rpki-validator:-) --out-dir Image archive/metadata directory (default: docker-out) --dockerfile Dockerfile path --builder-image Builder base image (pin for a supported release) --runtime-image Runtime base image (pin for a supported release) --allow-dirty Permit a local build and mark tag/metadata dirty --no-binfmt Do not install binfmt/qemu for a cross-architecture build --no-save Do not write docker save archive --no-load Build without loading the image into the local daemon -h, --help Show this help USAGE } normalize_arch() { case "$1" in amd64|x86_64) printf '%s\n' amd64 ;; arm64|aarch64) printf '%s\n' arm64 ;; *) return 1 ;; esac } safe_name() { printf '%s' "$1" | tr '/:' '--' } while [[ "$#" -gt 0 ]]; do case "$1" in --arch) TARGET_ARCH="$(normalize_arch "$2")" || { echo "unsupported architecture: $2" >&2; exit 2; } shift 2 ;; --image) IMAGE_TAG="$2" shift 2 ;; --out-dir) OUT_DIR="$2" shift 2 ;; --dockerfile) DOCKERFILE="$2" shift 2 ;; --builder-image) BUILDER_IMAGE="$2" shift 2 ;; --runtime-image) RUNTIME_IMAGE="$2" shift 2 ;; --allow-dirty) ALLOW_DIRTY=1 shift ;; --no-binfmt) INSTALL_BINFMT=0 shift ;; --no-save) SAVE_IMAGE=0 shift ;; --no-load) LOAD_IMAGE=0 shift ;; -h|--help) usage exit 0 ;; *) echo "unknown option: $1" >&2 usage >&2 exit 2 ;; esac done [[ -n "$TARGET_ARCH" ]] || { echo "--arch is required" >&2; usage >&2; exit 2; } [[ -f "$DOCKERFILE" ]] || { echo "missing Dockerfile: $DOCKERFILE" >&2; exit 2; } command -v docker >/dev/null 2>&1 || { echo "missing required command: docker" >&2; exit 2; } if SOURCE_COMMIT_FULL="$(git -C "$REPO_ROOT" rev-parse --verify HEAD 2>/dev/null)"; then : else SOURCE_COMMIT_FULL=unknown fi if SOURCE_COMMIT_SHORT="$(git -C "$REPO_ROOT" rev-parse --short=8 --verify HEAD 2>/dev/null)"; then : else SOURCE_COMMIT_SHORT=unknown fi SOURCE_DIRTY=false if [[ -n "$(git -C "$REPO_ROOT" status --short 2>/dev/null)" || "$SOURCE_COMMIT_FULL" == unknown ]]; then SOURCE_DIRTY=true fi if [[ "$SOURCE_DIRTY" == true && "$ALLOW_DIRTY" != 1 ]]; then echo "refusing dirty tree; pass --allow-dirty for a local development build" >&2 exit 2 fi VERSION="$(awk -F'"' '$1 == "version = " { print $2; exit }' "$REPO_ROOT/crates/panda-rpki-validator/Cargo.toml")" REVISION="$VERSION" if [[ "$SOURCE_DIRTY" == true ]]; then REVISION="${REVISION}-dirty" fi if [[ -z "$IMAGE_TAG" ]]; then IMAGE_TAG="panda-rpki-validator:${REVISION}-${TARGET_ARCH}" fi mkdir -p "$OUT_DIR" TARGET_PLATFORM="linux/$TARGET_ARCH" HOST_ARCH="" case "$(uname -m)" in x86_64|amd64) HOST_ARCH=amd64 ;; aarch64|arm64) HOST_ARCH=arm64 ;; *) echo "unsupported host architecture: $(uname -m)" >&2; exit 2 ;; esac if [[ "$HOST_ARCH" != "$TARGET_ARCH" && "$INSTALL_BINFMT" == 1 ]]; then echo "installing binfmt/qemu for $TARGET_ARCH" docker run --rm --privileged tonistiigi/binfmt --install "$TARGET_ARCH" fi METADATA_PATH="$OUT_DIR/$(safe_name "$IMAGE_TAG").build.env" IMAGE_TAR="$OUT_DIR/$(safe_name "$IMAGE_TAG").tar.gz" build_args=( --platform "$TARGET_PLATFORM" --file "$DOCKERFILE" --build-arg "BUILDER_IMAGE=$BUILDER_IMAGE" --build-arg "RUNTIME_IMAGE=$RUNTIME_IMAGE" --build-arg "SOURCE_COMMIT=$SOURCE_COMMIT_FULL" --build-arg "SOURCE_DIRTY=$SOURCE_DIRTY" --build-arg "BUILD_TIMESTAMP_UTC=$(date -u +%Y-%m-%dT%H:%M:%SZ)" --tag "$IMAGE_TAG" ) if [[ "$LOAD_IMAGE" == 1 ]]; then docker buildx build --load "${build_args[@]}" "$REPO_ROOT" else docker buildx build "${build_args[@]}" --output "type=oci,dest=$OUT_DIR/$(safe_name "$IMAGE_TAG").oci.tar" "$REPO_ROOT" fi if [[ "$SAVE_IMAGE" == 1 && "$LOAD_IMAGE" == 1 ]]; then docker save "$IMAGE_TAG" | gzip -c > "$IMAGE_TAR" fi IMAGE_DIGEST="" IMAGE_ID="" if [[ "$LOAD_IMAGE" == 1 ]]; then IMAGE_DIGEST="$(docker image inspect --format '{{index .RepoDigests 0}}' "$IMAGE_TAG" 2>/dev/null || true)" IMAGE_ID="$(docker image inspect --format '{{.Id}}' "$IMAGE_TAG")" fi BUILD_TIMESTAMP_UTC="$(date -u +%Y-%m-%dT%H:%M:%SZ)" { printf 'image=%s\n' "$IMAGE_TAG" printf 'platform=%s\n' "$TARGET_PLATFORM" printf 'arch=%s\n' "$TARGET_ARCH" printf 'host_arch=%s\n' "$HOST_ARCH" printf 'binfmt_installed=%s\n' "$([[ "$HOST_ARCH" != "$TARGET_ARCH" && "$INSTALL_BINFMT" == 1 ]] && printf true || printf false)" printf 'source_commit=%s\n' "$SOURCE_COMMIT_FULL" printf 'source_commit_short=%s\n' "$SOURCE_COMMIT_SHORT" printf 'source_dirty=%s\n' "$SOURCE_DIRTY" printf 'builder_image=%s\n' "$BUILDER_IMAGE" printf 'runtime_image=%s\n' "$RUNTIME_IMAGE" printf 'image_digest=%s\n' "$IMAGE_DIGEST" printf 'image_id=%s\n' "$IMAGE_ID" printf 'archive=%s\n' "$IMAGE_TAR" printf 'archive_sha256=%s\n' "$(sha256sum "$IMAGE_TAR" 2>/dev/null | awk '{print $1}' || true)" printf 'built_at_utc=%s\n' "$BUILD_TIMESTAMP_UTC" } > "$METADATA_PATH" echo "built image=$IMAGE_TAG platform=$TARGET_PLATFORM archive=$IMAGE_TAR"