Files
rustbox-package-builder/scripts/build-release.sh
T
2026-11-03 10:29:41 +01:00

177 lines
8.3 KiB
Bash
Executable File

#!/usr/bin/env bash
set -Eeuo pipefail
source /opt/rustbox-package-builder/scripts/lib.sh
: "${SHA:?SHA required}"
: "${SRC:?SRC required}"
REPOSITORY="$DATA_DIR/repository"
APTLY_CONF="$DATA_DIR/state/aptly.conf"
BUILD_TMP_DIR=${BUILD_TMP_DIR:-/tmp/rustbox-package-builder}
OUT="$BUILD_TMP_DIR/out"
PKG="$BUILD_TMP_DIR/package"
export CARGO_TARGET_DIR=${CARGO_TARGET_DIR:-$BUILD_TMP_DIR/cargo-target}
cleanup_build_artifacts() {
rm -rf -- "$OUT" "$PKG"
if is_true "${CLEAN_BUILD_CACHE:-1}"; then
rm -rf -- "$CARGO_TARGET_DIR"
fi
}
trap cleanup_build_artifacts EXIT
rm -rf -- "$OUT" "$PKG"
mkdir -p "$OUT" "$DATA_DIR/packages/deb" "$DATA_DIR/packages/rpm/x86_64" "$DATA_DIR/packages/suse/x86_64" "$CARGO_TARGET_DIR"
VERSION=$(python3.11 - "$SRC/Cargo.toml" <<'PY'
import sys, tomllib
p=tomllib.load(open(sys.argv[1],'rb'))
print(p['package']['version'])
PY
)
[[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+([.-][A-Za-z0-9._-]+)?$ ]] || fatal "unsupported RustBox version: $VERSION"
SHORT_SHA=${SHA:0:12}
requested=${PACKAGE_RELEASE:-auto}
if [[ "$requested" == auto ]]; then
last_version=""; last_release=0
[[ -r "$DATA_DIR/state/last_version" ]] && last_version=$(cat "$DATA_DIR/state/last_version")
[[ -r "$DATA_DIR/state/last_package_release" ]] && last_release=$(cat "$DATA_DIR/state/last_package_release")
if [[ "$last_version" == "$VERSION" && "$last_release" =~ ^[0-9]+$ ]]; then
RELEASE=$((last_release + 1))
else
RELEASE=1
fi
else
[[ "$requested" =~ ^[1-9][0-9]*$ ]] || fatal "PACKAGE_RELEASE must be auto or a positive integer"
RELEASE=$requested
fi
cd "$SRC"
TOOLCHAIN=$(/opt/rustbox-package-builder/scripts/ensure-rust-toolchain.sh "$SRC")
log "building RustBox $VERSION-$RELEASE from $SHA with Rust $TOOLCHAIN"
[[ -f Cargo.lock ]] || fatal "Cargo.lock is required for reproducible packages"
if ! grep -q 'log_dir' src/config.rs || ! grep -q 'log_dir' src/main.rs; then
fatal "RustBox source must support native file logging through log_dir"
fi
case "${RUN_LINUX_TESTS:-compile}" in
0|off|false|no) log "cargo tests skipped" ;;
1|compile|check|no-run) cargo "+$TOOLCHAIN" test --release --locked --no-run ;;
full|run) cargo "+$TOOLCHAIN" test --release --locked ;;
*) fatal "RUN_LINUX_TESTS must be 0, compile, or full" ;;
esac
cargo "+$TOOLCHAIN" build --release --locked
BINARY="$CARGO_TARGET_DIR/release/rustbox"
[[ -x "$BINARY" ]] || fatal "built binary not found: $BINARY"
"$BINARY" --version
RUSTBOX_CONFIG=/opt/rustbox-package-builder/packaging/rustbox.toml "$BINARY" --check-config
MAX_GLIBC_FOUND=$(objdump -T "$BINARY" 2>/dev/null | grep -oE 'GLIBC_[0-9]+\.[0-9]+' | sed 's/^GLIBC_//' | sort -V | tail -n1 || true)
if [[ -n "$MAX_GLIBC_FOUND" ]]; then
newest=$(printf '%s\n%s\n' "$MAX_GLIBC_VERSION" "$MAX_GLIBC_FOUND" | sort -V | tail -n1)
[[ "$newest" == "$MAX_GLIBC_VERSION" ]] || fatal "binary requires GLIBC_$MAX_GLIBC_FOUND; configured maximum is GLIBC_$MAX_GLIBC_VERSION"
log "glibc compatibility: GLIBC_$MAX_GLIBC_FOUND or older"
fi
/opt/rustbox-package-builder/scripts/prepare-package.sh "$SRC" "$PKG" "$BINARY" "$VERSION" "$RELEASE"
DEB="$OUT/rustbox_${VERSION}-${RELEASE}_amd64.deb"
RPM="$OUT/rustbox-${VERSION}-${RELEASE}.x86_64.rpm"
SUSE="$OUT/rustbox-${VERSION}-${RELEASE}.suse.x86_64.rpm"
nfpm package --config "$PKG/nfpm-deb.yaml" --packager deb --target "$DEB"
nfpm package --config "$PKG/nfpm-rpm.yaml" --packager rpm --target "$RPM"
nfpm package --config "$PKG/nfpm-suse.yaml" --packager rpm --target "$SUSE"
ar t "$DEB" | grep '^_gpgorigin$' >/dev/null || fatal "DEB package is not signed"
rpm -qpi "$RPM" | awk -F: '/^Signature/ && $0 !~ /\(none\)/ {ok=1} END{exit !ok}' || fatal "RPM package is not signed"
rpm -qpi "$SUSE" | awk -F: '/^Signature/ && $0 !~ /\(none\)/ {ok=1} END{exit !ok}' || fatal "SUSE RPM package is not signed"
cp -f "$DEB" "$DATA_DIR/packages/deb/"
cp -f "$RPM" "$DATA_DIR/packages/rpm/x86_64/"
cp -f "$SUSE" "$DATA_DIR/packages/suse/x86_64/"
package_retention=${PACKAGE_RETENTION:-10}
prune_package_files "$DATA_DIR/packages/deb" 'rustbox_*.deb' "$package_retention"
prune_package_files "$DATA_DIR/packages/rpm/x86_64" 'rustbox-*.x86_64.rpm' "$package_retention"
prune_package_files "$DATA_DIR/packages/suse/x86_64" 'rustbox-*.suse.x86_64.rpm' "$package_retention"
# Keep the mutable aptly repository in exact sync with the retained DEB files.
# Published snapshots are immutable, so rebuilding this repo does not disturb the
# currently served repository until the new snapshot is switched in below.
aptly -config="$APTLY_CONF" repo remove rustbox rustbox >/dev/null 2>&1 || true
aptly -config="$APTLY_CONF" repo add -force-replace rustbox "$DATA_DIR/packages/deb" >/dev/null
SNAPSHOT="rustbox-${VERSION}-${SHORT_SHA}-$(date +%s)"
aptly -config="$APTLY_CONF" snapshot create "$SNAPSHOT" from repo rustbox >/dev/null
APT_SIGN=(-batch -gpg-key="$GPG_KEY_ID")
if aptly -config="$APTLY_CONF" publish list -raw | awk -v d="$REPO_DISTRIBUTION" '$2 == d {found=1} END{exit !found}'; then
aptly -config="$APTLY_CONF" publish switch -force-overwrite "${APT_SIGN[@]}" "$REPO_DISTRIBUTION" . "$SNAPSHOT" >/dev/null
else
aptly -config="$APTLY_CONF" publish snapshot -force-overwrite "${APT_SIGN[@]}" -distribution="$REPO_DISTRIBUTION" -component="$REPO_COMPONENT" -architectures=amd64 "$SNAPSHOT" >/dev/null
fi
APT_PUBLIC="$DATA_DIR/aptly/public"
[[ -f "$APT_PUBLIC/dists/$REPO_DISTRIBUTION/InRelease" ]] || fatal "APT InRelease missing"
# Once the new snapshot is live, old aptly snapshots no longer serve traffic.
# Drop them and let aptly remove unreferenced pool objects.
while IFS= read -r old_snapshot; do
[[ -n "$old_snapshot" && "$old_snapshot" != "$SNAPSHOT" ]] || continue
if ! aptly -config="$APTLY_CONF" snapshot drop "$old_snapshot" >/dev/null 2>&1; then
log "WARN: could not drop old aptly snapshot $old_snapshot"
fi
done < <(aptly -config="$APTLY_CONF" snapshot list -raw | grep '^rustbox-' || true)
if ! aptly -config="$APTLY_CONF" db cleanup >/dev/null 2>&1; then
log "WARN: aptly database cleanup failed"
fi
RELEASE_ID="$(date -u '+%Y%m%dT%H%M%SZ')-${SHORT_SHA}"
RELEASE_DIR="$REPOSITORY/releases/$RELEASE_ID"
mkdir -p "$RELEASE_DIR/apt" "$RELEASE_DIR/rpm/x86_64" "$RELEASE_DIR/suse/x86_64"
# All paths are on the same persistent data volume. Hard-linking package and
# repository files avoids multiplying identical package payloads for every
# atomic release directory while preserving independent directory trees.
cp -al "$APT_PUBLIC/." "$RELEASE_DIR/apt/"
cp -al "$DATA_DIR/packages/rpm/x86_64/." "$RELEASE_DIR/rpm/x86_64/"
cp -al "$DATA_DIR/packages/suse/x86_64/." "$RELEASE_DIR/suse/x86_64/"
for rpm_root in "$RELEASE_DIR/rpm/x86_64"; do
rm -rf "$rpm_root/repodata"; createrepo_c --update "$rpm_root" >/dev/null
done
rm -rf "$RELEASE_DIR/suse/x86_64/repodata"; createrepo_c --update "$RELEASE_DIR/suse/x86_64" >/dev/null
GPG_SIGN=(--batch --yes --pinentry-mode loopback)
for rpm_root in "$RELEASE_DIR/rpm/x86_64"; do
gpg "${GPG_SIGN[@]}" --armor --detach-sign --local-user "$GPG_KEY_ID" --output "$rpm_root/repodata/repomd.xml.asc" "$rpm_root/repodata/repomd.xml"
done
gpg "${GPG_SIGN[@]}" --armor --detach-sign --local-user "$GPG_KEY_ID" --output "$RELEASE_DIR/suse/x86_64/repodata/repomd.xml.asc" "$RELEASE_DIR/suse/x86_64/repodata/repomd.xml"
gpg --batch --armor --export "$GPG_KEY_ID" > "$RELEASE_DIR/repo-key.gpg"
BUILT_AT=$(date -u '+%Y-%m-%dT%H:%M:%SZ')
for family in debian rhel suse; do
case "$family" in
debian) info_file="$RELEASE_DIR/DEBIAN-BUILD-INFO.txt" ;;
rhel) info_file="$RELEASE_DIR/RHEL-BUILD-INFO.txt" ;;
suse) info_file="$RELEASE_DIR/SUSE-BUILD-INFO.txt" ;;
esac
cat > "$info_file" <<INFO
app_version=$VERSION
package_release=$RELEASE
built_at=$BUILT_AT
packaging_revision=$PACKAGING_REVISION
source_url=$SOURCE_REPO
source_ref=$SOURCE_BRANCH
source_commit=$SHA
build_reason=source-or-packaging-change
package_family=$family
INFO
done
rm -f "$REPOSITORY/current.new"
ln -s "releases/$RELEASE_ID" "$REPOSITORY/current.new"
mv -Tf "$REPOSITORY/current.new" "$REPOSITORY/current"
printf '%s\n' "$SHA" | atomic_write "$DATA_DIR/state/last_commit"
printf '%s\n' "$VERSION" | atomic_write "$DATA_DIR/state/last_version"
printf '%s\n' "$RELEASE" | atomic_write "$DATA_DIR/state/last_package_release"
release_retention=${RELEASE_RETENTION:-10}
prune_release_dirs "$REPOSITORY/releases" "$release_retention" "$RELEASE_DIR"
log "published RustBox $VERSION-$RELEASE: $REPOSITORY_URL"