2026-10-13 21:20:28 -05:00
|
|
|
#!/usr/bin/env bash
|
|
|
|
|
#
|
|
|
|
|
# fdroid-publish.sh — build a signed release APK and add it to an F-Droid repo.
|
|
|
|
|
#
|
|
|
|
|
# Usage:
|
|
|
|
|
# FDROID_REPO_URL=https://fdroid.example.com/repo \
|
|
|
|
|
# ./scripts/fdroid-publish.sh --init-keystore ~/fdroid-mtgc # first time
|
|
|
|
|
# ./scripts/fdroid-publish.sh ~/fdroid-mtgc # every release after
|
|
|
|
|
#
|
|
|
|
|
# Or through npm: `npm run android:publish -- ~/fdroid-mtgc`.
|
|
|
|
|
#
|
|
|
|
|
# Options:
|
|
|
|
|
# --init-keystore generate the app signing key (once, ever) and keystore.properties
|
|
|
|
|
# --allow-dirty publish uncommitted changes; the version gets a -dirty suffix
|
|
|
|
|
#
|
|
|
|
|
# Environment:
|
|
|
|
|
# MTGC_FDROID_DIR repo directory, if not given as an argument
|
2026-10-13 21:29:19 -05:00
|
|
|
# FDROID_REPO_URL public address of <dir>/repo, ending in /repo; needed the first time,
|
|
|
|
|
# and changes the repo's address if set later
|
2026-10-13 21:20:28 -05:00
|
|
|
# MTGC_ANDROID_KEYSTORE where --init-keystore puts the key (default ~/.local/share/mtgc/android-release.jks)
|
|
|
|
|
#
|
|
|
|
|
# Serve <dir>/repo (and <dir>/archive if you like), never <dir> itself: it holds
|
|
|
|
|
# the repo signing key and its password. Needs fdroidserver (`pipx install
|
|
|
|
|
# fdroidserver`) and the Android setup from ./scripts/android-setup.sh.
|
|
|
|
|
#
|
|
|
|
|
set -euo pipefail
|
|
|
|
|
|
|
|
|
|
usage() {
|
2026-10-13 21:29:19 -05:00
|
|
|
sed -n '3,24p' "$0" | sed 's/^# \{0,1\}//'
|
2026-10-13 21:20:28 -05:00
|
|
|
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; }
|
|
|
|
|
|
|
|
|
|
APP_ID="com.garrettmills.mtgc"
|
|
|
|
|
APP_KEYSTORE="${MTGC_ANDROID_KEYSTORE:-$HOME/.local/share/mtgc/android-release.jks}"
|
|
|
|
|
APP_KEY_ALIAS="mtgc"
|
|
|
|
|
|
|
|
|
|
init_keystore=false
|
|
|
|
|
allow_dirty=false
|
|
|
|
|
dir="${MTGC_FDROID_DIR:-}"
|
|
|
|
|
while [ $# -gt 0 ]; do
|
|
|
|
|
case "$1" in
|
|
|
|
|
--init-keystore) init_keystore=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"
|
|
|
|
|
key_props="src-tauri/gen/android/keystore.properties"
|
|
|
|
|
|
|
|
|
|
# --- 1. Tools ---------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
source scripts/android-env.sh >/dev/null
|
|
|
|
|
|
|
|
|
|
if ! command -v fdroid >/dev/null 2>&1; then
|
|
|
|
|
err "fdroid not found. Install fdroidserver: pipx install fdroidserver"
|
|
|
|
|
exit 1
|
|
|
|
|
fi
|
|
|
|
|
build_tools="$(find "$ANDROID_HOME/build-tools" -mindepth 1 -maxdepth 1 -type d 2>/dev/null | sort -V | tail -1)"
|
|
|
|
|
apksigner="$build_tools/apksigner"
|
|
|
|
|
if [ ! -x "$apksigner" ]; then
|
|
|
|
|
err "apksigner not found under $ANDROID_HOME/build-tools (install a build-tools package)"
|
|
|
|
|
exit 1
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
# --- 2. App signing key -----------------------------------------------------
|
|
|
|
|
#
|
|
|
|
|
# Every update must be signed with the same key as the installed app, so this
|
|
|
|
|
# key is made once and kept forever. Lose it, and each phone has to uninstall
|
|
|
|
|
# the app before it will take a newer build.
|
|
|
|
|
|
|
|
|
|
if $init_keystore; then
|
|
|
|
|
if [ -e "$APP_KEYSTORE" ] || [ -e "$key_props" ]; then
|
|
|
|
|
err "a signing key already exists ($APP_KEYSTORE or $key_props); not overwriting it"
|
|
|
|
|
exit 1
|
|
|
|
|
fi
|
|
|
|
|
info "generating the app signing key at $APP_KEYSTORE"
|
|
|
|
|
mkdir -p "$(dirname "$APP_KEYSTORE")"
|
|
|
|
|
password="$(openssl rand -hex 24)"
|
|
|
|
|
keytool -genkeypair -keystore "$APP_KEYSTORE" -storetype PKCS12 \
|
|
|
|
|
-storepass "$password" -alias "$APP_KEY_ALIAS" \
|
|
|
|
|
-keyalg RSA -keysize 4096 -validity 10000 -dname "CN=mtgc" >/dev/null
|
|
|
|
|
chmod 600 "$APP_KEYSTORE"
|
|
|
|
|
(
|
|
|
|
|
umask 077
|
|
|
|
|
printf 'storeFile=%s\npassword=%s\nkeyAlias=%s\n' "$APP_KEYSTORE" "$password" "$APP_KEY_ALIAS" >"$key_props"
|
|
|
|
|
)
|
|
|
|
|
warn "Back up $APP_KEYSTORE and $key_props (it holds the password)."
|
|
|
|
|
warn "Without them, phones can't update the app without uninstalling it first."
|
|
|
|
|
elif [ ! -f "$key_props" ]; then
|
|
|
|
|
err "no app signing key ($key_props). Run once with --init-keystore."
|
|
|
|
|
exit 1
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
# --- 3. Version -------------------------------------------------------------
|
|
|
|
|
#
|
|
|
|
|
# The versionCode is the commit count, so every commit on main can ship with no
|
|
|
|
|
# manual bump and F-Droid sees each one as newer than the last.
|
|
|
|
|
|
2026-10-13 22:05:17 -05:00
|
|
|
source scripts/lib/release-version.sh
|
|
|
|
|
version_code="$release_count"
|
|
|
|
|
version_name="$release_version"
|
|
|
|
|
if $release_dirty; then
|
2026-10-13 21:20:28 -05:00
|
|
|
if ! $allow_dirty; then
|
|
|
|
|
err "the working tree has uncommitted changes; commit them or pass --allow-dirty"
|
|
|
|
|
exit 1
|
|
|
|
|
fi
|
|
|
|
|
version_name="$version_name-dirty"
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
# --- 4. The repo, the first time --------------------------------------------
|
|
|
|
|
|
2026-10-13 22:05:17 -05:00
|
|
|
repo_url="${FDROID_REPO_URL:-}"
|
|
|
|
|
repo_url="${repo_url%/}"
|
2026-10-13 21:29:19 -05:00
|
|
|
case "$repo_url" in
|
|
|
|
|
"" | */repo) ;;
|
|
|
|
|
*) err "FDROID_REPO_URL must end in /repo (fdroidserver requires it), e.g. $repo_url/repo"; exit 1 ;;
|
|
|
|
|
esac
|
|
|
|
|
|
2026-10-13 21:20:28 -05:00
|
|
|
if [ ! -f "$dir/config.yml" ]; then
|
2026-10-13 21:29:19 -05:00
|
|
|
if [ -z "$repo_url" ]; then
|
2026-10-13 21:20:28 -05:00
|
|
|
err "no F-Droid repo in $dir yet; set FDROID_REPO_URL to the address $dir/repo will be served at"
|
|
|
|
|
exit 1
|
|
|
|
|
fi
|
|
|
|
|
info "creating the F-Droid repo in $dir"
|
|
|
|
|
(cd "$dir" && fdroid init --android-home "$ANDROID_HOME" --no-prompt -q)
|
|
|
|
|
cat >>"$dir/config.yml" <<EOF
|
|
|
|
|
|
2026-10-13 21:29:19 -05:00
|
|
|
repo_url: $repo_url
|
2026-10-13 21:20:28 -05:00
|
|
|
repo_name: mtgc
|
|
|
|
|
repo_description: >-
|
|
|
|
|
Releases of mtgc, a personal Magic: The Gathering library catalog.
|
|
|
|
|
archive_older: 3
|
|
|
|
|
EOF
|
|
|
|
|
# `fdroid update` copies this into repo/icons/ itself.
|
|
|
|
|
cp src-tauri/icons/icon.png "$dir/icon.png"
|
|
|
|
|
fi
|
2026-10-13 21:29:19 -05:00
|
|
|
# A later FDROID_REPO_URL moves the repo to a new address.
|
|
|
|
|
if [ -n "$repo_url" ] && ! grep -qxF "repo_url: $repo_url" "$dir/config.yml"; then
|
|
|
|
|
info "setting repo_url to $repo_url"
|
|
|
|
|
sed -i "s|^repo_url:.*|repo_url: $repo_url|" "$dir/config.yml"
|
|
|
|
|
fi
|
2026-10-13 21:20:28 -05:00
|
|
|
|
|
|
|
|
if [ ! -f "$dir/metadata/$APP_ID.yml" ]; then
|
|
|
|
|
mkdir -p "$dir/metadata"
|
|
|
|
|
cat >"$dir/metadata/$APP_ID.yml" <<'EOF'
|
|
|
|
|
Categories:
|
|
|
|
|
- Games
|
|
|
|
|
License: MIT
|
|
|
|
|
AuthorName: Garrett Mills
|
|
|
|
|
SourceCode: https://code.garrettmills.dev/garrettmills/mtgc
|
|
|
|
|
|
|
|
|
|
Name: mtgc
|
|
|
|
|
Summary: 'Personal Magic: The Gathering library catalog'
|
|
|
|
|
Description: |-
|
|
|
|
|
Catalog your Magic: The Gathering collection: rapid fuzzy card entry, rich
|
|
|
|
|
search (colors, commander color identity, mana value, type, price, format
|
|
|
|
|
legality) and deck organization. Works offline, and syncs between devices
|
|
|
|
|
through an mtgc sync server.
|
|
|
|
|
EOF
|
|
|
|
|
fi
|
|
|
|
|
# Release builds shorten resource paths, so `fdroid update` can't find the
|
|
|
|
|
# launcher icon inside the APK; hand it over as metadata instead.
|
|
|
|
|
if [ ! -f "$dir/metadata/$APP_ID/en-US/icon.png" ]; then
|
|
|
|
|
mkdir -p "$dir/metadata/$APP_ID/en-US"
|
|
|
|
|
cp src-tauri/icons/icon.png "$dir/metadata/$APP_ID/en-US/icon.png"
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
# --- 5. Only ever move forward ----------------------------------------------
|
|
|
|
|
|
|
|
|
|
newest=0
|
|
|
|
|
for apk in "$dir"/repo/"${APP_ID}"_*.apk "$dir"/archive/"${APP_ID}"_*.apk; do
|
|
|
|
|
[ -e "$apk" ] || continue
|
|
|
|
|
code="${apk##*_}"
|
|
|
|
|
code="${code%.apk}"
|
|
|
|
|
[ "$code" -gt "$newest" ] && newest="$code"
|
|
|
|
|
done
|
|
|
|
|
if [ "$version_code" -le "$newest" ]; then
|
|
|
|
|
err "versionCode $version_code is not newer than $newest, already in the repo; commit something first"
|
|
|
|
|
exit 1
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
# --- 6. Build ---------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
info "building $version_name (versionCode $version_code)"
|
|
|
|
|
outputs="src-tauri/gen/android/app/build/outputs/apk/universal/release"
|
|
|
|
|
rm -rf "$outputs"
|
|
|
|
|
cargo tauri android build --apk --target aarch64 --ci \
|
|
|
|
|
--config "{\"version\":\"$version_name\",\"bundle\":{\"android\":{\"versionCode\":$version_code}}}"
|
|
|
|
|
|
|
|
|
|
apk="$(find "$outputs" -name '*.apk' | head -1)"
|
|
|
|
|
if [ -z "$apk" ]; then
|
|
|
|
|
err "the build produced no APK under $outputs"
|
|
|
|
|
exit 1
|
|
|
|
|
fi
|
|
|
|
|
case "$apk" in
|
|
|
|
|
*unsigned*) err "the APK came out unsigned; check $key_props"; exit 1 ;;
|
|
|
|
|
esac
|
|
|
|
|
"$apksigner" verify "$apk"
|
|
|
|
|
|
|
|
|
|
# --- 7. Publish -------------------------------------------------------------
|
|
|
|
|
|
2026-10-13 21:29:19 -05:00
|
|
|
published="$dir/repo/${APP_ID}_${version_code}.apk"
|
|
|
|
|
cp "$apk" "$published"
|
2026-10-13 21:20:28 -05:00
|
|
|
info "updating the repo index"
|
2026-10-13 21:29:19 -05:00
|
|
|
if ! (cd "$dir" && fdroid update -q); then
|
|
|
|
|
# Take the APK back out, or the next run would think it's already published.
|
|
|
|
|
rm -f "$published"
|
|
|
|
|
err "fdroid update failed; nothing was published"
|
|
|
|
|
exit 1
|
|
|
|
|
fi
|
2026-10-13 21:20:28 -05:00
|
|
|
|
|
|
|
|
# --- 8. Where to point F-Droid ----------------------------------------------
|
|
|
|
|
|
|
|
|
|
cfg() { sed -n "s/^$1: *//p" "$dir/config.yml" | head -1; }
|
|
|
|
|
fingerprint="$(keytool -list -v -keystore "$dir/$(cfg keystore)" -storepass "$(cfg keystorepass)" \
|
|
|
|
|
-alias "$(cfg repo_keyalias)" | sed -n 's/^[[:space:]]*SHA256: *//p' | tr -d ':')"
|
|
|
|
|
|
|
|
|
|
info "published $version_name"
|
|
|
|
|
cat <<EOF
|
|
|
|
|
|
|
|
|
|
Serve $dir/repo at $(cfg repo_url)
|
|
|
|
|
(not $dir itself — it holds the repo signing key). Add this to F-Droid:
|
|
|
|
|
|
|
|
|
|
$(cfg repo_url)?fingerprint=$fingerprint
|
|
|
|
|
EOF
|