#!/usr/bin/env bash # Build and package Firecracker M2Images from packaging/m2images.json. set -euo pipefail unset CDPATH script_dir=$(cd -- "$(dirname -- "$0")" && pwd -P) repo_dir=$(cd -- "${script_dir}/.." && pwd -P) manifest=${M2IMAGE_MANIFEST:-${repo_dir}/packaging/m2images.json} dist_dir=${DIST_DIR:-${repo_dir}/dist/m2images} out_dir=${OUT_DIR:-} compliance_dir=${M2IMAGE_COMPLIANCE_DIR:-${repo_dir}/images/compliance} alias_filter=all architecture=${M2IMAGE_ARCH:-} package_images=1 usage() { cat <<'EOF' Usage: ./scripts/build-m2images.sh [options] Options: --alias Build one manifest alias or all aliases (default: all) --arch Target architecture (default: uname -m) --manifest Alternate release manifest --dist-dir Package root (default: dist/m2images) --no-package Build image files without packaging them --list List configured aliases and exit -h, --help Show this help The manifest pins distribution versions, builders, artifact paths, internal revisions, and Cloudflare R2 object keys. Build each architecture on a native host of that architecture; package outputs are written below /. EOF } info() { printf '[INFO] %s\n' "$*"; } fail() { printf '[FAIL] %s\n' "$*" >&2; exit 1; } require_command() { command -v "$1" >/dev/null 2>&1 || fail "$1 is required"; } normalize_architecture() { case "$1" in x86_64|amd64) printf '%s\n' x86_64 ;; aarch64|arm64) printf '%s\n' aarch64 ;; *) fail "unsupported architecture: $1 (want x86_64 or aarch64)" ;; esac } while [ "$#" -gt 0 ]; do case "$1" in -h|--help) usage; exit 0 ;; --alias) [ "$#" -ge 2 ] || fail 'missing value for --alias'; alias_filter=$2; shift 2 ;; --alias=*) alias_filter=${1#--alias=}; shift ;; --arch) [ "$#" -ge 2 ] || fail 'missing value for --arch'; architecture=$2; shift 2 ;; --arch=*) architecture=${1#--arch=}; shift ;; --manifest) [ "$#" -ge 2 ] || fail 'missing value for --manifest'; manifest=$2; shift 2 ;; --manifest=*) manifest=${1#--manifest=}; shift ;; --dist-dir) [ "$#" -ge 2 ] || fail 'missing value for --dist-dir'; dist_dir=$2; shift 2 ;; --dist-dir=*) dist_dir=${1#--dist-dir=}; shift ;; --no-package) package_images=0; shift ;; --list) require_command python3 python3 "${script_dir}/m2image-manifest.py" --manifest "$manifest" aliases exit 0 ;; *) fail "unknown argument: $1" ;; esac done require_command python3 require_command sha256sum python3 "${script_dir}/m2image-manifest.py" --manifest "$manifest" validate >/dev/null architecture=$(normalize_architecture "${architecture:-$(uname -m)}") export M2IMAGE_ARCH=$architecture mkdir -p "$compliance_dir" if [ -z "$out_dir" ]; then out_dir="${dist_dir}/${architecture}" fi mapfile -t known_aliases < <( python3 "${script_dir}/m2image-manifest.py" --manifest "$manifest" aliases ) selected_aliases=() if [ "$alias_filter" = all ]; then selected_aliases=("${known_aliases[@]}") else for alias in "${known_aliases[@]}"; do if [ "$alias" = "$alias_filter" ]; then selected_aliases=("$alias") break fi done [ "${#selected_aliases[@]}" -eq 1 ] || fail "unknown --alias: $alias_filter" fi for alias in "${selected_aliases[@]}"; do builder=$(python3 "${script_dir}/m2image-manifest.py" --manifest "$manifest" \ field "$alias" builder.script) version=$(python3 "${script_dir}/m2image-manifest.py" --manifest "$manifest" \ field "$alias" version) [ -x "${repo_dir}/${builder}" ] || fail "builder is not executable: ${repo_dir}/${builder}" while IFS= read -r command; do [ -n "$command" ] && require_command "$command" done < <(python3 "${script_dir}/m2image-manifest.py" --manifest "$manifest" requires "$alias") builder_environment=() while IFS=$'\t' read -r key value; do [ -n "$key" ] || continue builder_environment+=("${key}=${value}") done < <(python3 "${script_dir}/m2image-manifest.py" --manifest "$manifest" environment "$alias") sbom_output="${compliance_dir}/${alias}-${architecture}.spdx.json" rm -f -- "$sbom_output" info "building ${alias} (distribution ${version}, ${architecture})" env "M2IMAGE_ALIAS=${alias}" "M2IMAGE_SBOM_OUTPUT=${sbom_output}" \ "${builder_environment[@]}" "${repo_dir}/${builder}" [ -s "$sbom_output" ] || fail "builder did not produce M2Image SBOM: $sbom_output" python3 - "$sbom_output" "$alias" <<'PY_VALIDATE' import json import sys path, expected_alias = sys.argv[1:3] try: with open(path, encoding="utf-8") as stream: doc = json.load(stream) except (OSError, json.JSONDecodeError) as exc: raise SystemExit(f"invalid M2Image SBOM {path}: {exc}") from exc if doc.get("spdxVersion") != "SPDX-2.3": raise SystemExit( f"invalid M2Image SBOM {path}: expected SPDX-2.3, " f"got {doc.get('spdxVersion')!r}" ) packages = doc.get("packages") if not isinstance(packages, list) or not packages: raise SystemExit(f"invalid M2Image SBOM {path}: packages must be a non-empty list") actual_alias = packages[0].get("name") if isinstance(packages[0], dict) else None if actual_alias != expected_alias: raise SystemExit( f"invalid M2Image SBOM {path}: expected image alias {expected_alias!r}, " f"got {actual_alias!r}" ) PY_VALIDATE M2IMAGE_MANIFEST="$manifest" IMAGE_ROOT="${repo_dir}/images" \ M2IMAGE_COMPLIANCE_DIR="$compliance_dir" \ bash "${script_dir}/collect-m2image-compliance.sh" "$alias" "$architecture" [ -s "${compliance_dir}/${alias}-${architecture}/bundle.json" ] \ || fail "builder did not produce M2Image compliance bundle for ${alias}/${architecture}" done if [ "$package_images" -eq 1 ]; then package_alias=$alias_filter info "packaging ${package_alias} into ${out_dir}" IMAGE_ROOT="${repo_dir}/images" OUT_DIR="$out_dir" DIST_DIR="$dist_dir" \ M2IMAGE_ARCH="$architecture" M2IMAGE_MANIFEST="$manifest" \ "${script_dir}/package-m2images.sh" --alias "$package_alias" --arch "$architecture" info "verifying ${out_dir}/SHA256SUMS" (cd "$out_dir" && sha256sum -c SHA256SUMS) fi info 'M2Image build complete'