build(android): make release buildable and instrumented testing possible
Four gaps that made "ship it" and "test it on hardware" impossible. RELEASE SIGNING. assembleRelease was silently producing app-release-unsigned.apk because there was no signingConfigs block at all — the failure mode where you only notice at install time. Credentials now resolve from keystore.properties, then local.properties, then WEBTERM_RELEASE_* env; nothing is created or committed, and keystore.properties.example is the template. The degradation is ENFORCED, not documented. The signingConfig is only created when credentials actually resolve — an empty config is precisely what makes AGP emit an unsigned artifact quietly — and a guard on packageRelease throws with an actionable message, distinguishing "not configured" from "credentials found but the keystore file is missing: <path>". It hooks the PACKAGE task on purpose: R8 has already run and reported by then, so a developer without a keystore still gets the full shrink result rather than an early abort. Verified: minifyReleaseWithR8 completes, packageRelease fails loudly, and no unsigned APK is written. VERSIONING. Was still the scaffold 1 / "0.1.0". Now 1 / "0.1.0-alpha01", where the suffix is a factual claim about device verification rather than decoration: alpha means device QA is essentially unstarted, beta means the A34/A35 checklist blocks pass on hardware, and plain 0.1.0 means the whole checklist is ticked. versionCode stays 1 because no artifact has ever left the build machine. MINIFICATION. release had isMinifyEnabled = false and an empty rules file — so nothing had ever exercised R8 on this app, and the first release build would have been the experiment. Now minify + resource shrinking, with a deliberately short rules file: the actual artifacts were checked, and kotlinx.serialization, Tink, Hilt, FCM, OkHttp, Compose and CameraX all ship their own consumer rules, so none are re-declared. The termux packages are kept whole because the JitPack artifacts ship no rules and :terminal-view binds to non-API internals — it writes the public TerminalView.mEmulator field and calls TerminalRenderer.getFontWidth()/getFontLineSpacing(). Getting that wrong yields a build that crashes only in production. lintVitalRelease also passes now, the two pre-existing lint ERRORS having been fixed earlier this session. ANDROID TEST ENABLEMENT. :app had no androidTest source set and no instrumentation runner, which is the mechanical reason A34/A35 have no code at all. The runner (Hilt-aware) and dependencies are wired, and a :macrobenchmark module exists. The tests themselves are a separate change. Verified: ./gradlew test :app:assembleDebug :app:assembleDebugAndroidTest :app:assembleBenchmark :macrobenchmark:assembleBenchmark :app:lintVitalRelease koverVerify -> BUILD SUCCESSFUL; 901 JVM tests, 0 failures on a forced re-execution with test-results deleted and --no-build-cache.
This commit is contained in:
89
android/macrobenchmark/build.gradle.kts
Normal file
89
android/macrobenchmark/build.gradle.kts
Normal file
@@ -0,0 +1,89 @@
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// :macrobenchmark (A35) — the macrobenchmark harness module.
|
||||
//
|
||||
// `com.android.test`, NOT `com.android.library`: macrobenchmark must drive the app
|
||||
// OUT of process (its own APK, its own process, UiAutomator across the boundary) so
|
||||
// that measured startup/frame timings are the real ones and not perturbed by the
|
||||
// test framework sharing the app's process.
|
||||
//
|
||||
// It instruments :app's `benchmark` variant — non-debuggable, R8'd and
|
||||
// resource-shrunk like release, but signed with the debug key so no release keystore
|
||||
// is needed (see app/build.gradle.kts → buildTypes.benchmark).
|
||||
//
|
||||
// This module currently has NO sources: the benchmark itself (pair → attach → type →
|
||||
// approve, plus a cold-start timing) is the next task's deliverable and belongs in
|
||||
// src/main/kotlin/. The module exists so that work is a matter of adding a file.
|
||||
//
|
||||
// Run (device/emulator required — `./gradlew :app:assembleDebug` style CI cannot):
|
||||
// ./gradlew :macrobenchmark:connectedBenchmarkAndroidTest
|
||||
//
|
||||
// AGP 9 has built-in Kotlin → apply ONLY the android plugin (no kotlin.android).
|
||||
// The plugin id is applied BARE (no catalog alias / no version): the root build script
|
||||
// already puts AGP on the classpath via the android.library + android.application
|
||||
// aliases, and Gradle rejects a third versioned request for the same plugin artifact
|
||||
// ("already on the classpath with an unknown version"). The version is still pinned in
|
||||
// one place — `agp` in gradle/libs.versions.toml. Same pattern as :terminal-view.
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
plugins {
|
||||
id("com.android.test")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "wang.yaojia.webterm.macrobenchmark"
|
||||
compileSdk = 36
|
||||
|
||||
defaultConfig {
|
||||
// Macrobenchmark itself needs API 23+; matching :app's 29 keeps the variant
|
||||
// matrix honest (a benchmark on an SDK :app cannot run on is meaningless).
|
||||
minSdk = 29
|
||||
targetSdk = 35
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
// The only variant this module is built in. `isDebuggable = true` applies to the
|
||||
// TEST apk (never to :app) — required so the harness can be instrumented at all.
|
||||
create("benchmark") {
|
||||
isDebuggable = true
|
||||
signingConfig = getByName("debug").signingConfig
|
||||
// :app publishes debug/release/benchmark; libraries publish debug/release.
|
||||
matchingFallbacks += listOf("release")
|
||||
}
|
||||
}
|
||||
|
||||
targetProjectPath = ":app"
|
||||
|
||||
// The harness runs in its OWN process and reaches the app only through UiAutomator /
|
||||
// shell, never by linking against its classes. Without this, AGP's
|
||||
// `checkTestedAppObfuscationBenchmark` refuses to build a non-minified test module
|
||||
// against a minified app — a check that is correct for Espresso-style tests (which do
|
||||
// link app symbols) and a false positive for macrobenchmark.
|
||||
experimentalProperties["android.experimental.self-instrumenting"] = true
|
||||
}
|
||||
|
||||
kotlin {
|
||||
jvmToolchain(17)
|
||||
}
|
||||
|
||||
// Only the `benchmark` variant is meaningful; disable the stock debug/release ones so
|
||||
// `./gradlew build` does not try to assemble a harness against a variant of :app that
|
||||
// does not exist.
|
||||
androidComponents {
|
||||
beforeVariants { variant ->
|
||||
variant.enable = variant.buildType == "benchmark"
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
// A `com.android.test` module's `main` source set IS its test code — deps go in
|
||||
// implementation, not androidTestImplementation.
|
||||
implementation(libs.androidx.test.ext.junit)
|
||||
implementation(libs.androidx.test.uiautomator)
|
||||
implementation(libs.androidx.benchmark.macro.junit4)
|
||||
}
|
||||
Reference in New Issue
Block a user