65238bd4512a

feat: Publish ABI-aware Android release updates (v2.9.12)

time
2026-07-30 23:53 UTC · 2 months ago
machine
koh2
parent
382630e6cd40
9 files VERSION+1 −1app/build.gradle+9 −7app/src/main/java/com/peachpasswords/updates/UpdateManifest.kt+10 −5app/src/main/java/com/peachpasswords/updates/UpdateRepository.kt+3 −1app/src/test/java/com/peachpasswords/updates/UpdateManifestTest.kt+20 −2kepr.build+4 −6scripts/kepr-build-apk.sh+19 −2scripts/kepr-build-helpers.sh+14 −6scripts/test-kepr-build-helpers.sh+13 −8
VERSION+1 −1
@@ -1 +1 @@1-2.9.111+2.9.12

view file

app/build.gradle+9 −7
@@ -32,7 +32,7 @@3232         minSdk 263333         targetSdk 353434         // Keep the Android code in Gradle; the semantic name comes from root VERSION.35-        versionCode 24015735+        versionCode 2401583636         versionName semanticVersion3737 3838         testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"@@ -64,12 +64,14 @@6464         }6565     }6666 67-    // ABI splits were tried and removed: the native libraries are ~2.5 MB of a68-    // 24 MB APK, so per-ABI builds saved 8% in exchange for four artifacts and69-    // a "which one do I need?" question at every install. The weight is 62 MB of70-    // DEX, because `minifyEnabled false` below leaves R8 off entirely — that is71-    // the lever worth pulling, and it needs its own pass with real-device72-    // verification (kotlinx.serialization, BouncyCastle, Hilt, Compose).67+    splits {68+        abi {69+            enable true70+            reset()71+            include "arm64-v8a", "x86_64"72+            universalApk false73+        }74+    }7375 7476     buildTypes {7577         debug {

view file

