diff --git a/docker-android/.gitignore b/docker-android/.gitignore new file mode 100644 index 0000000..f3367ab --- /dev/null +++ b/docker-android/.gitignore @@ -0,0 +1,3 @@ +# Chrome APK payload (~258 MB) — build input, not committed. Re-obtain and verify +# per README.md before building. The published image carries them baked in. +chrome/ diff --git a/docker-android/Dockerfile b/docker-android/Dockerfile new file mode 100644 index 0000000..fcad8d0 --- /dev/null +++ b/docker-android/Dockerfile @@ -0,0 +1,19 @@ +FROM budtmo/docker-android:emulator_14.0 + +LABEL maintainer="Click & Mortar " + +# Real, modern mobile Chrome on the Android 14 emulator. +# +# The stock budtmo emulator_14.0 system image bundles a frozen Google Chrome +# 113 (May 2023). This image bakes in an authentic, Google-signed Chrome +# 150.0.7871.63 (x86_64) + its matching Trichrome Library so the emulator runs +# a current mobile Chrome. The emulator ABI is x86_64,arm64-v8a but arm64 Chrome +# crashes under NDK translation, so the x86_64 build is mandatory. +# +# The APKs are the build INPUT (see README.md — provenance + SHA-256 + +# signature-cert verification); they are gitignored, not committed. They are +# installed onto the emulator AFTER it boots (no adb device exists at build +# time) by install-chrome.sh, which the Kubernetes Deployment invokes via a +# postStart lifecycle hook. +COPY chrome/ /opt/chrome/ +COPY install-chrome.sh /opt/install-chrome.sh diff --git a/docker-android/Makefile b/docker-android/Makefile new file mode 100644 index 0000000..8f1471d --- /dev/null +++ b/docker-android/Makefile @@ -0,0 +1,11 @@ +TAG = clickandmortar/docker-android:emulator_14.0-chrome150 + +all: build push + +default: build + +build: + docker build --platform linux/amd64 -f Dockerfile -t $(TAG) . + +push: + docker push $(TAG) diff --git a/docker-android/README.md b/docker-android/README.md new file mode 100644 index 0000000..520834e --- /dev/null +++ b/docker-android/README.md @@ -0,0 +1,64 @@ +# docker-android + modern Chrome + +`clickandmortar/docker-android:emulator_14.0-chrome150` + +Extends [`budtmo/docker-android:emulator_14.0`](https://hub.docker.com/r/budtmo/docker-android) +(Android 14 / API 34 emulator + Appium 2 + noVNC) by replacing the frozen +**Chrome 113** (May 2023) that ships in the system image with an authentic, +Google-signed **Chrome 150.0.7871.63 (x86_64)**. + +Used by Pilot's real-mobile-Chrome automated-test target (Appium, `target=android`); +see the Pilot repo `kubernetes/android/`. + +## Why x86_64, and why a separate Trichrome Library + +The emulator ABI is `x86_64,arm64-v8a` (arm64 via NDK translation), but arm64 +Chrome **crashes on launch** (SIGSEGV in v8/blink) under translation — the +**x86_64** build is mandatory. Modern Chrome uses the **Trichrome** model: the +Chrome `.apkm` bundle carries no native code (no `config.` split); all of it +lives in a separate, arch-specific **Trichrome Library** (`com.google.android.trichromelibrary`, +a versioned ``). So both must be installed, arch-matched and same +version. The Chrome bundle's versionCode is arch-stamped (`787106338` = x86_64; +the arm64 pair is `787106333`). + +## Payload provenance & verification (`chrome/`, gitignored) + +Downloaded from APKMirror (both `.apkm`/`.apk` are ZIPs of split APKs): + +| File in `chrome/` | Source file | versionCode | SHA-256 | +|---|---|---|---| +| `base.apk` + `split_chrome.apk` + `split_config.en.apk` + `split_config.fr.apk` + `split_on_demand.apk` | `com.android.chrome_150.0.7871.63-787106338_25lang_2feat_…apkmirror.com.apkm` | 787106338 | `72c7b86d5f96b2d69cb8af9f50cc26cc30b4f55d2c455cef253194005c6e1c11` | +| `trichrome.apk` | `com.google.android.trichromelibrary_150.0.7871.63-787106338_minAPI29(x86,x86_64)(nodpi)_…apkmirror.com.apk` | 787106338 | `2ed355038f1d301af09a347db40f34ae24657efb6dd54bd946948dc303a7d3b4` | + +**Authenticity does not rely on trusting APKMirror.** Verified with `apksigner` +against the emulator's factory Chrome (which ships in Google's system image): + +- Chrome `base.apk` signer cert SHA-256 = `f0fd6c5b410f25cb25c3b53346c8972fae30f8ee7411df910480ad6b2d60db83` + (**identical to the factory Chrome**) + Google Play Source Stamp + `3257d599a49d2c961a471ca9843f59d341a405884583fc087df4237b733bbd6d`. +- The verified Chrome declares `uses-static-library com.google.android.trichromelibrary` + version `787106338`, certDigest `b6198a8d5689b62b96a0aa3829ce2cc67d59497f78c469f8792b2cd9255490a1` + — which is exactly the Trichrome Library's signer cert. Chain closed, no external trust. + +To re-obtain: download the two files above from APKMirror, then in a shell: + +```bash +mkdir -p chrome && cd chrome +unzip -o base.apk split_chrome.apk split_config.en.apk split_config.fr.apk split_on_demand.apk +cp <…trichromelibrary…787106338…(x86,x86_64)…apk> trichrome.apk +# verify signer certs match the digests above (apksigner from any Android build-tools): +apksigner verify --print-certs base.apk # signer must be f0fd6c5b…60db83 +apksigner verify --print-certs trichrome.apk # signer must be b6198a8d…90a1 +``` + +## Build & push + +```bash +make build # docker build --platform linux/amd64 -t clickandmortar/docker-android:emulator_14.0-chrome150 . +make push # requires `docker login` as clickandmortar +``` + +The APKs are installed onto the emulator **after it boots** by `install-chrome.sh` +(the emulator has no adb device at build time). Pilot's Deployment triggers it via +a `postStart` lifecycle hook (fire-and-forget); the script is idempotent and logs +to `/tmp/install-chrome.log` in the pod. diff --git a/docker-android/install-chrome.sh b/docker-android/install-chrome.sh new file mode 100755 index 0000000..ebf6f06 --- /dev/null +++ b/docker-android/install-chrome.sh @@ -0,0 +1,50 @@ +#!/bin/bash +# Install the baked, verified Google Chrome 150 (x86_64) + Trichrome Library onto +# the Android emulator once it has finished booting. +# +# Invoked by the Deployment's postStart lifecycle hook (fire-and-forget). The +# emulator only exists at container runtime, so this cannot run at image-build +# time. Idempotent: a no-op if Chrome 150 is already installed (e.g. hook re-run). +# Always exits 0 so a transient failure never kills the pod via a failing hook; +# progress is logged to /tmp/install-chrome.log. +set -u + +ADB=/opt/android/platform-tools/adb +CHROME_DIR=/opt/chrome +WANT=150.0.7871.63 +LOG=/tmp/install-chrome.log +exec >>"$LOG" 2>&1 + +log() { echo "[install-chrome $(date -u +%H:%M:%S)] $*"; } +version() { "$ADB" shell dumpsys package com.android.chrome 2>/dev/null | grep -m1 versionName | tr -d ' \r'; } + +log "start (want $WANT)" + +case "$(version)" in + *"$WANT"*) log "Chrome $WANT already installed — nothing to do"; exit 0 ;; +esac + +# Wait for the emulator to finish booting (cold boot can take a few minutes). +"$ADB" wait-for-device +for _ in $(seq 1 120); do + [ "$("$ADB" shell getprop sys.boot_completed 2>/dev/null | tr -d ' \r')" = "1" ] && break + sleep 3 +done +log "boot_completed=$("$ADB" shell getprop sys.boot_completed 2>/dev/null | tr -d ' \r')" + +# Trichrome Library first (a versioned Chrome depends on), then +# the Chrome app splits. -g grants runtime permissions. +if ! "$ADB" install -r -g "$CHROME_DIR/trichrome.apk"; then + log "ERROR: Trichrome Library install failed"; exit 0 +fi +if ! "$ADB" install-multiple -r -g \ + "$CHROME_DIR/base.apk" \ + "$CHROME_DIR/split_chrome.apk" \ + "$CHROME_DIR/split_config.en.apk" \ + "$CHROME_DIR/split_config.fr.apk" \ + "$CHROME_DIR/split_on_demand.apk"; then + log "ERROR: Chrome splits install failed"; exit 0 +fi + +log "done — Chrome now $(version)" +exit 0