diff --git a/.gitea/scripts/fdroid.sh b/.gitea/scripts/fdroid.sh index ab0c0ab..5f9a71b 100755 --- a/.gitea/scripts/fdroid.sh +++ b/.gitea/scripts/fdroid.sh @@ -1,35 +1,32 @@ #!/bin/sh set -eu -version=$1 +version=${1:-} here=$(pwd) root=${PACKAGES_ROOT:-/var/www/packages.arasaka.software} keep=${FDROID_KEEP:-3} url=https://packages.arasaka.software/fdroid/repo fingerprint=5F52CFF4F52F4E0B6DA6CD044BFCEAF78D1D42DB24F2470602DB7F19CBFDF846 id=software.arasaka.meweb -apk=$here/releases/$version/MeWeb-$version-android.apk -test -f "$apk" if [ -z "${MEWEB_KEYSTORE_B64:-}" ] || [ -z "${MEWEB_KEYSTORE_PASS:-}" ]; then echo "::error::the repository secrets MEWEB_KEYSTORE_B64 and MEWEB_KEYSTORE_PASS are not set" exit 1 fi -major=$(echo "$version" | cut -d. -f1) -minor=$(echo "$version" | cut -d. -f2) -patch=$(echo "$version" | cut -d. -f3) -code=$((major * 10000 + minor * 100 + patch)) +if [ -n "$version" ]; then + apk=$here/releases/$version/MeWeb-$version-android.apk + test -f "$apk" + major=$(echo "$version" | cut -d. -f1) + minor=$(echo "$version" | cut -d. -f2) + patch=$(echo "$version" | cut -d. -f3) + code=$((major * 10000 + minor * 100 + patch)) +elif [ ! -e "$root/fdroid/repo" ]; then + echo "no repository at $root/fdroid/repo yet: the first release makes it" + exit 0 +fi tools/setup.sh --group fdroid -tools=$here/tools/fdroid/linux-x64 +tools=$(cd "$here/tools/fdroid/linux-x64" && pwd -P) uv=$tools/uv-x86_64-unknown-linux-gnu/uv -work=$(mktemp -d) -trap 'rm -rf "$work"' EXIT -venv=$work/venv -settled=$(date -u -d '14 days ago' +%Y-%m-%dT%H:%M:%SZ) -env -u MEWEB_KEYSTORE_B64 -u MEWEB_KEYSTORE_PASS UV_CACHE_DIR="$tools/cache" UV_PYTHON_INSTALL_DIR="$tools/python" \ - "$uv" venv -q --python 3 "$venv" -env -u MEWEB_KEYSTORE_B64 -u MEWEB_KEYSTORE_PASS UV_CACHE_DIR="$tools/cache" \ - "$uv" pip install -q --python "$venv/bin/python" --exclude-newer "$settled" fdroidserver -echo "fdroidserver $("$venv/bin/fdroid" --version), the newest released before $settled" +python=$(command -v python3) JAVA_HOME=$(ls -d "$here"/tools/jdk/linux-x64/*/ | head -n 1) ANDROID_HOME=$here/tools/android-sdk PATH=$JAVA_HOME/bin:$PATH @@ -37,6 +34,33 @@ export JAVA_HOME ANDROID_HOME PATH exec 9>>"$root/.lock" flock -w 2700 9 +work=$(mktemp -d) +trap 'rm -rf "$work"' EXIT +keyless() { + env -u MEWEB_KEYSTORE_B64 -u MEWEB_KEYSTORE_PASS UV_CACHE_DIR="$tools/cache" "$@" +} + +if [ -n "$version" ] && "$tools/current/bin/fdroid" --version > /dev/null 2>&1; then + install=$(readlink -f "$tools/current") +else + settled=$(date -u -d '14 days ago' +%Y-%m-%dT%H:%M:%SZ) + echo fdroidserver | keyless "$uv" pip compile -q --python "$python" --exclude-newer "$settled" --generate-hashes \ + --no-header --no-annotate -o "$work/lock.txt" - + install=$tools/fdroidserver-$(sha256sum < "$work/lock.txt" | cut -c1-12) + if ! "$install/bin/fdroid" --version > /dev/null 2>&1; then + rm -rf "$install" + keyless "$uv" venv -q --python "$python" "$install" + keyless "$uv" pip sync -q --python "$install/bin/python" "$work/lock.txt" + cp "$work/lock.txt" "$install/lock.txt" + fi +fi +tool=$("$install/bin/fdroid" --version) +stamp=$( { cat "$install/lock.txt"; cd packaging/fdroid && find config.yml icon.png metadata -type f | LC_ALL=C sort | xargs sha256sum; } | sha256sum | cut -d' ' -f1) +if [ -z "$version" ] && [ "$(cat "$(readlink -f "$root/fdroid/repo")/../built-from" 2>/dev/null)" = "$stamp" ]; then + echo "nothing new: the repository is already built with fdroidserver $tool and this listing" + exit 0 +fi +echo "fdroidserver $tool, $(basename "$install")" (umask 077 && printf '%s' "$MEWEB_KEYSTORE_B64" | base64 -d > "$work/meweb.keystore") MEWEB_KEYSTORE=$work/meweb.keystore @@ -46,13 +70,16 @@ held=$(keytool -J-Duser.language=en -list -v -keystore "$MEWEB_KEYSTORE" -storep test "$held" = "$fingerprint" state=$root/.fdroid -snap=$state/$(date -u +%Y%m%d%H%M%S)-$version +snap=$state/$(date -u +%Y%m%d%H%M%S)-${version:-tools} mkdir -p "$snap/repo" "$snap/metadata" "$snap/tmp" if [ -e "$root/fdroid/repo" ]; then prior=$(readlink -f "$root/fdroid/repo")/.. for file in "$prior"/repo/*.apk; do [ -e "$file" ] && ln "$file" "$snap/repo/" done + for icons in "$prior"/repo/icons*; do + [ -d "$icons" ] && cp -a "$icons" "$snap/repo/" + done cp -a "$prior/metadata/." "$snap/metadata/" cp "$prior/repo/index-v2.json" "$snap/repo/" if [ -f "$prior/tmp/apkcache.json" ]; then cp "$prior/tmp/apkcache.json" "$snap/tmp/"; fi @@ -61,15 +88,19 @@ cp packaging/fdroid/config.yml "$snap/config.yml" chmod 600 "$snap/config.yml" cp packaging/fdroid/icon.png "$snap/icon.png" cp -r packaging/fdroid/metadata/. "$snap/metadata/" -rm -f "$snap/repo/${id}_$code.apk" -cp "$apk" "$snap/repo/${id}_$code.apk" +if [ -n "$version" ]; then + rm -f "$snap/repo/${id}_$code.apk" + cp "$apk" "$snap/repo/${id}_$code.apk" +fi (cd "$snap/repo" && ls "$id"_*.apk | sed 's/.*_\([0-9]*\)\.apk$/\1/' | sort -n | head -n "-$keep" | while read -r old; do rm -f "${id}_$old.apk" done) +newest=$(cd "$snap/repo" && ls "$id"_*.apk | sed 's/.*_\([0-9]*\)\.apk$/\1/' | sort -n | tail -n 1) -(cd "$snap" && "$venv/bin/fdroid" update) +(cd "$snap" && "$install/bin/fdroid" update) rm -rf "$snap/repo/status" chmod -R u=rwX,go=rX "$snap/repo" +echo "$stamp" > "$snap/built-from" mkdir -p "$root/fdroid" ln -sfn "../.fdroid/$(basename "$snap")/repo" "$root/fdroid/repo.new" @@ -77,13 +108,19 @@ mv -T -f "$root/fdroid/repo.new" "$root/fdroid/repo" ls -1d "$state"/*/ | sort | head -n -3 | while read -r old; do find "$old" -maxdepth 0 -mmin +1440 -exec rm -rf {} + done -echo "published ${id}_$code.apk in $root/fdroid/repo" +echo "published $root/fdroid/repo, newest ${id}_$newest.apk, with fdroidserver $tool" if [ "$root" = /var/www/packages.arasaka.software ]; then curl -fsS "$url/entry.jar" -o "$work/entry.jar" signed=$(keytool -J-Duser.language=en -printcert -jarfile "$work/entry.jar" | sed -n 's/^.*SHA256: *//p' | head -n 1 | tr -d ':') test "$signed" = "$fingerprint" - curl -fsS "$url/index-v2.json" | grep -Eq "\"versionCode\": ?$code\b" - curl -fsSI "$url/${id}_$code.apk" > /dev/null - echo "live at $url: $id $version ($code)" + curl -fsS "$url/index-v2.json" | grep -Eq "\"versionCode\": ?$newest\b" + curl -fsSI "$url/${id}_$newest.apk" > /dev/null + echo "live at $url: newest $id $newest" fi + +ln -sfn "$(basename "$install")" "$tools/current.new" +mv -T -f "$tools/current.new" "$tools/current" +for old in "$tools"/fdroidserver-*; do + [ "$old" = "$install" ] || rm -rf "$old" +done diff --git a/.gitea/workflows/fdroid.yml b/.gitea/workflows/fdroid.yml new file mode 100644 index 0000000..957dd17 --- /dev/null +++ b/.gitea/workflows/fdroid.yml @@ -0,0 +1,22 @@ +name: F-Droid tools + +on: + schedule: + - cron: '23 5 * * *' + workflow_dispatch: + +jobs: + refresh: + name: The newest F-Droid tools, tried on the repository + runs-on: nuvola + steps: + - uses: actions/checkout@v4 + + - name: Prepare + run: .gitea/scripts/prepare.sh + + - name: Rebuild the repository on packages.arasaka.software when the tools or the listing changed + env: + MEWEB_KEYSTORE_B64: ${{ secrets.MEWEB_KEYSTORE_B64 }} + MEWEB_KEYSTORE_PASS: ${{ secrets.MEWEB_KEYSTORE_PASS }} + run: .gitea/scripts/fdroid.sh diff --git a/CLAUDE.md b/CLAUDE.md index ecb4e12..b86d708 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -44,7 +44,8 @@ is root-owned, so CI uses the runner user's own `~/.dotnet` (passolo's `tools/re | `MEWEB_KEYSTORE` | release step | a temporary file in `$RUNNER_TEMP` decoded by `.gitea/scripts/android.sh`, removed on exit | `build.yml` runs on every push to `master`; `release.yml` on a `v*` tag (publishes the Gitea release) or by hand -(`publish: false` builds every platform and deploys the server only). Both run on the Arasaka org runner `nuvola`, +(`publish: false` builds every platform and deploys the server only); `fdroid.yml` daily at 05:23 UTC or by hand +(the F-Droid tools, below). All three run on the Arasaka org runner `nuvola`, user `gitea-runner`, home `/var/lib/gitea-runner`: `tools/sdk.sh` installs the SDK from `global.json` into its `~/.dotnet` (the system SDK in `/opt/dotnet` is older and root-owned), and the JDK, nfpm and the Android SDK are cached in `~/.cache/meweb`. There is deliberately no `pull_request` trigger: the runner is the production box. @@ -176,20 +177,24 @@ The Android release key: alias `meweb`, certificate SHA-256 - **The APK reaches an F-Droid repository on packages.arasaka.software from the same job** (31 Oct 2026), after the release is published, so the repository can lag the release page but never lead it. `.gitea/scripts/fdroid.sh` installs fdroidserver with uv from `setup.json` (its download cache in `tools/fdroid`, cached like the JDK), because - the box has no `ensurepip` and the apt package would bring 242 others. **Nothing pins fdroidserver** (owner, 31 Oct - 2026: no patching it from time to time): each run installs the newest fdroidserver, and the newest of everything it - needs, published at least 14 days before the run, so a bad upload has usually been pulled before the job sees it, - and prints the version it got. It installs into a fresh environment in a temporary folder with the signing key taken - out of its environment; nothing is kept between runs, because an environment's scripts carry the absolute path they - were made at, and 0.15.1's step failed with `fdroid: not found` on a kept one. A new fdroidserver that breaks - `fdroid update` fails this step after the release is published: the repository then lags, and the log names the - version to look at. + the box has no `ensurepip` and the apt package would bring 242 others. **Nothing pins fdroidserver; a daily job + keeps it current** (owner, 31 Oct 2026: a daily service, not patching it from time to time). `fdroid.yml` runs the + same script with no version at 05:23 UTC: it resolves the newest fdroidserver, and the newest of everything it + needs, published at least 14 days earlier (a bad upload has usually been pulled by then), against the box's own + `python3`, into a hash-locked list; a new list gets its own install, `fdroidserver-` in the cache's + real path (an install's scripts carry the absolute path they were made at; 0.15.1 failed with `fdroid: not found` on + one made from a deleted folder). When that list or the listing in `packaging/fdroid/` differs from what the live + repository was built from (`built-from` beside each snapshot), it rebuilds the repository with it, and only once + that is live and verified does the install become `current` and the older ones go. A release uses `current` and + downloads nothing, so a new fdroidserver that breaks fails the daily job, never a release; the job's log names the + version. Every install runs with the signing key out of its environment. It signs the repository with the APK's own key (`MEWEB_KEYSTORE_B64` and `MEWEB_KEYSTORE_PASS`, alias `meweb`), so the F-Droid fingerprint is the certificate's `5F52CFF4…FDF846` and there is no second key to lose; it refuses any other. It builds a new snapshot in `/var/www/packages.arasaka.software/.fdroid/` from the live one (the APKs as hard links, so `rm` then `cp` as above; - the metadata, `apkcache.json` and the previous `index-v2.json`, which is where fdroidserver reads each APK's first - date), adds this version, keeps the newest three, runs `fdroid update`, deletes `repo/status/` (it names the + the metadata, the icons, `apkcache.json` and the previous `index-v2.json`, which is where fdroidserver reads each + APK's first date; without the icons a rebuild with no new APK fails, because the cache skips extracting them), adds + this version (a release) or nothing (the daily job), keeps the newest three, runs `fdroid update`, deletes `repo/status/` (it names the build machine's paths and `/etc/issue.net`), swaps the `fdroid/repo` link and checks the live signature. The key was already never to be regenerated; now everyone who added the repository trusts only its fingerprint too. `packaging/fdroid/` holds the repository's settings, its icon (the Arasaka emblem) and the app's listing in English and Italian; the download