app/src/main/java/com/peachpasswords/updates/UpdateManifest.kt+10 −5
@@ -61,13 +61,18 @@6161 class DownloadSizeException(message: String) : Exception(message)6262 6363 /**64- * SETTLED — Kepr accepts `android-arm64` as of kepr v1.56.237, matching its65- * `<os>-<arch>` platform vocabulary. This is the key the release manifest is66- * published under, so it is the key the updater must look up. It has to stay67- * identical to `platforms =` in this repo's `kepr.build` and to Kepr's68- * `release_platforms` allowlist; changing any one alone breaks APK publishing.64+ * Kepr platform keys for the two APK splits Peach publishes. `android-x86`65+ * carries the x86_64 APK; the shorter public key matches Kepr's platform66+ * vocabulary while Android's ABI name remains `x86_64` inside the archive.6967  */7068 const val ANDROID_PLATFORM_KEY = "android-arm64"69+const val ANDROID_X86_PLATFORM_KEY = "android-x86"70+71+fun androidPlatformKey(supportedAbis: Array<String>): String? = when {72+    "arm64-v8a" in supportedAbis -> ANDROID_PLATFORM_KEY73+    "x86_64" in supportedAbis -> ANDROID_X86_PLATFORM_KEY74+    else -> null75+}7176 7277 /**7378  * Hard ceiling on a downloaded APK. Kepr publishes ~24 MB today; 50 MB leaves

view file

app/src/main/java/com/peachpasswords/updates/UpdateRepository.kt+3 −1
@@ -113,7 +113,9 @@113113             .build()114114         client.newCall(request).execute().use { response ->115115             if (!response.isSuccessful) throw IOException("HTTP ${response.code}")116-            parseReleaseManifest(response.body?.string().orEmpty())116+            val platform = androidPlatformKey(Build.SUPPORTED_ABIS)117+                ?: throw ManifestFormatException("this Android ABI is not supported")118+            parseReleaseManifest(response.body?.string().orEmpty(), platform)117119         }118120     }119121 

view file

app/src/test/java/com/peachpasswords/updates/UpdateManifestTest.kt+20 −2
@@ -55,6 +55,25 @@5555         assertEquals("https://x/y", parseReleaseManifest(body).url)5656     }5757 58+    @Test59+    fun `platform selection follows the device ABI`() {60+        assertEquals(ANDROID_PLATFORM_KEY, androidPlatformKey(arrayOf("arm64-v8a", "armeabi-v7a")))61+        assertEquals(ANDROID_X86_PLATFORM_KEY, androidPlatformKey(arrayOf("x86_64", "x86")))62+        assertNull(androidPlatformKey(arrayOf("armeabi-v7a")))63+    }64+65+    @Test66+    fun `x86 device selects the x86 artifact`() {67+        val body = """68+            {"version":"2.3.68","platforms":{"android-x86":69+              {"url":"https://x/x86.apk","checksum":"sha256:${"b".repeat(64)}"}}}70+        """.trimIndent()71+        assertEquals(72+            "https://x/x86.apk",73+            parseReleaseManifest(body, androidPlatformKey(arrayOf("x86_64"))!!).url,74+        )75+    }76+5877     @Test(expected = ManifestFormatException::class)5978     fun `no platforms is a hard failure`() {6079         parseReleaseManifest("""{"version":"2.3.68"}""")@@ -62,8 +81,7 @@6281 6382     @Test(expected = ManifestFormatException::class)6483     fun `non-canonical platform key is rejected, no sole-artifact fallback`() {65-        // A Kepr build that only published android-x86 used to be accepted via66-        // the sole-artifact fallback; now it is refused outright.84+        // An ARM64 device must not silently accept the x86 artifact.6785         val body = """6886             {"version":"2.3.68","platforms":{"android-x86":6987               {"url":"https://x/y","checksum":"sha256:${"b".repeat(64)}"}}}

view file

kepr.build+4 −6
@@ -1,11 +1,9 @@11 # Kepr Builds config for peach-android.22 #3-# An APK is a single artifact that runs on every supported ABI, so one platform4-# entry is the honest mapping. `platforms` here must match ANDROID_PLATFORM_KEY5-# in app/src/main/java/com/peachpasswords/updates/UpdateManifest.kt (what the6-# in-app updater looks up) and Kepr's own `release_platforms` allowlist, which7-# accepts android-arm64 as of kepr v1.56.237.3+# Koh runs the command once per platform with KEPR_TARGET set. The script stages4+# the matching signed split APK at the fixed output path. These keys must match5+# UpdateManifest.kt and Kepr's release_platforms allowlist.86 command = ./scripts/kepr-build-apk.sh97 output = build/kepr/peach-android.apk108 trigger = manual11-platforms = android-arm649+platforms = android-arm64, android-x86

view file

scripts/kepr-build-apk.sh+19 −2
@@ -78,7 +78,22 @@7878 7979 "$ROOT_DIR/gradlew" --no-configuration-cache :app:assembleRelease8080 81-APK="$ROOT_DIR/app/build/outputs/apk/release/app-release.apk"81+case "${KEPR_TARGET:-}" in82+    android-arm64)83+        APK_NAME="app-arm64-v8a-release.apk"84+        EXPECTED_ABI="arm64-v8a"85+        ;;86+    android-x86)87+        APK_NAME="app-x86_64-release.apk"88+        EXPECTED_ABI="x86_64"89+        ;;90+    *)91+        echo "refusing to publish: unsupported KEPR_TARGET '${KEPR_TARGET:-unset}'" >&292+        exit 193+        ;;94+esac95+96+APK="$ROOT_DIR/app/build/outputs/apk/release/$APK_NAME"8297 METADATA="$ROOT_DIR/app/build/outputs/apk/release/output-metadata.json"8398 if [ ! -f "$APK" ] || [ ! -r "$APK" ]; then8499     echo "refusing to publish: release APK was not produced" >&2@@ -99,7 +114,9 @@99114     "$METADATA" \100115     "$BADGING_FILE" \101116     "$ROOT_VERSION" \102-    "com.peachpasswords"; then117+    "com.peachpasswords" \118+    "$APK_NAME" \119+    "$EXPECTED_ABI"; then103120     exit 1104121 fi105122 

view file

scripts/kepr-build-helpers.sh+14 −6
@@ -87,19 +87,21 @@8787 # Validate Gradle output-metadata.json, the APK's aapt2 badging, and VERSION as8888 # one release identity. The Python standard library is used only for parsing8989 # generated JSON; duplicate keys and ambiguous element lists are rejected.90-# Arguments: metadata-json badging-file root-version expected-app-id90+# Arguments: metadata-json badging-file root-version expected-app-id expected-output expected-abi9191 validate_release_metadata() {9292     metadata_file=$19393     badging_file=$29494     root_version=$39595     expected_app_id=$496+    expected_output=$597+    expected_abi=$69698 97-    python3 - "$metadata_file" "$badging_file" "$root_version" "$expected_app_id" <<'PY'99+    python3 - "$metadata_file" "$badging_file" "$root_version" "$expected_app_id" "$expected_output" "$expected_abi" <<'PY'98100 import json99101 import re100102 import sys101103 102-metadata_path, badging_path, root_version, expected_app_id = sys.argv[1:]104+metadata_path, badging_path, root_version, expected_app_id, expected_output, expected_abi = sys.argv[1:]103105 104106 def reject(message):105107     raise SystemExit("release metadata rejected: " + message)@@ -131,9 +133,12 @@131133 if metadata.get("applicationId") != expected_app_id:132134     reject("Gradle applicationId does not match production package")133135 elements = metadata.get("elements")134-if not isinstance(elements, list) or len(elements) != 1 or not isinstance(elements[0], dict):135-    reject("Gradle metadata must contain exactly one APK element")136-element = elements[0]136+if not isinstance(elements, list) or not all(isinstance(item, dict) for item in elements):137+    reject("Gradle metadata elements are malformed")138+matches = [item for item in elements if item.get("outputFile") == expected_output]139+if len(matches) != 1:140+    reject("Gradle metadata must contain exactly one matching APK element")141+element = matches[0]137142 if element.get("versionName") != root_version:138143     reject("Gradle versionName does not match VERSION")139144 expected_code = element.get("versionCode")@@ -164,5 +169,8 @@164169 if apk_version != root_version:165170     reject("APK versionName does not match VERSION")166171 strict_semver(apk_version)172+native_lines = [line for line in badging.splitlines() if line.startswith("native-code:")]173+if native_lines != [f"native-code: '{expected_abi}'"]:174+    reject("APK native ABI does not match requested platform")167175 PY168176 }

view file

scripts/test-kepr-build-helpers.sh+13 −8
@@ -52,24 +52,29 @@5252 cat > "$TEMP_ROOT/metadata.json" <<'JSON'5353 {5454   "applicationId": "com.peachpasswords",55-  "elements": [{"versionCode": 240146, "versionName": "2.8.4", "outputFile": "app-release.apk"}]55+  "elements": [56+    {"versionCode": 240146, "versionName": "2.8.4", "outputFile": "app-arm64-v8a-release.apk"},57+    {"versionCode": 240146, "versionName": "2.8.4", "outputFile": "app-x86_64-release.apk"}58+  ]5659 }5760 JSON58-printf "package: name='com.peachpasswords' versionCode='240146' versionName='2.8.4'\n" > "$TEMP_ROOT/badging"59-validate_release_metadata "$TEMP_ROOT/metadata.json" "$TEMP_ROOT/badging" 2.8.4 com.peachpasswords61+printf "package: name='com.peachpasswords' versionCode='240146' versionName='2.8.4'\nnative-code: 'arm64-v8a'\n" > "$TEMP_ROOT/badging"62+validate_release_metadata "$TEMP_ROOT/metadata.json" "$TEMP_ROOT/badging" 2.8.4 com.peachpasswords app-arm64-v8a-release.apk arm64-v8a6063 6164 printf '{"applicationId":"com.peachpasswords",' > "$TEMP_ROOT/metadata.malformed"62-expect_failure validate_release_metadata "$TEMP_ROOT/metadata.malformed" "$TEMP_ROOT/badging" 2.8.4 com.peachpasswords65+expect_failure validate_release_metadata "$TEMP_ROOT/metadata.malformed" "$TEMP_ROOT/badging" 2.8.4 com.peachpasswords app-arm64-v8a-release.apk arm64-v8a6366 cat > "$TEMP_ROOT/metadata.duplicate.json" <<'JSON'6467 {"applicationId":"com.peachpasswords","applicationId":"com.peachpasswords","elements":[{"versionCode":240146,"versionName":"2.8.4"}]}6568 JSON66-expect_failure validate_release_metadata "$TEMP_ROOT/metadata.duplicate.json" "$TEMP_ROOT/badging" 2.8.4 com.peachpasswords69+expect_failure validate_release_metadata "$TEMP_ROOT/metadata.duplicate.json" "$TEMP_ROOT/badging" 2.8.4 com.peachpasswords app-arm64-v8a-release.apk arm64-v8a6770 cat > "$TEMP_ROOT/metadata.ambiguous.json" <<'JSON'6871 {"applicationId":"com.peachpasswords","elements":[{"versionCode":240146,"versionName":"2.8.4"},{"versionCode":240147,"versionName":"2.8.4"}]}6972 JSON70-expect_failure validate_release_metadata "$TEMP_ROOT/metadata.ambiguous.json" "$TEMP_ROOT/badging" 2.8.4 com.peachpasswords71-expect_failure validate_release_metadata "$TEMP_ROOT/metadata.json" "$TEMP_ROOT/badging" 2.8.5 com.peachpasswords73+expect_failure validate_release_metadata "$TEMP_ROOT/metadata.ambiguous.json" "$TEMP_ROOT/badging" 2.8.4 com.peachpasswords app-release.apk arm64-v8a74+expect_failure validate_release_metadata "$TEMP_ROOT/metadata.json" "$TEMP_ROOT/badging" 2.8.5 com.peachpasswords app-arm64-v8a-release.apk arm64-v8a7275 printf "package: name='com.peachpasswords' versionCode='240146' versionName='2.8.5'\n" > "$TEMP_ROOT/badging.mismatch"73-expect_failure validate_release_metadata "$TEMP_ROOT/metadata.json" "$TEMP_ROOT/badging.mismatch" 2.8.4 com.peachpasswords76+printf "native-code: 'arm64-v8a'\n" >> "$TEMP_ROOT/badging.mismatch"77+expect_failure validate_release_metadata "$TEMP_ROOT/metadata.json" "$TEMP_ROOT/badging.mismatch" 2.8.4 com.peachpasswords app-arm64-v8a-release.apk arm64-v8a78+expect_failure validate_release_metadata "$TEMP_ROOT/metadata.json" "$TEMP_ROOT/badging" 2.8.4 com.peachpasswords app-arm64-v8a-release.apk x86_647479 7580 printf 'PASS: stable tool selection, malformed/ambiguous metadata, and semantic-version mismatch gates\n'

view file