202 lines
6.4 KiB
Bash
202 lines
6.4 KiB
Bash
|
|
#!/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" <<EOF
|
||
|
|
[$PACKAGE]
|
||
|
|
name=mtgc
|
||
|
|
baseurl=$repo_url
|
||
|
|
enabled=1
|
||
|
|
gpgcheck=1
|
||
|
|
repo_gpgcheck=1
|
||
|
|
gpgkey=$repo_url/RPM-GPG-KEY-$PACKAGE
|
||
|
|
EOF
|
||
|
|
fi
|
||
|
|
gpg_ --armor --export "$fpr" >"$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 <<EOF
|
||
|
|
|
||
|
|
Serve $dir at $(sed -n 's/^baseurl=//p' "$dir/$PACKAGE.repo"). On each machine, once:
|
||
|
|
|
||
|
|
sudo dnf config-manager addrepo --from-repofile=$repo_file_url
|
||
|
|
sudo dnf install $PACKAGE
|
||
|
|
|
||
|
|
After that, \`sudo dnf upgrade\` picks up new releases.
|
||
|
|
EOF
|