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>
118 lines
5.1 KiB
Bash
Executable File
118 lines
5.1 KiB
Bash
Executable File
#!/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"
|