#!/usr/bin/env bash # Build the release APK on the Mac and stage it for the Pi (SPEC §10.6, D-100). # Usage, from the main checkout: infra/publish-apk.sh # 1. refuses a dirty working tree (the version is derived from HEAD) # 2. version (SPEC §10.6): version_code = `git rev-list --count HEAD`, version_name = YYYY.MM.DD- of # HEAD's commit date (Europe/Stockholm), passed as -Pkvitt.versionCode / -Pkvitt.versionName # 3. :app:assembleRelease through tools/gradle-serial.sh (JDK 25 from Homebrew on PATH, JAVA_HOME untouched) # 4. writes into infra/pi/out/apk/: kvitt-.apk, kvitt-.mapping.txt (R8 mapping, for # reading stack traces; never served), and latest.json (written to a temp file, then renamed) # 5. keeps the newest 5 APKs (and their mappings) in infra/pi/out/apk/ # Then the owner copies the folder to the Pi and runs /opt/kvitt/install-apk.sh there (infra/README.md). # Refuses an unsigned build: without the kvitt.release.* keys in android/local.properties the APK cannot be # installed over an earlier release (D-024, D-101). set -euo pipefail repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" cd "$repo_root" [[ -z "$(git status --porcelain)" ]] || { echo "publish-apk.sh: the working tree is dirty; commit or stash first." >&2; exit 1; } version_code="$(git rev-list --count HEAD)" short_sha="$(git rev-parse --short=7 HEAD)" commit_day="$(TZ=Europe/Stockholm git show -s --date=format-local:%Y.%m.%d --format=%cd HEAD)" version_name="$commit_day-$short_sha" echo "Release $version_name (version_code $version_code)" PATH="/opt/homebrew/opt/openjdk@25/bin:$PATH" tools/gradle-serial.sh :app:assembleRelease \ "-Pkvitt.versionCode=$version_code" "-Pkvitt.versionName=$version_name" out_dir="$repo_root/android/app/build/outputs/apk/release" apk="$out_dir/app-release.apk" mapping="$repo_root/android/app/build/outputs/mapping/release/mapping.txt" if [[ ! -f "$apk" ]]; then if [[ -f "$out_dir/app-release-unsigned.apk" ]]; then echo "publish-apk.sh: the build is unsigned (no kvitt.release.* keys in android/local.properties); not published." >&2 else echo "publish-apk.sh: no APK in $out_dir" >&2 fi exit 1 fi # The APK must be the one just built: output-metadata.json carries the version code Gradle used. python3 - "$out_dir/output-metadata.json" "$version_code" <<'PY' import json, sys meta = json.load(open(sys.argv[1])) codes = {str(e.get("versionCode")) for e in meta.get("elements", [])} if sys.argv[2] not in codes: sys.exit(f"publish-apk.sh: output-metadata.json has version codes {sorted(codes)}, expected {sys.argv[2]}") PY [[ -f "$mapping" ]] || { echo "publish-apk.sh: no R8 mapping at $mapping" >&2; exit 1; } dest="$repo_root/infra/pi/out/apk" mkdir -p "$dest" file_name="kvitt-$version_code.apk" cp "$apk" "$dest/.$file_name.tmp" && mv -f "$dest/.$file_name.tmp" "$dest/$file_name" cp "$mapping" "$dest/kvitt-$version_code.mapping.txt" size_bytes="$(wc -c < "$dest/$file_name" | tr -d ' ')" sha256="$(shasum -a 256 "$dest/$file_name" | cut -d' ' -f1)" released_at="$(python3 -c 'import datetime; t=datetime.datetime.now(datetime.timezone.utc); print(t.strftime("%Y-%m-%dT%H:%M:%S.") + f"{t.microsecond // 1000:03d}Z")')" tmp="$(mktemp "$dest/.latest.json.XXXXXX")" cat > "$tmp" <@:/opt/kvitt/apk/" echo " ssh @ /opt/kvitt/install-apk.sh"