237 lines
8.9 KiB
Bash
Executable File
237 lines
8.9 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
set -Eeuo pipefail
|
|
source /opt/rust-package-builder/scripts/lib.sh
|
|
|
|
SHA=${1:?commit SHA required}
|
|
SHORT_SHA=${SHA:0:12}
|
|
BUILD_ID="$(date -u '+%Y%m%dT%H%M%SZ')-${SHORT_SHA}"
|
|
WORK="$DATA_DIR/work/$BUILD_ID"
|
|
SRC="$WORK/source"
|
|
PKG="$WORK/package"
|
|
OUT="$WORK/out"
|
|
REPOSITORY="$DATA_DIR/repository"
|
|
APTLY_CONF="$DATA_DIR/state/aptly.conf"
|
|
|
|
cleanup() {
|
|
if [[ "${KEEP_WORKSPACE:-0}" != "1" ]]; then
|
|
rm -rf "$WORK"
|
|
else
|
|
log "workspace kept: $WORK"
|
|
fi
|
|
}
|
|
trap cleanup EXIT
|
|
|
|
mkdir -p "$SRC" "$PKG" "$OUT"
|
|
|
|
log "cloning source"
|
|
git clone --quiet --no-tags --single-branch --branch "$SOURCE_BRANCH" "$SOURCE_REPO" "$SRC"
|
|
git -C "$SRC" checkout --quiet --detach "$SHA"
|
|
ACTUAL_SHA=$(git -C "$SRC" rev-parse HEAD)
|
|
[[ "$ACTUAL_SHA" == "$SHA" ]] || fatal "checkout SHA mismatch"
|
|
|
|
cd "$SRC"
|
|
META_JSON=$(cargo metadata --format-version 1 --no-deps)
|
|
export META_JSON SRC
|
|
|
|
readarray -t META < <(python3 <<'PY'
|
|
import json, os
|
|
m=json.loads(os.environ['META_JSON'])
|
|
src=os.path.realpath(os.environ['SRC'])
|
|
packages=m.get('packages', [])
|
|
chosen=None
|
|
for p in packages:
|
|
if os.path.realpath(os.path.dirname(p['manifest_path'])) == src:
|
|
chosen=p; break
|
|
if chosen is None:
|
|
defaults=set(m.get('workspace_default_members') or [])
|
|
for p in packages:
|
|
if p['id'] in defaults:
|
|
chosen=p; break
|
|
if chosen is None and packages:
|
|
chosen=packages[0]
|
|
if chosen is None:
|
|
raise SystemExit('no Cargo package found')
|
|
bin_targets=[t for t in chosen.get('targets', []) if 'bin' in t.get('kind', [])]
|
|
if not bin_targets:
|
|
raise SystemExit('no binary target found')
|
|
values=[
|
|
chosen.get('name') or '',
|
|
chosen.get('version') or '',
|
|
bin_targets[0].get('name') or '',
|
|
chosen.get('description') or '',
|
|
chosen.get('license') or '',
|
|
chosen.get('homepage') or '',
|
|
chosen.get('repository') or '',
|
|
]
|
|
for v in values:
|
|
print(str(v).replace('\n',' '))
|
|
PY
|
|
)
|
|
|
|
CARGO_NAME=${META[0]}
|
|
VERSION=${META[1]}
|
|
BINARY_NAME=${META[2]}
|
|
DESCRIPTION=${META[3]}
|
|
LICENSE=${META[4]}
|
|
CARGO_HOMEPAGE=${META[5]}
|
|
CARGO_REPOSITORY=${META[6]}
|
|
|
|
[[ -n "$VERSION" && -n "$BINARY_NAME" ]] || fatal "invalid Cargo metadata"
|
|
|
|
PACKAGE_NAME=${PACKAGE_NAME:-}
|
|
MAINTAINER=${PACKAGE_MAINTAINER:-}
|
|
CONTROL_HOMEPAGE=""
|
|
if [[ -r "$SRC/debian/control" ]]; then
|
|
[[ -n "$PACKAGE_NAME" ]] || PACKAGE_NAME=$(read_first_field "$SRC/debian/control" Package || true)
|
|
[[ -n "$MAINTAINER" ]] || MAINTAINER=$(read_first_field "$SRC/debian/control" Maintainer || true)
|
|
CONTROL_HOMEPAGE=$(read_first_field "$SRC/debian/control" Homepage || true)
|
|
fi
|
|
PACKAGE_NAME=${PACKAGE_NAME:-${CARGO_NAME//-/}}
|
|
MAINTAINER=${MAINTAINER:-Rust Package Builder <packages@localhost>}
|
|
DESCRIPTION=${DESCRIPTION:-$PACKAGE_NAME}
|
|
LICENSE=${LICENSE:-Proprietary}
|
|
HOMEPAGE=${PACKAGE_HOMEPAGE:-${CARGO_HOMEPAGE:-${CARGO_REPOSITORY:-${CONTROL_HOMEPAGE:-$SOURCE_REPO}}}}
|
|
|
|
REQUESTED_PACKAGE_RELEASE=$PACKAGE_RELEASE
|
|
if [[ "$REQUESTED_PACKAGE_RELEASE" == "auto" ]]; then
|
|
LAST_VERSION=""
|
|
LAST_PACKAGE_RELEASE=0
|
|
[[ -r "$DATA_DIR/state/last_version" ]] && LAST_VERSION=$(cat "$DATA_DIR/state/last_version")
|
|
[[ -r "$DATA_DIR/state/last_package_release" ]] && LAST_PACKAGE_RELEASE=$(cat "$DATA_DIR/state/last_package_release")
|
|
if [[ "$LAST_VERSION" == "$VERSION" && "$LAST_PACKAGE_RELEASE" =~ ^[0-9]+$ ]]; then
|
|
PACKAGE_RELEASE=$((LAST_PACKAGE_RELEASE + 1))
|
|
else
|
|
PACKAGE_RELEASE=1
|
|
fi
|
|
else
|
|
PACKAGE_RELEASE=$REQUESTED_PACKAGE_RELEASE
|
|
fi
|
|
|
|
export PACKAGE_NAME VERSION BINARY_NAME DESCRIPTION LICENSE HOMEPAGE MAINTAINER PACKAGE_RELEASE
|
|
log "building version $VERSION ($PACKAGE_NAME / $BINARY_NAME)"
|
|
log "cargo build started"
|
|
if [[ -f Cargo.lock ]]; then
|
|
cargo build --release --locked
|
|
else
|
|
cargo build --release
|
|
fi
|
|
log "cargo build completed"
|
|
|
|
BINARY_PATH="$CARGO_TARGET_DIR/release/$BINARY_NAME"
|
|
[[ -x "$BINARY_PATH" ]] || fatal "built binary not found: $BINARY_PATH"
|
|
|
|
MAX_GLIBC_FOUND=$(objdump -T "$BINARY_PATH" 2>/dev/null | grep -oE 'GLIBC_[0-9]+\.[0-9]+' | sed 's/^GLIBC_//' | sort -V | tail -n1 || true)
|
|
if [[ -n "$MAX_GLIBC_FOUND" ]]; then
|
|
if [[ "$(printf '%s\n%s\n' "$MAX_GLIBC_VERSION" "$MAX_GLIBC_FOUND" | sort -V | tail -n1)" != "$MAX_GLIBC_VERSION" ]]; then
|
|
fatal "binary requires GLIBC_$MAX_GLIBC_FOUND, target maximum is GLIBC_$MAX_GLIBC_VERSION"
|
|
fi
|
|
log "glibc compatibility check: <= $MAX_GLIBC_FOUND"
|
|
fi
|
|
|
|
/opt/rust-package-builder/scripts/prepare-package.sh "$SRC" "$PKG" "$BINARY_PATH"
|
|
|
|
DEB_FILE="$OUT/${PACKAGE_NAME}_${VERSION}-${PACKAGE_RELEASE}_amd64.deb"
|
|
RPM_FILE="$OUT/${PACKAGE_NAME}-${VERSION}-${PACKAGE_RELEASE}.x86_64.rpm"
|
|
|
|
log "creating deb package"
|
|
nfpm package --config "$PKG/nfpm-deb.yaml" --packager deb --target "$DEB_FILE"
|
|
log "creating rpm package"
|
|
nfpm package --config "$PKG/nfpm-rpm.yaml" --packager rpm --target "$RPM_FILE"
|
|
|
|
ar t "$DEB_FILE" | grep '^_gpgorigin$' >/dev/null || fatal "DEB package does not contain nFPM GPG origin signature"
|
|
rpm -qpi "$RPM_FILE" | awk -F: '/^Signature/ && $0 !~ /\(none\)/ {ok=1} END{exit !ok}' || fatal "RPM package is not signed"
|
|
log "package signatures verified"
|
|
|
|
cp -f "$DEB_FILE" "$DATA_DIR/packages/deb/"
|
|
cp -f "$RPM_FILE" "$DATA_DIR/packages/rpm/x86_64/"
|
|
|
|
log "updating aptly internal repository"
|
|
aptly -config="$APTLY_CONF" repo add -force-replace package-builder "$DEB_FILE" >/dev/null
|
|
SNAPSHOT="release-${VERSION}-${SHORT_SHA}-$(date +%s)"
|
|
aptly -config="$APTLY_CONF" snapshot create "$SNAPSHOT" from repo package-builder >/dev/null
|
|
|
|
APTLY_SIGN_ARGS=(-batch -gpg-key="$GPG_KEY_ID")
|
|
if [[ -r "$GPG_PASSPHRASE_FILE" ]]; then
|
|
APTLY_SIGN_ARGS+=(-passphrase-file="$GPG_PASSPHRASE_FILE")
|
|
fi
|
|
|
|
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 \
|
|
"${APTLY_SIGN_ARGS[@]}" \
|
|
"$REPO_DISTRIBUTION" . "$SNAPSHOT" >/dev/null
|
|
else
|
|
aptly -config="$APTLY_CONF" publish snapshot \
|
|
"${APTLY_SIGN_ARGS[@]}" \
|
|
-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 "aptly did not create InRelease"
|
|
[[ -f "$APT_PUBLIC/dists/$REPO_DISTRIBUTION/Release.gpg" ]] || fatal "aptly did not create Release.gpg"
|
|
gpg --batch --verify \
|
|
"$APT_PUBLIC/dists/$REPO_DISTRIBUTION/Release.gpg" \
|
|
"$APT_PUBLIC/dists/$REPO_DISTRIBUTION/Release" >/dev/null 2>&1 || fatal "APT Release signature verification failed"
|
|
|
|
RELEASE_DIR="$REPOSITORY/releases/$BUILD_ID"
|
|
mkdir -p "$RELEASE_DIR/apt" "$RELEASE_DIR/rpm/x86_64"
|
|
|
|
CURRENT_TARGET=""
|
|
if [[ -L "$REPOSITORY/current" ]]; then
|
|
CURRENT_LINK=$(readlink "$REPOSITORY/current")
|
|
CURRENT_TARGET="$REPOSITORY/$CURRENT_LINK"
|
|
fi
|
|
|
|
log "staging apt repository"
|
|
if [[ -n "$CURRENT_TARGET" && -d "$CURRENT_TARGET/apt" ]]; then
|
|
rsync -a --delete --link-dest="$CURRENT_TARGET/apt"/ "$APT_PUBLIC"/ "$RELEASE_DIR/apt"/
|
|
else
|
|
rsync -a --delete "$APT_PUBLIC"/ "$RELEASE_DIR/apt"/
|
|
fi
|
|
|
|
log "updating rpm repository"
|
|
if [[ -n "$CURRENT_TARGET" && -d "$CURRENT_TARGET/rpm" ]]; then
|
|
rsync -a --delete --link-dest="$CURRENT_TARGET/rpm"/ "$DATA_DIR/packages/rpm"/ "$RELEASE_DIR/rpm"/
|
|
else
|
|
rsync -a --delete "$DATA_DIR/packages/rpm"/ "$RELEASE_DIR/rpm"/
|
|
fi
|
|
rm -rf "$RELEASE_DIR/rpm/x86_64/repodata"
|
|
createrepo_c --update "$RELEASE_DIR/rpm/x86_64" >/dev/null
|
|
|
|
GPG_SIGN_ARGS=(--batch --yes --pinentry-mode loopback)
|
|
if [[ -r "$GPG_PASSPHRASE_FILE" ]]; then
|
|
GPG_SIGN_ARGS+=(--passphrase-file "$GPG_PASSPHRASE_FILE")
|
|
fi
|
|
gpg "${GPG_SIGN_ARGS[@]}" --armor --detach-sign \
|
|
--local-user "$GPG_KEY_ID" \
|
|
--output "$RELEASE_DIR/rpm/x86_64/repodata/repomd.xml.asc" \
|
|
"$RELEASE_DIR/rpm/x86_64/repodata/repomd.xml"
|
|
gpg --batch --verify \
|
|
"$RELEASE_DIR/rpm/x86_64/repodata/repomd.xml.asc" \
|
|
"$RELEASE_DIR/rpm/x86_64/repodata/repomd.xml" >/dev/null 2>&1 || fatal "RPM metadata signature verification failed"
|
|
|
|
gpg --batch --armor --export "$GPG_KEY_ID" > "$RELEASE_DIR/repo-key.gpg"
|
|
[[ -s "$RELEASE_DIR/repo-key.gpg" ]] || fatal "public GPG key export failed"
|
|
|
|
/opt/rust-package-builder/scripts/generate-index.sh "$RELEASE_DIR/index.html" "$PACKAGE_NAME"
|
|
|
|
# Validate all externally visible endpoints before the atomic switch.
|
|
[[ -s "$RELEASE_DIR/index.html" ]]
|
|
[[ -f "$RELEASE_DIR/apt/dists/$REPO_DISTRIBUTION/InRelease" ]]
|
|
[[ -f "$RELEASE_DIR/rpm/x86_64/repodata/repomd.xml" ]]
|
|
|
|
log "publishing repository atomically"
|
|
rm -f "$REPOSITORY/current.new"
|
|
ln -s "releases/$BUILD_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' "$PACKAGE_RELEASE" | atomic_write "$DATA_DIR/state/last_package_release"
|
|
printf '%s\n' "$BUILD_ID" | atomic_write "$DATA_DIR/state/last_release"
|
|
|
|
log "build completed: version=$VERSION package_release=$PACKAGE_RELEASE commit=$SHA release=$BUILD_ID"
|