Files
ai-app/app/shellApp/build.gradle.kts
T
irisandClaude Sonnet ceabd00805 E5: package app/shellApp without Gradle (cargo xtask apk)
New xtask/ crate (no deps) runs cargo ndk -> javac -> d8 -> aapt2 ->
zipalign -> apksigner directly, signed with the same key build-apk.sh
uses. Both pass conditions proved on the ai-app-2 emulator: the xtask
APK installs over the Gradle-built shellApp, and the notification
service starts and posts a real notification while backgrounded.

Adds one printRuntimeClasspathJars task to shellApp/build.gradle.kts
(and a matching signingConfig) -- the one disclosed Gradle call the
xtask still makes, to resolve the AndroidX/​:link dependency graph.
That call's Kotlin compilation of :link as a side effect also answers
E3's open kotlinc question, so no Java port of ServerStore was needed.
Wires a second Apk component into .dev-updater.ron beside the existing
one. Full writeup in RUST.md's E5 box.

Co-Authored-By: Claude Sonnet <noreply@anthropic.com>
2026-09-05 07:11:44 -04:00

164 lines
6.6 KiB
Kotlin

plugins { alias(libs.plugins.androidApplication) }
// E3 (RUST.md): the Kotlin/Java shell being replaced by a thin JNI bridge
// into Rust (`../../android-shell`). Deliberately its own module rather
// than a rewrite of `:androidApp` in place -- that module is ~13,000 lines
// of working Compose UI this experiment does not touch, and the two can be
// installed side by side on the same development device (see
// `settings.SCHEME`'s doc in `android-shell` for why the deep-link scheme
// and Keystore alias are not the production app's). No Compose plugin, no
// Kotlin source of its own: `MainActivity`/`NotificationService` are plain
// Java, and the CA constant below is generated as Java too.
//
// The CA this build pins is baked in the same way `androidApp`'s does --
// see that module's `build.gradle.kts` comment for the reasoning (the
// trust boundary follows the machine that builds, never a pasted copy).
// `PinnedCa.java`'s package must match `android-shell`'s
// `settings::load_pinned_ca` lookup (`com/example/aiapp/shell/PinnedCa`).
val pinnedCaPath: String =
System.getenv("AI_APP_CA")
?: "${System.getenv("XDG_CONFIG_HOME") ?: "${System.getProperty("user.home")}/.config"}" +
"/ai-app/certs/ca.pem"
abstract class GeneratePinnedCa : DefaultTask() {
@get:Input abstract val caPath: Property<String>
@get:InputFile
@get:Optional
@get:PathSensitive(PathSensitivity.NONE)
abstract val caCertificate: RegularFileProperty
@get:OutputDirectory abstract val outputDir: DirectoryProperty
@TaskAction
fun generate() {
val path = caPath.get()
val ca = File(path)
if (!ca.isFile) {
throw GradleException(
"No CA certificate at $path.\n" +
"Start ai-server (or app/ui-sandbox.sh) once on this machine first -- it " +
"generates the CA this build pins.\n" +
"Set AI_APP_CA=/path/to/ca.pem to build against a different one."
)
}
val pem = ca.readText().trim()
if (!pem.startsWith("-----BEGIN CERTIFICATE-----")) {
throw GradleException("$path is not a PEM certificate.")
}
val dir = outputDir.get().dir("com/example/aiapp/shell").asFile
dir.mkdirs()
// Same reasoning as androidApp's generatePinnedCert: the text block
// must start immediately after the opening `"""`, or
// CertificateFactory stops recognising the "-----BEGIN" preamble.
File(dir, "PinnedCa.java")
.writeText(
"""
|// Generated from $path by the generatePinnedCa task. Do not edit.
|package com.example.aiapp.shell;
|
|public final class PinnedCa {
| private PinnedCa() {}
| public static final String PINNED_CA_PEM = ""${'"'}
|$pem""${'"'};
|}
|"""
.trimMargin()
)
}
}
val generatePinnedCa =
tasks.register<GeneratePinnedCa>("generatePinnedCa") {
val ca = file(pinnedCaPath)
caPath.set(pinnedCaPath)
if (ca.isFile) {
caCertificate.set(ca)
}
}
android {
namespace = "com.example.aiapp.shell"
compileSdk = 37
defaultConfig {
applicationId = "com.example.aiapp.shell"
minSdk = 24
targetSdk = 37
versionCode = 1
versionName = "1.0"
}
// Same reasoning and same key as androidApp's (see that module's comment): E5 (RUST.md)
// signs its own, Gradle-free build with this same keystore, and the two can only
// `adb install -r` over each other if they carry the same certificate.
val keystore = System.getenv("AI_APP_KEYSTORE")
signingConfigs {
if (keystore != null) {
create("release") {
storeFile = file(keystore)
storePassword = System.getenv("AI_APP_KEYSTORE_PASSWORD")
keyAlias = "ai-app"
keyPassword = storePassword
}
}
}
buildTypes {
getByName("release") {
isMinifyEnabled = false
if (keystore != null) signingConfig = signingConfigs.getByName("release")
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_21
targetCompatibility = JavaVersion.VERSION_21
}
}
// E5 (RUST.md): the xtask dexes and packages this module's Java sources itself, but it does
// not resolve Maven dependencies -- reimplementing a dependency resolver was out of scope for a
// packaging step, so this one task is the single place Gradle still runs in that pipeline. It
// asks the dependency graph for the *post-transform* jars (AARs already unpacked to a classes
// jar, the same artifact type AGP's own dexing task consumes) rather than the raw configuration,
// which would hand back .aar files d8 cannot read directly.
val artifactType = Attribute.of("artifactType", String::class.java)
tasks.register("printRuntimeClasspathJars") {
description = "Writes the resolved release runtime classpath jars, one per line, for xtask."
val outputFile = layout.buildDirectory.file("xtask/runtime-classpath.txt")
outputs.file(outputFile)
val jars =
configurations
.getByName("releaseRuntimeClasspath")
.incoming
.artifactView { attributes.attribute(artifactType, "android-classes-jar") }
.files
// Captured as a plain FileCollection (not the ArtifactView itself, which the
// configuration cache cannot serialize) so this task is still cacheable.
inputs.files(jars)
doLast {
val file = outputFile.get().asFile
file.parentFile.mkdirs()
file.writeText(jars.joinToString("\n") { it.absolutePath })
}
}
androidComponents {
onVariants { variant ->
variant.sources.java?.addGeneratedSourceDirectory(generatePinnedCa, GeneratePinnedCa::outputDir)
}
}
dependencies {
// The Keystore-sealed enrollment (ServerStore/ServerSettings) --
// android-shell's settings.rs calls into this Kotlin class directly
// over JNI rather than re-sealing the token in Rust; see that file's
// module doc.
implementation(project(":link"))
// NotificationCompat/NotificationManagerCompat/NotificationChannelCompat/
// ServiceCompat -- android-shell's notify.rs calls these classes over
// JNI so the pre-26 fallback behaviour (no channels) lives once, in
// the library that already has it, rather than being re-derived as a
// set of Build.VERSION.SDK_INT branches in Rust.
implementation(libs.androidx.core.ktx)
}