halkyon_app's WhatsNew reads what each release changed from the app's assets/whats_new/ (<version>.<language>.txt, English and German, a change a line): after an update a sheet lists every release since the one the device showed last, newest first, once; Settings shows the running version's again, above About. Nothing on a fresh install. tool/release.dart drafts the missing texts from the commits that count after "Go on?" (German marked TRANSLATE:), opens them in the editor until they fit (no mark left, F-Droid's 500 characters) and commits them with the version. The English is the Forgejo notes' What's new, each language F-Droid's changelog of the build, written at publishing and copied in by tool/fdroid_repo.sh. Learn and Train bundle their texts, Learn's 0.22.0 and Train's 0.1.0 written to start. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
156 lines
7 KiB
Shell
Executable file
156 lines
7 KiB
Shell
Executable file
#!/usr/bin/env bash
|
|
# Updates the F-Droid repository at https://halkyon.app/fdroid/repo and
|
|
# uploads it: adds an APK if one is given, signs the index with the repository's
|
|
# key, and uploads everything to the halkyon.app server as the user fdroid, whose key can only
|
|
# write into the folder Caddy serves (rrsync). tool/release.dart runs it with each
|
|
# release's APK; run alone, it publishes the repository as it is, e.g. after its
|
|
# settings changed.
|
|
#
|
|
# tool/fdroid_repo.sh [APK]
|
|
# tool/fdroid_repo.sh --check (the tools, the key and its password only)
|
|
#
|
|
# The repository's working folder, outside Git, keeps every APK released so far:
|
|
# $HALKYON_FDROID_DIR (default below). Its settings and the apps' listings come
|
|
# from tool/release/fdroid/, copied in at every run. The index key is a PKCS #12
|
|
# keystore at $HALKYON_FDROID_KEYSTORE, alias halkyon-fdroid, whose certificate's
|
|
# SHA-256 is pinned in tool/release/fdroid-index-certificate.sha256: an index
|
|
# signed otherwise would make the F-Droid app refuse the repository. Its password
|
|
# comes from $HALKYON_FDROID_KEYSTORE_PASSWORD, or is asked for; it's never
|
|
# written down. Needs fdroidserver v2.4.5 (pipx), rsync, curl, jq, a JDK, the Android
|
|
# SDK, and the ssh host ramiel-fdroid. APKs released before that the working folder
|
|
# lacks come from the repository first (restore, below).
|
|
set -euo pipefail
|
|
cd "$(git rev-parse --show-toplevel)"
|
|
|
|
readonly working="${HALKYON_FDROID_DIR:-$HOME/.local/share/halkyon-learn/fdroid}"
|
|
keystore="${HALKYON_FDROID_KEYSTORE:-$HOME/.local/share/halkyon-learn/fdroid-index.p12}"
|
|
readonly key_alias='halkyon-fdroid'
|
|
readonly pinned_file='tool/release/fdroid-index-certificate.sha256'
|
|
readonly upload_host='ramiel-fdroid'
|
|
readonly repo_url='https://halkyon.app/fdroid/repo'
|
|
|
|
fail() {
|
|
echo "✗ $*" >&2
|
|
exit 1
|
|
}
|
|
step() { echo "▸ $*"; }
|
|
|
|
[ $# -le 1 ] || fail 'Usage: tool/fdroid_repo.sh [APK | --check]'
|
|
check_only=false
|
|
apk=''
|
|
case "${1:-}" in
|
|
--check) check_only=true ;;
|
|
'') ;;
|
|
*) apk="$1" ;;
|
|
esac
|
|
|
|
for tool in fdroid rsync curl jq; do
|
|
command -v "$tool" >/dev/null || fail "$tool is missing."
|
|
done
|
|
# fdroid takes jarsigner and keytool from the PATH, apksigner from the SDK.
|
|
jdk_bin=$(dirname "$(command -v java || echo /snap/android-studio/current/jbr/bin/java)")
|
|
[ -x "$jdk_bin/jarsigner" ] && [ -x "$jdk_bin/keytool" ] ||
|
|
fail 'No JDK found (jarsigner, keytool).'
|
|
export PATH="$jdk_bin:$PATH"
|
|
export ANDROID_HOME="${ANDROID_HOME:-${ANDROID_SDK_ROOT:-$HOME/Android/Sdk}}"
|
|
|
|
[ -f "$keystore" ] || fail "No keystore at $keystore (HALKYON_FDROID_KEYSTORE)."
|
|
keystore=$(realpath "$keystore")
|
|
export HALKYON_FDROID_KEYSTORE="$keystore"
|
|
[ -f "$pinned_file" ] || fail "$pinned_file is missing."
|
|
pinned=$(grep -v '^#' "$pinned_file" | tr -d ' :\n' | tr 'a-f' 'A-F' || true)
|
|
[[ "$pinned" =~ ^[0-9A-F]{64}$ ]] || fail "$pinned_file holds no SHA-256."
|
|
|
|
if [ -z "${HALKYON_FDROID_KEYSTORE_PASSWORD:-}" ]; then
|
|
read -rsp "Password of $keystore: " HALKYON_FDROID_KEYSTORE_PASSWORD
|
|
echo
|
|
fi
|
|
export HALKYON_FDROID_KEYSTORE_PASSWORD
|
|
# The F-Droid app knows the repository by this fingerprint: the certificate's
|
|
# SHA-256 in capitals, without colons.
|
|
fingerprint=$(keytool -list -v -keystore "$keystore" -alias "$key_alias" \
|
|
-storepass:env HALKYON_FDROID_KEYSTORE_PASSWORD 2>/dev/null |
|
|
sed -n 's/^.*SHA256: *//p' | head -n 1 | tr -d ':' | tr 'a-f' 'A-F' || true)
|
|
[ -n "$fingerprint" ] || fail "Wrong password, or no key $key_alias in $keystore."
|
|
[ "$fingerprint" = "$pinned" ] ||
|
|
fail "The key $key_alias in $keystore isn't the one in $pinned_file."
|
|
if $check_only; then
|
|
exit 0
|
|
fi
|
|
|
|
step 'Updating the F-Droid repository'
|
|
mkdir -p "$working/repo"
|
|
|
|
# A working folder on another laptop, or a new one, holds none of the APKs released
|
|
# before, and the upload below mirrors it with --delete-after: so every APK the
|
|
# published indexes name (repo and archive) comes from the repository first, unless the
|
|
# folder has it, its SHA-256 checked against the index's either way. The index is the
|
|
# repository's own, over HTTPS; the check guards against a broken download, not
|
|
# against the halkyon.app server.
|
|
restore() {
|
|
local dir="$1" status name hash type
|
|
mkdir -p "$working/$dir"
|
|
status=$(curl -sSL -o "$working/$dir/.published-index" -w '%{http_code}' \
|
|
"${repo_url%/repo}/$dir/index-v1.json") || fail "Can't reach the published $dir index."
|
|
case "$status" in
|
|
200) ;;
|
|
404) return 0 ;; # nothing published there yet
|
|
*) fail "The published $dir index answered $status." ;;
|
|
esac
|
|
jq -r '.packages[][] | [.apkName, .hash, .hashType] | @tsv' "$working/$dir/.published-index" |
|
|
while IFS=$'\t' read -r name hash type; do
|
|
[ "$type" = sha256 ] || fail "$name: the index gives no SHA-256."
|
|
[[ "$name" =~ ^[A-Za-z0-9._-]+\.apk$ ]] || fail "An APK named $name in the index."
|
|
if [ ! -f "$working/$dir/$name" ]; then
|
|
echo " $dir/$name from the repository"
|
|
curl -sSfL -o "$working/$dir/$name.part" "${repo_url%/repo}/$dir/$name"
|
|
mv "$working/$dir/$name.part" "$working/$dir/$name"
|
|
fi
|
|
echo "$hash $working/$dir/$name" | sha256sum --check --quiet ||
|
|
fail "$dir/$name isn't the APK the index names."
|
|
done
|
|
rm "$working/$dir/.published-index"
|
|
}
|
|
restore repo
|
|
restore archive
|
|
cp -r tool/release/fdroid/. "$working/"
|
|
# What's new in the release as each language's changelog of its build
|
|
# (metadata/<app id>/<locale>/changelogs/<build>.txt), which tool/release.dart
|
|
# writes from the app's texts and names here; kept in the working folder after.
|
|
if [ -n "${HALKYON_FDROID_METADATA:-}" ]; then
|
|
cp -r "$HALKYON_FDROID_METADATA/." "$working/metadata/"
|
|
fi
|
|
# fdroid refuses a config.yml others can read.
|
|
chmod 600 "$working/config.yml"
|
|
# Each app's own icon in the F-Droid app (the repository's is the kingfisher,
|
|
# halkyon.png in tool/release/fdroid/): Learn's the snap's, Train's its launcher's
|
|
# source. fdroid can't find the launcher icon in a release APK, whose build shortens
|
|
# the resources' paths (res/9w.png rather than res/mipmap-hdpi/ic_launcher.png;
|
|
# fdroidserver issue 885).
|
|
for icon in app.halkyon.learn:snap/gui/halkyon-learn.png \
|
|
app.halkyon.train:apps/train/halkyon_train_flutter/assets/icon/halkyon_train_icon.png; do
|
|
mkdir -p "$working/metadata/${icon%%:*}/en-US"
|
|
cp "${icon#*:}" "$working/metadata/${icon%%:*}/en-US/icon.png"
|
|
done
|
|
if [ -n "$apk" ]; then
|
|
[ -f "$apk" ] || fail "No APK at $apk."
|
|
cp "$apk" "$working/repo/"
|
|
fi
|
|
# Names every APK as F-Droid does (<app id>_<version code>.apk) and signs the
|
|
# indexes with the repository's key.
|
|
(cd "$working" && fdroid update --rename-apks)
|
|
|
|
step "Uploading it to $upload_host"
|
|
# The APKs and icons first, the indexes last, so the F-Droid app never reads an
|
|
# index naming a file that isn't there yet.
|
|
for dir in repo archive; do
|
|
[ -d "$working/$dir" ] || continue
|
|
rsync -a --chmod=D755,F644 --exclude='index*' --exclude='entry.*' \
|
|
"$working/$dir/" "$upload_host:$dir/"
|
|
done
|
|
for dir in repo archive; do
|
|
[ -d "$working/$dir" ] || continue
|
|
rsync -a --chmod=D755,F644 --delete-after "$working/$dir/" "$upload_host:$dir/"
|
|
done
|
|
|
|
echo " $repo_url?fingerprint=$fingerprint"
|