diff --git a/README.md b/README.md index 6875405..e9b447e 100644 --- a/README.md +++ b/README.md @@ -139,6 +139,55 @@ From then on, `sudo dnf upgrade` picks up new releases. - The RPM is built against this machine's libraries, so it installs on this Fedora release and newer. +### Distributing to Debian/Ubuntu (apt repo) + +Debian-based machines get the app the same way, from an apt repo you host. +`scripts/deb-publish.sh` builds the `.deb` here on Fedora and adds it to the +repo in a directory you choose. It needs: + +```bash +sudo dnf install reprepro dpkg +``` + +It signs with the same key as the dnf repo. If you haven't made that key yet, +pass `--init-key` the first time. Either way, give the first run the address +the directory will be served at: + +```bash +DEB_REPO_URL=https://deb.example.com/mtgc ./scripts/deb-publish.sh ~/deb-mtgc +``` + +After that, publishing is just: + +```bash +./scripts/deb-publish.sh ~/deb-mtgc # or: npm run deb:publish -- ~/deb-mtgc +``` + +Then, on each machine, add the repo once and install: + +```bash +sudo curl -fsSLo /etc/apt/sources.list.d/mtgc.sources https://deb.example.com/mtgc/mtgc.sources +``` + +```bash +sudo apt update && sudo apt install mtgc +``` + +From then on, `sudo apt upgrade` picks up new releases. + +- **Serve the whole directory.** It holds `pool/`, the signed `dists/`, the + public key (`mtgc.asc`) and `mtgc.sources`, which carries the key inline so + machines don't need a separate keyring step. `conf/` and `db/` are + reprepro's own state; they're harmless to serve but aren't needed. The + newest three versions are kept, so a bad release can be undone with + `sudo apt install mtgc=`. +- Versions are the same as the RPM's. A build published with `--allow-dirty` + gets a `~dirty` suffix, which apt sorts below the clean build of the same + version. +- The `.deb` is built against this machine's libraries, so it needs a distro + with at least this Fedora's glibc (2.43 on Fedora 44). Older Debian and + Ubuntu releases can't install it. + ### Distributing to Windows The Windows installer is cross-built here on Linux and published to a plain diff --git a/package.json b/package.json index 288a96a..568531b 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "sync-server:push": "scripts/sync-server-image.sh push", "android:publish": "scripts/fdroid-publish.sh", "rpm:publish": "scripts/rpm-publish.sh", + "deb:publish": "scripts/deb-publish.sh", "windows:publish": "scripts/windows-publish.sh" }, "dependencies": { diff --git a/scripts/deb-publish.sh b/scripts/deb-publish.sh new file mode 100755 index 0000000..5ca64f4 --- /dev/null +++ b/scripts/deb-publish.sh @@ -0,0 +1,219 @@ +#!/usr/bin/env bash +# +# deb-publish.sh — build the desktop app's .deb and add it to a signed apt repo. +# +# Usage: +# DEB_REPO_URL=https://deb.example.com/mtgc \ +# ./scripts/deb-publish.sh --init-key ~/deb-mtgc # first time +# ./scripts/deb-publish.sh ~/deb-mtgc # every release after +# +# Or through npm: `npm run deb:publish -- ~/deb-mtgc`. +# +# Options: +# --init-key generate the package signing key (once, ever; skip it if +# rpm-publish.sh already made one, the two share it) +# --allow-dirty publish uncommitted changes; the version gets a ~dirty suffix +# +# Environment: +# MTGC_DEB_DIR repo directory, if not given as an argument +# DEB_REPO_URL public address of the repo directory; needed the first time, +# and changes the address in mtgc.sources if set later +# MTGC_DEB_GNUPGHOME where the key lives (default ~/.local/share/mtgc/gnupg) +# +# The whole repo directory is public — the signing key lives elsewhere — so +# serve it as is. Needs reprepro and dpkg (`sudo dnf install reprepro dpkg`) +# and the desktop setup from ./scripts/devel-setup.sh. +# +set -euo pipefail + +usage() { + sed -n '3,26p' "$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" +SUITE="stable" +GNUPGHOME="${MTGC_DEB_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_DEB_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 reprepro dpkg-deb gpg; do + if ! command -v "$tool" >/dev/null 2>&1; then + err "$tool not found. Install them: sudo dnf install reprepro dpkg" + exit 1 + fi +done + +# --- 2. Package signing key ------------------------------------------------- +# +# The same key and keyring as rpm-publish.sh, with no passphrase so publishing +# runs unattended. Every machine trusts it once, when it adds the repo; lose it +# and each one has to take 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 +deb_version="$release_version" +if $release_dirty; then + if ! $allow_dirty; then + err "the working tree has uncommitted changes; commit them or pass --allow-dirty" + exit 1 + fi + # ~ sorts before everything, so the clean build of this version still wins. + deb_version="$release_version~dirty" +fi + +# --- 4. The repo's address -------------------------------------------------- +# +# mtgc.sources carries the public key inline, so a machine adds the repo with +# one download and no separate keyring step. + +repo_url="${DEB_REPO_URL:-}" +repo_url="${repo_url%/}" +case "$repo_url" in + "" | http://* | https://*) ;; + *) err "DEB_REPO_URL must be an http(s):// address"; exit 1 ;; +esac +if [ -z "$repo_url" ] && [ ! -f "$dir/$PACKAGE.sources" ]; then + err "no repo in $dir yet; set DEB_REPO_URL to the address $dir will be served at" + exit 1 +fi +gpg_ --armor --export "$fpr" >"$dir/$PACKAGE.asc" +if [ -z "$repo_url" ]; then + repo_url="$(sed -n 's/^URIs: //p' "$dir/$PACKAGE.sources")" +fi +{ + cat <"$dir/$PACKAGE.sources" + +mkdir -p "$dir/conf" +cat >"$dir/conf/distributions" </dev/null | awk '{ print $NF }') +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 --------------------------------------------------------------- + +info "building $deb_version" +outputs="target/release/bundle/deb" +rm -rf "$outputs" +# reprepro files packages by section, which Tauri leaves out unless told. +cargo tauri build --bundles deb --ci \ + --config "{\"version\":\"$release_version\",\"bundle\":{\"linux\":{\"deb\":{\"section\":\"games\",\"priority\":\"optional\"}}}}" + +built="$(find "$outputs" -maxdepth 1 -name '*.deb' | head -1)" +if [ -z "$built" ]; then + err "the build produced no .deb under $outputs" + exit 1 +fi + +# Tauri's deb config has no release field, so mark a dirty build by repacking. +if [ "$deb_version" != "$release_version" ]; then + unpacked="$outputs/unpacked" + dpkg-deb -R "$built" "$unpacked" + sed -i "s/^Version: .*/Version: $deb_version/" "$unpacked/DEBIAN/control" + repacked="$outputs/${PACKAGE}_${deb_version}_amd64.deb" + dpkg-deb --root-owner-group -b "$unpacked" "$repacked" >/dev/null + rm -rf "$unpacked" "$built" + built="$repacked" +fi + +# --- 7. Publish ------------------------------------------------------------- +# +# reprepro writes the pool, the indexes and the signed Release in one go, and +# drops all but the newest $KEEP versions; if it fails, nothing changed. + +info "adding it to the repo" +if ! repo_ includedeb "$SUITE" "$built"; then + err "reprepro failed; nothing was published" + exit 1 +fi + +# --- 8. How to use it ------------------------------------------------------- + +info "published $deb_version" +cat <