#!/usr/bin/env bash set -Eeuo pipefail # RustProxy multi-arch OCI builder for RouterOS. # Rust compilation runs on BUILDPLATFORM. Target binaries are cross-compiled, # so rustc/cargo do not run through QEMU for arm64 builds. SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" resolve_project_root() { if [[ -n "${RUSTPROXY_PROJECT_ROOT:-}" ]]; then (cd "$RUSTPROXY_PROJECT_ROOT" && pwd) return fi if command -v git >/dev/null 2>&1 && git -C "$SCRIPT_DIR" rev-parse --show-toplevel >/dev/null 2>&1; then git -C "$SCRIPT_DIR" rev-parse --show-toplevel return fi local dir="$SCRIPT_DIR" while [[ "$dir" != "/" ]]; do if [[ -f "$dir/Cargo.toml" ]]; then printf '%s\n' "$dir" return fi dir="$(dirname "$dir")" done return 1 } PROJECT_ROOT="$(resolve_project_root)" || { echo "Cannot find repository root (Cargo.toml)." >&2 echo "Set RUSTPROXY_PROJECT_ROOT=/path/to/RustProxy." >&2 exit 1 } [[ -f "$PROJECT_ROOT/Cargo.toml" ]] || { echo "Missing $PROJECT_ROOT/Cargo.toml" >&2 exit 1 } # Supports the invocation: # ENV_FILE=scripts/routeros-oci-build/.env bash scripts/routeros-oci-build/build-routeros-oci.sh push ENV_FILE="${ROUTEROS_OCI_ENV_FILE:-${ENV_FILE:-$SCRIPT_DIR/.env}}" if [[ "$ENV_FILE" != /* ]]; then ENV_FILE="$PROJECT_ROOT/$ENV_FILE" fi [[ -f "$ENV_FILE" ]] || { echo "Missing environment file: $ENV_FILE" >&2 echo "Copy $SCRIPT_DIR/.env.example to $SCRIPT_DIR/.env and edit it." >&2 exit 1 } set -a # shellcheck disable=SC1090 source "$ENV_FILE" set +a ACTION="${1:-push}" VERSION="$(awk ' /^\[package\][[:space:]]*$/ { in_package=1; next } /^\[/ && in_package { exit } in_package && /^[[:space:]]*version[[:space:]]*=/ { line=$0 sub(/^[^=]*=[[:space:]]*"/, "", line) sub(/".*/, "", line) print line exit } ' "$PROJECT_ROOT/Cargo.toml")" [[ -n "$VERSION" ]] || { echo "Cannot determine package version from Cargo.toml" >&2; exit 1; } # Registry/image. Preferred format: REGISTRY + IMAGE_NAME. # OCI_IMAGE is kept as a compatible shorthand. if [[ -n "${OCI_IMAGE:-}" ]]; then IMAGE_REPO="${OCI_IMAGE%/}" REGISTRY="${OCI_REGISTRY:-${IMAGE_REPO%%/*}}" else : "${REGISTRY:?Set REGISTRY in $ENV_FILE}" : "${IMAGE_NAME:?Set IMAGE_NAME in $ENV_FILE}" REGISTRY="${REGISTRY#https://}" REGISTRY="${REGISTRY#http://}" REGISTRY="${REGISTRY%/}" IMAGE_REPO="${REGISTRY}/${IMAGE_NAME#/}" fi REGISTRY="${REGISTRY#https://}" REGISTRY="${REGISTRY#http://}" REGISTRY="${REGISTRY%/}" IMAGE_REPO="${IMAGE_REPO#https://}" IMAGE_REPO="${IMAGE_REPO#http://}" IMAGE_REPO="${IMAGE_REPO%/}" BUILD_PLATFORMS="${BUILD_PLATFORMS:-linux/amd64,linux/arm64}" BUILDER="${BUILDER:-rustproxy-routeros}" RUST_VERSION="${RUST_VERSION:-1.88}" DEBIAN_SUITE="${DEBIAN_SUITE:-trixie}" DOCKERFILE="${DOCKERFILE:-$SCRIPT_DIR/Dockerfile.routeros}" OCI_TAG="${OCI_TAG:-$VERSION}" OCI_LATEST="${OCI_LATEST:-1}" OCI_EXTRA_TAGS="${OCI_EXTRA_TAGS:-routeros}" OCI_PULL="${OCI_PULL:-1}" OCI_NO_CACHE="${OCI_NO_CACHE:-0}" OCI_VERIFY="${OCI_VERIFY:-1}" OCI_REGISTRY_CACHE="${OCI_REGISTRY_CACHE:-0}" OCI_PROGRESS="${OCI_PROGRESS:-plain}" IMAGE_TAG="${IMAGE_REPO}:${OCI_TAG}" [[ -f "$DOCKERFILE" ]] || { echo "Missing Dockerfile: $DOCKERFILE" >&2; exit 1; } REGISTRY_USERNAME="${OCI_USERNAME:-${REGISTRY_USERNAME:-}}" REGISTRY_SECRET="${OCI_TOKEN:-${OCI_PASSWORD:-${REGISTRY_PASSWORD:-}}}" registry_login() { if [[ -z "$REGISTRY_USERNAME" && -z "$REGISTRY_SECRET" ]]; then return 0 fi if [[ -z "$REGISTRY_USERNAME" || -z "$REGISTRY_SECRET" ]]; then echo "Set both OCI_USERNAME and OCI_PASSWORD/OCI_TOKEN, or neither." >&2 exit 1 fi printf '%s' "$REGISTRY_SECRET" | docker login "$REGISTRY" \ --username "$REGISTRY_USERNAME" \ --password-stdin } ensure_builder() { command -v docker >/dev/null 2>&1 || { echo "docker is required" >&2; exit 127; } docker buildx version >/dev/null 2>&1 || { echo "docker buildx is required" >&2; exit 127; } if docker buildx inspect "$BUILDER" >/dev/null 2>&1; then docker buildx use "$BUILDER" else local args=(create --name "$BUILDER" --driver docker-container --use) [[ -z "${BUILDER_CONFIG:-}" ]] || args+=(--config "$BUILDER_CONFIG") docker buildx "${args[@]}" >/dev/null fi docker buildx inspect --bootstrap >/dev/null } revision="unknown" if command -v git >/dev/null 2>&1 && git -C "$PROJECT_ROOT" rev-parse --is-inside-work-tree >/dev/null 2>&1; then revision="$(git -C "$PROJECT_ROOT" rev-parse --short=12 HEAD 2>/dev/null || printf 'unknown')" fi created="$(date -u +'%Y-%m-%dT%H:%M:%SZ')" common_build_args() { BUILD_ARGS=( build --builder "$BUILDER" --file "$DOCKERFILE" --target runtime-trixie --build-arg "BUILD_VERSION=$VERSION" --build-arg "RUST_VERSION=$RUST_VERSION" --build-arg "DEBIAN_SUITE=$DEBIAN_SUITE" --provenance=false --sbom=false --progress "$OCI_PROGRESS" --label "org.opencontainers.image.title=RustProxy" --label "org.opencontainers.image.version=$VERSION" --label "org.opencontainers.image.revision=$revision" --label "org.opencontainers.image.created=$created" ) [[ "$OCI_PULL" == "1" ]] && BUILD_ARGS+=(--pull) [[ "$OCI_NO_CACHE" == "1" ]] && BUILD_ARGS+=(--no-cache) if [[ "$OCI_REGISTRY_CACHE" == "1" ]]; then BUILD_ARGS+=( --cache-from "type=registry,ref=${IMAGE_REPO}:buildcache" --cache-to "type=registry,ref=${IMAGE_REPO}:buildcache,mode=max,image-manifest=true,oci-mediatypes=true" ) fi } build_push() { registry_login ensure_builder common_build_args BUILD_ARGS+=( --platform "$BUILD_PLATFORMS" --tag "$IMAGE_TAG" ) if [[ "$OCI_LATEST" == "1" && "$OCI_TAG" != "latest" ]]; then BUILD_ARGS+=(--tag "${IMAGE_REPO}:latest") fi if [[ -n "$OCI_EXTRA_TAGS" ]]; then local tag IFS=',' read -r -a extra_tags <<< "$OCI_EXTRA_TAGS" for tag in "${extra_tags[@]}"; do tag="${tag//[[:space:]]/}" [[ -n "$tag" ]] && BUILD_ARGS+=(--tag "${IMAGE_REPO}:${tag}") done fi BUILD_ARGS+=(--push "$PROJECT_ROOT") cat < RustProxy RouterOS OCI build image: $IMAGE_TAG platforms: $BUILD_PLATFORMS Rust: $RUST_VERSION runtime: debian:$DEBIAN_SUITE-slim builder: $BUILDER project: $PROJECT_ROOT Dockerfile: $DOCKERFILE Rust compilation runs on BUILDPLATFORM; arm64 is cross-compiled. INFO docker buildx "${BUILD_ARGS[@]}" if [[ "$OCI_VERIFY" == "1" ]]; then echo echo "==> Published manifest" docker buildx imagetools inspect "$IMAGE_TAG" fi } build_load() { local arch="${2:-amd64}" platform case "$arch" in amd64|x86_64) platform="linux/amd64"; arch="amd64" ;; arm64|aarch64) platform="linux/arm64"; arch="arm64" ;; *) echo "Usage: $0 load [amd64|arm64]" >&2; exit 2 ;; esac ensure_builder common_build_args BUILD_ARGS+=( --platform "$platform" --tag "${IMAGE_REPO}:local-${arch}" --load "$PROJECT_ROOT" ) docker buildx "${BUILD_ARGS[@]}" printf 'Loaded %s:local-%s (%s)\n' "$IMAGE_REPO" "$arch" "$platform" } show_config() { cat <&2 exit 2 ;; esac