feat: an F-Droid repository at learn.halkyon.app
Releases also publish the APK in the project's own F-Droid repository, https://learn.halkyon.app/fdroid/repo. tool/fdroid_repo.sh adds the APK, signs the indexes with the repository's own key and uploads them to ramiel as the user fdroid, whose key can only write into the folder Caddy serves (rrsync), the APKs first and the indexes last. tool/release.sh checks that key and its password before building and publishes after the Forgejo release. - tool/release/fdroid/ holds the repository's settings (Halkyon, the snap's icon) and the app's listing (the snap's text), with the anti-features TetheredNet (learn.halkyon.eu only, for now) and NonFreeNet (YouTube, the server's SSPL) declared with their reasons, in F-Droid's own definitions and with its icons (fdroiddata, AGPL v3). - The index key's certificate is pinned: a new key would make the F-Droid app refuse the repository. - fdroidserver v2.4.5, since Ubuntu's v2.2.1 knows neither TetheredNet nor reasons. README and AGENTS.md describe it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
475ff99ead
commit
ae2496335d
9 changed files with 209 additions and 9 deletions
107
tool/fdroid_repo.sh
Executable file
107
tool/fdroid_repo.sh
Executable file
|
|
@ -0,0 +1,107 @@
|
|||
#!/usr/bin/env bash
|
||||
# Updates the F-Droid repository at https://learn.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 ramiel as the user fdroid, whose key can only
|
||||
# write into the folder Caddy serves (rrsync). tool/release.sh 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, a JDK, the Android SDK,
|
||||
# and the ssh host ramiel-fdroid.
|
||||
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://learn.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; 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"
|
||||
cp -r tool/release/fdroid/. "$working/"
|
||||
# fdroid refuses a config.yml others can read.
|
||||
chmod 600 "$working/config.yml"
|
||||
cp snap/gui/halkyon-learn.png "$working/halkyon.png"
|
||||
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"
|
||||
|
|
@ -2,17 +2,19 @@
|
|||
# Builds a release and publishes it: the Android APK (one for every phone, signed with
|
||||
# the app's key) and the Linux snap, both connecting to learn.halkyon.eu, the project's
|
||||
# instance; a list of their checksums signed with GPG; all of it as a release on
|
||||
# code.halkyon.app, and the snap in the Snap Store's edge channel. Run on a developer's
|
||||
# laptop, never in CI: the app's key never leaves it.
|
||||
# code.halkyon.app, the APK in the F-Droid repository at learn.halkyon.app
|
||||
# (tool/fdroid_repo.sh), and the snap in the Snap Store's edge channel. Run on a
|
||||
# developer's laptop, never in CI: the app's key never leaves it.
|
||||
#
|
||||
# tool/release.sh v0.1.0
|
||||
#
|
||||
# Before: the version, with a build number above the last release's, in
|
||||
# halkyon_learn_flutter/pubspec.yaml on main, pushed; the tag signed on that commit and
|
||||
# pushed (git tag -s v0.1.0 -m 'Halkyon Learn 0.1.0' && git push origin v0.1.0);
|
||||
# `snapcraft login` with the project's account. The script asks for the key's password
|
||||
# and a Forgejo token (scope write:repository) first, checks both, and then runs
|
||||
# unattended until GPG asks for its passphrase; neither is ever written down.
|
||||
# `snapcraft login` with the project's account. The script asks for the key's password,
|
||||
# the F-Droid repository key's password and a Forgejo token (scope write:repository)
|
||||
# first, checks them, and then runs unattended until GPG asks for its passphrase (and
|
||||
# ssh for the upload key's, unless an agent holds it); none is ever written down.
|
||||
#
|
||||
# The app's key is a PKCS #12 keystore at $HALKYON_ANDROID_KEYSTORE (default below), its
|
||||
# alias $HALKYON_ANDROID_KEY_ALIAS. Its certificate's SHA-256 is committed in
|
||||
|
|
@ -25,6 +27,7 @@ readonly forgejo='https://code.halkyon.app'
|
|||
readonly repository='project-halkyon/halkyon_learn'
|
||||
readonly pubspec='halkyon_learn_flutter/pubspec.yaml'
|
||||
readonly certificate_file='tool/release/android-certificate.sha256'
|
||||
readonly fdroid_certificate_file='tool/release/fdroid-index-certificate.sha256'
|
||||
keystore="${HALKYON_ANDROID_KEYSTORE:-$HOME/.local/share/halkyon-learn/android-release.p12}"
|
||||
readonly key_alias="${HALKYON_ANDROID_KEY_ALIAS:-halkyon-learn}"
|
||||
|
||||
|
|
@ -111,6 +114,13 @@ key_certificate=$(HALKYON_ANDROID_KEYSTORE_PASSWORD="$password" "$keytool" -list
|
|||
[ "$key_certificate" = "$certificate" ] ||
|
||||
fail "The key $key_alias in $keystore isn't the one in $certificate_file."
|
||||
|
||||
read -rsp "Password of the F-Droid repository's key: " HALKYON_FDROID_KEYSTORE_PASSWORD
|
||||
echo
|
||||
export HALKYON_FDROID_KEYSTORE_PASSWORD
|
||||
tool/fdroid_repo.sh --check
|
||||
fdroid_fingerprint=$(grep -v '^#' "$fdroid_certificate_file" | tr -d ' :\n' | tr 'a-f' 'A-F')
|
||||
readonly fdroid_fingerprint
|
||||
|
||||
read -rsp "Forgejo token for $forgejo (write:repository): " token
|
||||
echo
|
||||
# The token goes to curl in a configuration read from a file descriptor, so it never
|
||||
|
|
@ -187,6 +197,8 @@ the project's instance.
|
|||
|
||||
- **Android:** \`$apk\`, one APK for every phone, signed with the app's key; the \
|
||||
SHA-256 of its certificate is \`$certificate\`.
|
||||
- **F-Droid:** the same APK in the repository \
|
||||
https://learn.halkyon.app/fdroid/repo?fingerprint=$fdroid_fingerprint
|
||||
- **Linux:** \`$snap\`, also in the Snap Store: \`snap install halkyon-learn --edge\`.
|
||||
|
||||
\`SHA256SUMS\` lists the files' checksums, signed with the GPG key \`$signer\` \
|
||||
|
|
@ -201,6 +213,10 @@ for file in "$apk" "$snap" SHA256SUMS SHA256SUMS.asc; do
|
|||
done
|
||||
echo " $forgejo/$repository/releases/tag/$tag"
|
||||
|
||||
step 'Publishing the APK in the F-Droid repository'
|
||||
tool/fdroid_repo.sh "$out/$apk"
|
||||
unset HALKYON_FDROID_KEYSTORE_PASSWORD
|
||||
|
||||
# Last, as the Store may hold a revision for review; the release above stands either way.
|
||||
step "Uploading the snap to the Snap Store's edge channel (as $account)"
|
||||
snapcraft upload --release=edge "$out/$snap" ||
|
||||
|
|
|
|||
5
tool/release/fdroid-index-certificate.sha256
Normal file
5
tool/release/fdroid-index-certificate.sha256
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
# The SHA-256 of the certificate that signs the indexes of the F-Droid repository at
|
||||
# https://learn.halkyon.app/fdroid/repo (key alias halkyon-fdroid), as keytool prints it.
|
||||
# The F-Droid app knows the repository by it; tool/fdroid_repo.sh uploads no index
|
||||
# signed otherwise.
|
||||
E8:08:27:88:75:6F:6B:7E:68:2A:A2:2F:CE:AD:24:B5:3E:2A:8B:09:12:AD:66:D6:5E:8C:2D:4F:07:BB:94:B0
|
||||
26
tool/release/fdroid/config.yml
Normal file
26
tool/release/fdroid/config.yml
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
# The F-Droid repository at https://learn.halkyon.app/fdroid/repo, kept by
|
||||
# tool/fdroid_repo.sh: this folder is copied into the repository's working
|
||||
# folder (outside Git) before every update. Nothing secret is in here: the index
|
||||
# key's path and password come from the environment.
|
||||
repo_url: https://learn.halkyon.app/fdroid/repo
|
||||
archive_url: https://learn.halkyon.app/fdroid/archive
|
||||
repo_name: Halkyon
|
||||
repo_description: >-
|
||||
Halkyon's apps, signed by their maintainer: the same APKs as on
|
||||
code.halkyon.app.
|
||||
archive_name: Halkyon (archive)
|
||||
archive_description: >-
|
||||
Older versions of Halkyon's apps.
|
||||
# The snap's icon, which tool/fdroid_repo.sh copies beside this file.
|
||||
repo_icon: halkyon.png
|
||||
archive_icon: halkyon.png
|
||||
# The newest three versions of each app stay in the repository, older ones move
|
||||
# to the archive.
|
||||
archive_older: 3
|
||||
|
||||
keystore: $HALKYON_FDROID_KEYSTORE
|
||||
repo_keyalias: halkyon-fdroid
|
||||
# A PKCS #12 keystore has one password, for the store and its key.
|
||||
keystorepass: {env: HALKYON_FDROID_KEYSTORE_PASSWORD}
|
||||
keypass: {env: HALKYON_FDROID_KEYSTORE_PASSWORD}
|
||||
keydname: CN=Halkyon, O=Halkyon
|
||||
13
tool/release/fdroid/config/antiFeatures.yml
Normal file
13
tool/release/fdroid/config/antiFeatures.yml
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
# The anti-features the repository's apps declare, as the main F-Droid
|
||||
# repository defines them: names, descriptions and icons from fdroiddata
|
||||
# (https://gitlab.com/fdroid/fdroiddata, config/antiFeatures.yml and the icons
|
||||
# beside it). The two icons are F-Droid's, under the GNU AGPL v3, fdroiddata's
|
||||
# licence. fdroid copies them into the repository's icons.
|
||||
NonFreeNet:
|
||||
name: Non-Free Network Services
|
||||
description: This app promotes or depends entirely on a non-free network service
|
||||
icon: ic_antifeature_nonfreenet.png
|
||||
TetheredNet:
|
||||
name: Tethered Network Services
|
||||
description: This app depends entirely on a certain instance of a network service
|
||||
icon: ic_antifeature_tetherednet.png
|
||||
3
tool/release/fdroid/config/categories.yml
Normal file
3
tool/release/fdroid/config/categories.yml
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
# The categories the repository's apps are listed in.
|
||||
Education:
|
||||
name: Education
|
||||
BIN
tool/release/fdroid/config/ic_antifeature_nonfreenet.png
Normal file
BIN
tool/release/fdroid/config/ic_antifeature_nonfreenet.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 7.3 KiB |
BIN
tool/release/fdroid/config/ic_antifeature_tetherednet.png
Normal file
BIN
tool/release/fdroid/config/ic_antifeature_tetherednet.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.6 KiB |
25
tool/release/fdroid/metadata/app.halkyon.learn.yml
Normal file
25
tool/release/fdroid/metadata/app.halkyon.learn.yml
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
# Halkyon Learn's listing in the F-Droid repository: the snap's summary and
|
||||
# description (snap/snapcraft.yaml), and its anti-features with their reasons.
|
||||
Categories:
|
||||
- Education
|
||||
License: AGPL-3.0-or-later
|
||||
AuthorName: Halkyon
|
||||
AuthorEmail: support@halkyon.app
|
||||
WebSite: https://learn.halkyon.app
|
||||
SourceCode: https://code.halkyon.app/project-halkyon/halkyon_learn
|
||||
|
||||
AntiFeatures:
|
||||
TetheredNet: It connects to learn.halkyon.eu, the project's instance, and can't be
|
||||
pointed at another one yet.
|
||||
NonFreeNet: YouTube videos in study material play through YouTube, and the
|
||||
server's licence (SSPL) isn't a free licence by F-Droid's definition.
|
||||
|
||||
Name: Halkyon Learn
|
||||
Summary: Learn with flashcards, studied offline and synced between your devices
|
||||
Description: |-
|
||||
Halkyon Learn is a spaced-repetition learning app: studysets of flashcards with
|
||||
rich text, notes, learning paths and PDFs, reviewed on an FSRS schedule. It works
|
||||
offline and syncs between your devices.
|
||||
|
||||
It starts with an anonymous account; signing in with GitHub later keeps
|
||||
everything. It connects to learn.halkyon.eu, the project's instance.
|
||||
Loading…
Reference in a new issue