From d0115e53467cdb1422ac43bc96a6e909e220b415 Mon Sep 17 00:00:00 2001 From: "William K. Santiago" Date: Sat, 22 Aug 2026 11:44:58 -0400 Subject: [PATCH 1/3] Build universal debug APK for connected tests to fix per-ABI split install (#482) --- app/build.gradle.kts | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 8633786e..67736c20 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -4,6 +4,12 @@ plugins { id("com.google.devtools.ksp") } +// True when the requested tasks include instrumented (connected) tests; used to +// disable per-ABI splits so a universal debug APK is built for the test device. +val runningInstrumentedTests = gradle.startParameter.taskNames.any { + it.contains("AndroidTest", ignoreCase = true) +} + android { namespace = "io.privkey.keep" compileSdk = 37 @@ -96,9 +102,14 @@ android { // Per-ABI APK splits for F-Droid: ship one APK per architecture instead of a // single universal APK. Each split gets a distinct versionCode assigned in // the androidComponents block below. + // + // Disabled while running instrumented (connected) tests: splits produce per-ABI + // app APKs but the androidTest APK is universal, and the install path cannot + // reconcile the two, so installPackages fails before any test runs and it is + // misreported as failing tests (GH #482). A universal debug APK installs fine. splits { abi { - isEnable = true + isEnable = !runningInstrumentedTests reset() include("arm64-v8a", "x86_64") isUniversalApk = false From 87a95cc24b3f9cbd397621033559c2d048bc729a Mon Sep 17 00:00:00 2001 From: "William K. Santiago" Date: Sat, 22 Aug 2026 13:38:23 -0400 Subject: [PATCH 2/3] Recognize connected-test lifecycle tasks and reject release+instrumented mixes --- app/build.gradle.kts | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 67736c20..8b28e4ce 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -6,8 +6,28 @@ plugins { // True when the requested tasks include instrumented (connected) tests; used to // disable per-ABI splits so a universal debug APK is built for the test device. +// Matches both the explicit test tasks (connectedDebugAndroidTest, +// assembleDebugAndroidTest, ...) and the lifecycle wrappers (connectedCheck), +// so any connected-test entry point gets the universal APK, not just the one CI runs. val runningInstrumentedTests = gradle.startParameter.taskNames.any { - it.contains("AndroidTest", ignoreCase = true) + it.contains("AndroidTest", ignoreCase = true) || + it.contains("connected", ignoreCase = true) +} + +// splits.abi is a global (non-per-variant) config, so disabling it for an +// instrumented-test run also strips the per-ABI release splits and their version +// codes, silently producing a universal release APK at the base versionCode. +// Refuse the mixed invocation rather than emit a broken F-Droid artifact (GH #482). +val assemblingRelease = gradle.startParameter.taskNames.any { + it.contains("Release", ignoreCase = true) && + (it.contains("assemble", ignoreCase = true) || it.contains("bundle", ignoreCase = true)) +} +if (runningInstrumentedTests && assemblingRelease) { + throw GradleException( + "Do not request instrumented tests and a release build in the same Gradle " + + "invocation: disabling ABI splits for the universal test APK would also " + + "strip the per-ABI release splits and their version codes. Run them separately." + ) } android { From 0b2dde75e519fa7ac81d34c5b262475898f7566e Mon Sep 17 00:00:00 2001 From: "William K. Santiago" Date: Sat, 22 Aug 2026 14:11:12 -0400 Subject: [PATCH 3/3] Match camelCase-abbreviated connected-test tasks when disabling ABI splits --- app/build.gradle.kts | 38 ++++++++++++++++++++++++++++++++------ 1 file changed, 32 insertions(+), 6 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 8b28e4ce..5674ac76 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -4,14 +4,40 @@ plugins { id("com.google.devtools.ksp") } +// Canonical connected/instrumented-test task names. Gradle lets any of these be +// abbreviated in camelCase on the command line (`cAT` -> connectedAndroidTest, +// `cC` -> connectedCheck, `cDAT` -> connectedDebugAndroidTest), and an abbreviation +// contains neither "connected" nor "AndroidTest", so a substring match alone misses +// a local `./gradlew cAT` and it silently keeps ABI splits enabled, still hitting #482. +val connectedTestTasks = listOf( + "connectedCheck", + "connectedAndroidTest", + "connectedDebugAndroidTest", + "assembleAndroidTest", + "assembleDebugAndroidTest", +) + +// True when the requested `name` is a Gradle camelCase abbreviation of `full`: +// same number of camel-hump segments, each a case-insensitive prefix of the +// corresponding segment of `full` (so `cAT` matches connectedAndroidTest but plain +// `cat` — a single lowercase hump — matches nothing multi-hump). +fun abbreviatesCamelCase(name: String, full: String): Boolean { + val nameHumps = name.split(Regex("(?=\\p{Upper})")) + val fullHumps = full.split(Regex("(?=\\p{Upper})")) + return nameHumps.size == fullHumps.size && + nameHumps.indices.all { fullHumps[it].startsWith(nameHumps[it], ignoreCase = true) } +} + // True when the requested tasks include instrumented (connected) tests; used to // disable per-ABI splits so a universal debug APK is built for the test device. -// Matches both the explicit test tasks (connectedDebugAndroidTest, -// assembleDebugAndroidTest, ...) and the lifecycle wrappers (connectedCheck), -// so any connected-test entry point gets the universal APK, not just the one CI runs. -val runningInstrumentedTests = gradle.startParameter.taskNames.any { - it.contains("AndroidTest", ignoreCase = true) || - it.contains("connected", ignoreCase = true) +// Matches the explicit test tasks (connectedDebugAndroidTest, ...), the lifecycle +// wrappers (connectedCheck), and their camelCase abbreviations, so any connected-test +// entry point gets the universal APK, not just the one CI runs. +val runningInstrumentedTests = gradle.startParameter.taskNames.any { requested -> + val name = requested.substringAfterLast(':') + name.contains("AndroidTest", ignoreCase = true) || + name.contains("connected", ignoreCase = true) || + connectedTestTasks.any { abbreviatesCamelCase(name, it) } } // splits.abi is a global (non-per-variant) config, so disabling it for an