Build the shared F-Droid publishing tooling: sign, index, publish
Adds sign-apk.sh (align-check + apksigner), fdroid-update.sh (fdroid update over a binary repo, signing the index with its own separate key), publish.sh (S3-compatible upload) and the composite action tying them together, matching weblib-ci's shape. zipalign is not a standalone nixpkgs attribute; resolved by measurement rather than assumption that Gradle/AGP already aligns its output (Android's own docs, corroborated by fdroidserver never shelling out to zipalign) and added check_zip_alignment.py as the safety net, since apksigner itself does not check alignment at all. nixpkgs is pinned via a committed nixpkgs-pin revision, since this repo has no flake.lock of its own for with-nixpkgs.sh to read (fdroid-repo-ci#1, weblib-ci#9) -- a missing or malformed pin fails loudly rather than degrading to an unpinned nix shell. No consumer repo, bucket, hostname or channel name is hard-coded: which repo holds the keys and runs the publish has already changed once during this issue and may again, so every example uses a placeholder. 42 offline/local-only test assertions across five suites (run_tests.sh), including fault injection on the alignment check, the password/argument validation, and the with-nixpkgs.sh fail-loud paths. Co-authored-by: bit <bit@das-labor.org>
This commit is contained in:
Executable
+117
@@ -0,0 +1,117 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# Align-check and sign a prebuilt, unsigned release APK with a keystore the
|
||||
# caller supplies. Exists because binary-repo F-Droid mode never builds
|
||||
# anything itself (fdroid-repo-ci#1) -- something still has to turn a Gradle
|
||||
# build's output into a signed APK before `fdroid update` can read its
|
||||
# certificate.
|
||||
#
|
||||
# KEYSTORE_PASSWORD=... bash sign-apk.sh \
|
||||
# unsigned.apk signed.apk keystore.jks release-key KEYSTORE_PASSWORD
|
||||
#
|
||||
# Positional args: <unsigned.apk> <signed.apk> <keystore> <key-alias>
|
||||
# <keystore-pass-env> [<key-pass-env>]. The last two are *names* of
|
||||
# environment variables to read the passwords from, not the passwords
|
||||
# themselves -- an argument shows up in the job log and in `ps`, a keystore
|
||||
# password must not. If <key-pass-env> is omitted, the keystore password is
|
||||
# reused as the key password, which is the common case for a keystore holding
|
||||
# one key. Naming the variable rather than fixing it lets a caller with
|
||||
# several keystores (an APK key here, a separate repo index key for
|
||||
# fdroid-update.sh) avoid a collision without this repo inventing a
|
||||
# convention for it.
|
||||
#
|
||||
# Any arguments after the fifth/sixth are forwarded verbatim to both
|
||||
# `apksigner sign` and `apksigner verify`, e.g. `--min-sdk-version`/
|
||||
# `--max-sdk-version` to bypass real manifest parsing for a fixture that has
|
||||
# no valid binary AndroidManifest.xml -- see the test suite.
|
||||
#
|
||||
# ## Alignment, measured rather than assumed (fdroid-repo-ci#1)
|
||||
#
|
||||
# `zipalign` is not a standalone nixpkgs attribute -- only `androidenv`'s
|
||||
# composed build-tools carry it, and that pulls a full versioned Android SDK
|
||||
# for one binary. Before adding that dependency:
|
||||
#
|
||||
# developer.android.com/tools/zipalign states plainly that a build made
|
||||
# through the Android Gradle Plugin -- which `flutter build apk` drives
|
||||
# underneath, same as a plain Android Studio build -- already aligns its zip
|
||||
# entries via zipflinger, and that `zipalign` is only needed afterwards to
|
||||
# *verify* that, not to perform it. `apksigner` does not check or complain
|
||||
# about alignment either way: signing and verifying a deliberately misaligned
|
||||
# fixture here produced no warning from `apksigner verify --verbose` at all --
|
||||
# measured while writing this script, not inferred from the docs.
|
||||
#
|
||||
# So this script does not depend on `zipalign`, but it does not take the AGP
|
||||
# claim on faith either: `check_zip_alignment.py`, a few lines against the
|
||||
# stdlib `zipfile` module, checks every *uncompressed* entry -- the only ones
|
||||
# alignment affects -- before signing, and refuses to sign an APK that fails
|
||||
# it, rather than silently producing one that behaves badly on a device. If
|
||||
# that check ever trips on a real build, the fix is to pull in androidenv's
|
||||
# zipalign, not to weaken the check.
|
||||
#
|
||||
# Confidence in "AGP already aligns its output" is high (Google's own docs,
|
||||
# corroborated by fdroidserver 2.4.3's source never shelling out to zipalign
|
||||
# anywhere at all) but NOT independently confirmed here against a real
|
||||
# `flutter build apk` output -- that build was judged too expensive to run
|
||||
# for this measurement (weblib-viewer#5 already declined a CI job on the same
|
||||
# toolchain for the same reason, and this host had ~7.5G free against a
|
||||
# multi-platform Android SDK + NDK). check_zip_alignment.py is the safety net
|
||||
# for that gap: it runs against every APK this script is asked to sign, not
|
||||
# just the fixtures in the test suite.
|
||||
#
|
||||
# Pins apksigner via with-nixpkgs.sh/nixpkgs-pin, next to this script.
|
||||
set -euo pipefail
|
||||
|
||||
here=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
|
||||
|
||||
if [ "$#" -lt 5 ]; then
|
||||
cat >&2 <<'USAGE'
|
||||
usage: sign-apk.sh <unsigned.apk> <signed.apk> <keystore> <key-alias> \
|
||||
<keystore-pass-env> [<key-pass-env>] [extra apksigner args...]
|
||||
|
||||
<keystore-pass-env> and <key-pass-env> name environment variables holding the
|
||||
passwords -- they are never passed as arguments.
|
||||
USAGE
|
||||
exit 2
|
||||
fi
|
||||
|
||||
unsigned=$1
|
||||
signed=$2
|
||||
keystore=$3
|
||||
alias_name=$4
|
||||
ks_pass_env=$5
|
||||
shift 5
|
||||
|
||||
key_pass_env=$ks_pass_env
|
||||
if [ "$#" -gt 0 ] && [[ $1 != -* ]]; then
|
||||
key_pass_env=$1
|
||||
shift
|
||||
fi
|
||||
|
||||
extra_args=("$@")
|
||||
|
||||
[ -f "$unsigned" ] || { echo "sign-apk.sh: no such file: $unsigned" >&2; exit 2; }
|
||||
[ -f "$keystore" ] || { echo "sign-apk.sh: no such keystore: $keystore" >&2; exit 2; }
|
||||
: "${!ks_pass_env:?sign-apk.sh: environment variable $ks_pass_env (keystore password) is not set}"
|
||||
: "${!key_pass_env:?sign-apk.sh: environment variable $key_pass_env (key password) is not set}"
|
||||
|
||||
echo "sign-apk.sh: checking zip alignment of $unsigned"
|
||||
bash "$here/with-nixpkgs.sh" python3 python3 "$here/check_zip_alignment.py" "$unsigned"
|
||||
|
||||
echo "sign-apk.sh: signing $unsigned -> $signed"
|
||||
# apksigner's --ks-pass/--key-pass take "env:VARNAME", reading the variable's
|
||||
# *current* value at apksigner's own invocation, not ours -- pass the names
|
||||
# through so nothing beyond this script ever holds the password in an
|
||||
# argument list.
|
||||
bash "$here/with-nixpkgs.sh" apksigner apksigner sign \
|
||||
--ks "$keystore" \
|
||||
--ks-key-alias "$alias_name" \
|
||||
--ks-pass "env:$ks_pass_env" \
|
||||
--key-pass "env:$key_pass_env" \
|
||||
--out "$signed" \
|
||||
"${extra_args[@]}" \
|
||||
"$unsigned"
|
||||
|
||||
echo "sign-apk.sh: verifying $signed"
|
||||
bash "$here/with-nixpkgs.sh" apksigner apksigner verify --print-certs \
|
||||
"${extra_args[@]}" \
|
||||
"$signed"
|
||||
Reference in New Issue
Block a user