#!/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: # []. 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 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 \ [] [extra apksigner args...] and 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"