#!/usr/bin/env bash # # rpm-publish.sh — build a signed RPM of the desktop app and add it to a dnf repo. # # Usage: # RPM_REPO_URL=https://rpm.example.com/mtgc \ # ./scripts/rpm-publish.sh --init-key ~/rpm-mtgc # first time # ./scripts/rpm-publish.sh ~/rpm-mtgc # every release after # # Or through npm: `npm run rpm:publish -- ~/rpm-mtgc`. # # Options: # --init-key generate the package signing key (once, ever) # --allow-dirty publish uncommitted changes; the RPM release becomes 0.dirty # # Environment: # MTGC_RPM_DIR repo directory, if not given as an argument # RPM_REPO_URL public address of the repo directory; needed the first time, # and changes the address in mtgc.repo if set later # MTGC_RPM_GNUPGHOME where --init-key keeps the key (default ~/.local/share/mtgc/gnupg) # # The whole repo directory is public — the signing key lives elsewhere — so # serve it as is. Needs rpm-sign and createrepo_c (`sudo dnf install rpm-sign # createrepo_c`) and the desktop setup from ./scripts/devel-setup.sh. # set -euo pipefail usage() { sed -n '3,25p' "$0" | sed 's/^# \{0,1\}//' exit "${1:-0}" } info() { printf '\033[1;34m==>\033[0m %s\n' "$*"; } warn() { printf '\033[1;33m[warn]\033[0m %s\n' "$*"; } err() { printf '\033[1;31m[err]\033[0m %s\n' "$*" >&2; } PACKAGE="mtgc" GNUPGHOME="${MTGC_RPM_GNUPGHOME:-$HOME/.local/share/mtgc/gnupg}" KEEP=3 # versions kept in the repo, so a bad release can be downgraded from init_key=false allow_dirty=false dir="${MTGC_RPM_DIR:-}" while [ $# -gt 0 ]; do case "$1" in --init-key) init_key=true ;; --allow-dirty) allow_dirty=true ;; -h | --help) usage ;; -*) usage 1 ;; *) dir="$1" ;; esac shift done [ -n "$dir" ] || usage 1 mkdir -p "$dir" dir="$(cd "$dir" && pwd)" repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" cd "$repo_root" # --- 1. Tools --------------------------------------------------------------- for tool in rpmsign createrepo_c gpg; do if ! command -v "$tool" >/dev/null 2>&1; then err "$tool not found. Install them: sudo dnf install rpm-sign createrepo_c" exit 1 fi done # --- 2. Package signing key ------------------------------------------------- # # A key of its own in its own keyring, with no passphrase so publishing runs # unattended. Every machine trusts it once, when it adds the repo; lose it and # each one has to import a new key before it takes another update. gpg_() { gpg --homedir "$GNUPGHOME" --batch "$@"; } key_fpr() { gpg_ --list-secret-keys --with-colons 2>/dev/null | awk -F: '/^fpr/ { print $10; exit }'; } if $init_key; then if [ -n "$(key_fpr)" ]; then err "a signing key already exists in $GNUPGHOME; not making another" exit 1 fi info "generating the package signing key in $GNUPGHOME" mkdir -p "$GNUPGHOME" chmod 700 "$GNUPGHOME" gpg_ --pinentry-mode loopback --passphrase '' \ --quick-gen-key "mtgc packages" rsa4096 sign never 2>/dev/null warn "Back up $GNUPGHOME." warn "Without it, every machine has to trust a new key before it takes another update." fi fpr="$(key_fpr)" if [ -z "$fpr" ]; then err "no package signing key in $GNUPGHOME. Run once with --init-key." exit 1 fi # --- 3. Version ------------------------------------------------------------- source scripts/lib/release-version.sh release=1 if $release_dirty; then if ! $allow_dirty; then err "the working tree has uncommitted changes; commit them or pass --allow-dirty" exit 1 fi release=0.dirty fi # --- 4. The repo's address -------------------------------------------------- repo_url="${RPM_REPO_URL:-}" repo_url="${repo_url%/}" case "$repo_url" in "" | http://* | https://*) ;; *) err "RPM_REPO_URL must be an http(s):// address"; exit 1 ;; esac if [ -z "$repo_url" ] && [ ! -f "$dir/$PACKAGE.repo" ]; then err "no repo in $dir yet; set RPM_REPO_URL to the address $dir will be served at" exit 1 fi if [ -n "$repo_url" ]; then cat >"$dir/$PACKAGE.repo" <"$dir/RPM-GPG-KEY-$PACKAGE" # --- 5. Only ever move forward ---------------------------------------------- newest=0 for rpm_file in "$dir/$PACKAGE"-*.rpm; do [ -e "$rpm_file" ] || continue patch="$(rpm -qp --qf '%{VERSION}' "$rpm_file" 2>/dev/null)" patch="${patch##*.}" [ "$patch" -gt "$newest" ] && newest="$patch" done if [ "$release_count" -le "$newest" ]; then err "version $release_version is not newer than the repo's x.y.$newest; commit something first" exit 1 fi # --- 6. Build and sign ------------------------------------------------------ info "building $release_version-$release" outputs="target/release/bundle/rpm" rm -rf "$outputs" cargo tauri build --bundles rpm --ci \ --config "{\"version\":\"$release_version\",\"bundle\":{\"linux\":{\"rpm\":{\"release\":\"$release\"}}}}" built="$(find "$outputs" -name '*.rpm' | head -1)" if [ -z "$built" ]; then err "the build produced no RPM under $outputs" exit 1 fi rpmsign --addsign --define "_gpg_path $GNUPGHOME" --define "_gpg_name $fpr" "$built" >/dev/null # --- 7. Publish ------------------------------------------------------------- update_metadata() { createrepo_c --quiet --update "$dir" && gpg_ --yes --local-user "$fpr" --armor --detach-sign "$dir/repodata/repomd.xml" } published="$dir/$(basename "$built")" cp "$built" "$published" info "updating the repo metadata" if ! update_metadata; then # Take the RPM back out, or the next run would think it's already published. rm -f "$published" update_metadata || true err "updating the repo metadata failed; nothing was published" exit 1 fi # Drop all but the newest few versions. mapfile -t old < <(find "$dir" -maxdepth 1 -name "$PACKAGE-*.rpm" | sort -V | head -n -"$KEEP") if [ "${#old[@]}" -gt 0 ]; then rm -f "${old[@]}" update_metadata fi # --- 8. How to use it ------------------------------------------------------- info "published $release_version-$release" repo_file_url="$(sed -n 's/^baseurl=//p' "$dir/$PACKAGE.repo")/$PACKAGE.repo" cat <