scripts/package-m2images.sh
Rama main · réplica de lectura del repositorio original.
#!/usr/bin/env bash
# Package manifest-defined M2Image artifacts as sparse tar.zst archives.
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}
image_root=${IMAGE_ROOT:-${FIRECRAB_IMAGE_ROOT:-${repo_dir}/images}}
compliance_root=${M2IMAGE_COMPLIANCE_DIR:-${image_root}/compliance}
dist_dir=${DIST_DIR:-${repo_dir}/dist/m2images}
out_dir=${OUT_DIR:-}
architecture=${M2IMAGE_ARCH:-}
alias_filter=all
zstd_level=${ZSTD_LEVEL:-19}
zstd_threads=${ZSTD_THREADS:-2}
motd_file=${MOTD_FILE:-${repo_dir}/assets/firecrab-motd}
usage() {
cat <<'EOF'
Usage: ./scripts/package-m2images.sh [options]
Options:
--alias <alias|all> Package one manifest alias or all aliases
--arch <x86_64|aarch64> Artifact architecture (default: uname -m)
--manifest <path> Alternate release manifest
--dist-dir <path> Package root (default: dist/m2images)
-h, --help Show this help
Output is <dist-dir>/<arch>/{alias}.tar.zst plus SHA256SUMS. Set OUT_DIR to
override only the architecture output directory.
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 ;;
*) fail "unknown argument: $1" ;;
esac
done
for command in python3 tar zstd sha256sum debugfs cp; do require_command "$command"; done
python3 "${script_dir}/m2image-manifest.py" --manifest "$manifest" validate >/dev/null
architecture=$(normalize_architecture "${architecture:-$(uname -m)}")
[ -n "$out_dir" ] || out_dir="${dist_dir}/${architecture}"
[ -d "$image_root" ] || fail "image root not found: $image_root"
[ -f "$motd_file" ] || fail "MOTD file not found: $motd_file"
case "$zstd_threads" in ''|*[!0-9]*) fail 'ZSTD_THREADS must be a non-negative integer' ;; esac
case "$zstd_level" in ''|*[!0-9]*) fail 'ZSTD_LEVEL must be a non-negative integer' ;; esac
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
[ "$alias" = "$alias_filter" ] && selected_aliases=("$alias")
done
[ "${#selected_aliases[@]}" -eq 1 ] || fail "unknown --alias: $alias_filter"
fi
mkdir -p "$out_dir"
package_work_dir=$(mktemp -d "${out_dir}/.package.XXXXXX")
trap 'rm -rf -- "$package_work_dir"' EXIT
package_one() {
local alias=$1
local output="${out_dir}/${alias}.tar.zst"
local staging="${package_work_dir}/${alias}"
local rootfs_rel=''
local rel
local -a files=()
mkdir -p "$staging"
while IFS= read -r rel; do
[ -n "$rel" ] || continue
[ -f "${image_root}/${rel}" ] || fail "missing ${image_root}/${rel} (build ${alias} first)"
files+=("$rel")
case "$rel" in rootfs/*) rootfs_rel=$rel ;; esac
mkdir -p "$staging/$(dirname -- "$rel")"
cp --reflink=auto --sparse=always -- "${image_root}/${rel}" "$staging/$rel"
done < <(python3 "${script_dir}/m2image-manifest.py" --manifest "$manifest" \
artifacts "$alias" "$architecture")
[ -n "$rootfs_rel" ] || fail "no rootfs artifact configured for $alias/$architecture"
local compliance_source="${compliance_root}/${alias}-${architecture}"
[ -d "$compliance_source" ] \
|| fail "missing M2Image compliance bundle: $compliance_source (build $alias first)"
for required in bundle.json source-map.json source-publication-plan.json \
sbom.spdx.json README.txt licenses/index.json licenses/GPL-2.0-only.txt; do
[ -s "${compliance_source}/${required}" ] \
|| fail "missing M2Image compliance artifact: ${compliance_source}/${required}"
done
python3 - "${compliance_source}/sbom.spdx.json" "$alias" <<'PY_SBOM'
import json
import sys
path, expected_alias = sys.argv[1:3]
with open(path, encoding='utf-8') as stream:
doc = json.load(stream)
if doc.get('spdxVersion') != 'SPDX-2.3':
raise SystemExit('M2Image SBOM is not SPDX 2.3')
packages = doc.get('packages')
if not isinstance(packages, list) or not packages:
raise SystemExit('M2Image SBOM packages must be a non-empty list')
first = packages[0]
if not isinstance(first, dict) or first.get('name') != expected_alias:
actual = first.get('name') if isinstance(first, dict) else None
raise SystemExit(
f"M2Image SBOM alias mismatch: expected {expected_alias!r}, got {actual!r}"
)
PY_SBOM
mkdir -p "$staging/compliance"
cp -a -- "$compliance_source/." "$staging/compliance/"
files+=("compliance")
cp -- "$motd_file" "$staging/.firecrab-motd"
(
cd "$staging"
debugfs -w -R 'rm /etc/motd' "$rootfs_rel" >/dev/null 2>&1 || true
motd_output=$(debugfs -w -R 'write .firecrab-motd /etc/motd' "$rootfs_rel" 2>&1)
case "$motd_output" in
*'Allocated inode'*) ;;
*) fail "could not install MOTD into $alias rootfs: $motd_output" ;;
esac
)
rm -f -- "$staging/.firecrab-motd"
info "packing $alias/$architecture -> $output"
tar --sparse -C "$staging" -cf - "${files[@]}" \
| zstd -T"$zstd_threads" -"$zstd_level" -f -o "$output"
bytes=$(wc -c <"$output" | tr -d ' ')
info " $alias: $bytes bytes ($(numfmt --to=iec-i --suffix=B "$bytes" 2>/dev/null || printf '%s B' "$bytes"))"
}
for alias in "${selected_aliases[@]}"; do package_one "$alias"; done
info "writing ${out_dir}/SHA256SUMS"
(
cd "$out_dir"
: >SHA256SUMS
# Ignore stale archives from aliases removed from the current manifest.
# Source archives are sibling release artifacts and must survive a binary
# repack instead of silently falling out of the checksum contract.
for alias in "${known_aliases[@]}"; do
for suffix in '.tar.zst' '.sources.tar.zst'; do
candidate="${alias}${suffix}"
[ ! -f "$candidate" ] || sha256sum "$candidate" >>SHA256SUMS
done
done
)
# A complete cross-architecture catalog is generated only when every manifest
# package is present. The publisher performs the strict generation before R2
# upload; this best-effort copy is useful while assembling artifacts locally.
if python3 "${script_dir}/m2image-manifest.py" --manifest "$manifest" catalog \
--dist-dir "$dist_dir" --output "${dist_dir}/catalog.json" 2>/dev/null; then
info "updated ${dist_dir}/catalog.json"
else
info 'catalog deferred until all x86_64 and aarch64 packages are present'
fi
info "done (${#selected_aliases[@]} archive(s) in $out_dir)"