#!/usr/bin/env bash # Publish a built release APK into the Flamenet F-Droid repository. # # 🔴 **This is OUR OWN repository, not a submission to f-droid.org.** The # difference is not cosmetic: f-droid.org rebuilds from source and signs with # THEIR key, so an app already installed under our signature could not be # upgraded by it — Android refuses a differently-signed APK over an existing # install. This repo indexes the APK we already signed, so it upgrades cleanly # for everybody who sideloaded from the website. # # 🔴 **The repo working directory holds a signing keystore and its password.** # It lives OUTSIDE every git checkout on purpose — all four repos are public. # Never move it in, and never redirect (`>`) over config.yml: that file holds # the only on-disk copy of the keystore password, and this project has already # clobbered a credentials file that way once. # # ⚠️ The repo index key is permanent. Replacing it makes every client that has # added this repository reject its updates, silently, until they remove and # re-add it. Back up ~/flamenet-fdroid off this machine. # # Usage: scripts/publish-fdroid.sh [path/to/app-release.apk] set -euo pipefail FDROID_DIR="${FDROID_DIR:-$HOME/flamenet-fdroid}" FDROID_BIN="${FDROID_BIN:-}" APK="${1:-app/build/outputs/apk/release/app-release.apk}" REMOTE="${FDROID_REMOTE:-ovh}" REMOTE_DIR="${FDROID_REMOTE_DIR:-/srv/prod/messenger/fdroid}" [ -f "$APK" ] || { echo "no APK at $APK — build it first" >&2; exit 1; } [ -d "$FDROID_DIR" ] || { echo "no repo at $FDROID_DIR" >&2; exit 1; } [ -f "$FDROID_DIR/keystore.p12" ] || { echo "no keystore in $FDROID_DIR — refusing to re-init over a live repo" >&2; exit 1; } if [ -z "$FDROID_BIN" ]; then echo "set FDROID_BIN to the fdroid executable (a venv with fdroidserver)" >&2 exit 1 fi export JAVA_HOME="${JAVA_HOME:-/opt/homebrew/opt/openjdk@21}" export PATH="$JAVA_HOME/bin:$PATH" export ANDROID_HOME="${ANDROID_HOME:-$HOME/Library/Android/sdk}" # Name the file by its real version, read out of the APK rather than typed — # same rule as the update manifest. aapt2="$(find "$ANDROID_HOME/build-tools" -maxdepth 2 -name aapt2 -type f | sort -V | tail -1)" badging="$("$aapt2" dump badging "$APK")" name="$(sed -n "s/.*versionName='\([^']*\)'.*/\1/p" <<<"$badging" | head -1)" [ -n "$name" ] || { echo "could not read versionName from $APK" >&2; exit 1; } echo "publishing $APK as version $name" cp "$APK" "$FDROID_DIR/repo/flamenet-messenger-$name.apk" cd "$FDROID_DIR" # `update` re-reads every APK in repo/, rebuilds the index and signs it. Older # releases move to archive/ per `archive_older` rather than being deleted, so a # version somebody is pinned to does not vanish. "$FDROID_BIN" update --pretty echo "syncing to $REMOTE:$REMOTE_DIR" # 🔴 Exclude fdroidserver's own status/ directory. It writes update.json and # running.json containing the ABSOLUTE PATHS of the build machine — the # maintainer's home directory, username, toolchain locations — and the fact # that signing happens on that machine. Published, that is targeting data for # a supply-chain attack on the laptop holding the permanent Android release # key. fdroid's own `deploy` command excludes it; a raw rsync does not, so it # went public until an audit caught it. rsync -az --delete --exclude 'status/' -e ssh repo/ "$REMOTE:/tmp/fdroid-repo-new/" [ -d archive ] && rsync -az --delete --exclude 'status/' -e ssh archive/ "$REMOTE:/tmp/fdroid-archive-new/" || true # shellcheck disable=SC2029 ssh "$REMOTE" "set -e sudo rm -rf $REMOTE_DIR/repo && sudo mkdir -p $REMOTE_DIR sudo cp -r /tmp/fdroid-repo-new $REMOTE_DIR/repo && rm -rf /tmp/fdroid-repo-new if [ -d /tmp/fdroid-archive-new ]; then sudo rm -rf $REMOTE_DIR/archive sudo cp -r /tmp/fdroid-archive-new $REMOTE_DIR/archive && rm -rf /tmp/fdroid-archive-new fi sudo chown -R root:root $REMOTE_DIR && sudo chmod -R a+rX $REMOTE_DIR" echo echo "published. verify it is actually being served:" echo " curl -sS https://flamenetmessenger.com/fdroid/repo/index-v2.json | head -c 200"