Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
4a4bd62
ADFA-4931: stop shipping a second Kotlin compiler
fryanpan Aug 13, 2026
f02c8a5
ADFA-4931: point the TestKit fixtures at the toolchain they now test
fryanpan Aug 13, 2026
91ff0c5
ADFA-4931: cover the compiler link, and correct what the daemon deps say
fryanpan Aug 13, 2026
3e273ba
ADFA-4931: stop shipping four more jars the distribution already has
fryanpan Aug 13, 2026
ca008b1
ADFA-4931: follow the distribution's versions for the daemon's last t…
fryanpan Aug 13, 2026
6492228
ADFA-4931: collapse jars the two on-device stores both extracted
fryanpan Aug 13, 2026
747bf98
ADFA-4931: pin that staging runs the link, not just that the link works
fryanpan Sep 18, 2026
10426d4
ADFA-4931: cover the dedup and link paths' remaining branches
fryanpan Sep 18, 2026
456559f
ADFA-4931: hash the jars the daemon loads, not the ones it was built …
fryanpan Sep 22, 2026
4ad887c
ADFA-4931: link the distribution's Kotlin jars instead of shipping a …
fryanpan Sep 24, 2026
d8de72f
ADFA-4931: document that the daemon loads the distribution's compiler
fryanpan Sep 24, 2026
6927b14
ADFA-4931: stop a re-stage from truncating the distribution's own jars
fryanpan Sep 24, 2026
2249dfb
ADFA-4931: run the distribution jar check after the asset downloads
fryanpan Oct 2, 2026
2cb9d0f
ADFA-4931: take the linked jars' versions from the catalog
fryanpan Oct 2, 2026
f19f1e0
ADFA-4931: re-stage when a linked jar no longer resolves
fryanpan Oct 2, 2026
e61a9d1
ADFA-4931: link kotlin-reflect from the distribution too
fryanpan Oct 2, 2026
db7cf61
ADFA-4931: take the daemon's Kotlin at KOTLIN_VERSION and drop the ca…
fryanpan Oct 2, 2026
69d2dff
ADFA-4931: check the distribution release builds package, not only th…
fryanpan Oct 2, 2026
d68ff70
ADFA-4931: correct the stager's class doc, which still said 62 MB
fryanpan Oct 2, 2026
32aaf27
ADFA-4931: cover the re-stage when the linked-jar list is unreadable
fryanpan Oct 2, 2026
98d50bf
ADFA-4931: gate release variants on the distribution check, not task …
fryanpan Oct 2, 2026
9621289
ADFA-4931: gate every variant that bundles the distribution, not just…
fryanpan Oct 2, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ These structural facts shape every module. Day-to-day build *commands* live in `
- **SDK levels** (`build-logic/.../build/config/BuildConfig.kt`): `COMPILE_SDK=36`, `MIN_SDK=28`, `TARGET_SDK=28`. **`TARGET_SDK` is deliberately pinned at 28:** higher targets enforce W^X (write-xor-execute), which blocks executing code from app-writable files. That is fatal for an on-device IDE that compiles and runs code (Gradle, `javac`, Termux binaries), so it is a hard requirement, not tech debt. `MIN_SDK_FOR_APPS_BUILT_WITH_COGO=16` is the floor for the apps a *user* builds with CoGo — distinct from CoGo's own `MIN_SDK`.
- **Native asset bundling.** The on-device LLM (`llama-impl`) ships as a per-flavor native AAR, wired through the root `build.gradle.kts` (`bundleLlamaV8Assets` / `assembleV8Assets`, …); prebuilt per-flavor assets live under `assets/release/v7/` and `assets/release/v8/`.
- **Native lib compression** (ADFA-2306, ADFA-4729). The app manifest hard-codes `android:extractNativeLibs="true"` (required: the installer must materialize libs in `nativeLibraryDir`, e.g. `libshizuku.so` is an executable the adb shell runs from there). That attribute overrides the `jniLibs.useLegacyPackaging` DSL, so AGP packages `lib/<abi>/*.so` deflate-compressed in **every** APK — ~5.9 MB smaller (`libtree-sitter-kotlin.so` alone is 4.18 MB → 339 kB). The trap is the `recompressApk` post-step (release always, debug in CI only): its no-compress lists in `app/build.gradle.kts` must NOT contain `"so"`, or it silently re-stores the libs and undoes the saving — which is what ADFA-2306 fixed for release and ADFA-4729 for CI debug. Locally built debug APKs (including the e2e farm's) never run that step and were always fine.
- **Quick Build shares the Gradle distribution's toolchain** (ADFA-4931). `:app`'s `quickBuildDaemonZip` leaves the Kotlin compiler and eight other jars out of the daemon zip, because the on-device Gradle distribution already carries them; `QuickBuildArtifactStager` links them in at provision time. That makes the daemon's Kotlin version the distribution's, so `:quickbuild:daemon` takes its Kotlin artifacts at `org.adfa.constants.KOTLIN_VERSION`, and `:app:quickBuildDistJarCheck` fails the build when the distribution it packages lacks a linked jar. See [ADR 0016](docs/adr/0016-quick-build-compiles-outside-gradle.md).
- **`app` package layout is by concern, not feature:** `activities`, `fragments`, `services`, `di`, `agent`, `viewmodel(s)`, `repositories`, `roomData`, `localWebServer`, `preferences`, `ui` (Compose screens live under `ui/compose`), `templates/manager` (the Manager screen's `.cgt`-parsing data layer, with direct filesystem access to `Environment.TEMPLATES_DIR` — distinct from the plugin-facing `IdeTemplateService` in `plugin-api`/`plugin-manager`), `utils`, ….

## Technology Stack
Expand Down
163 changes: 162 additions & 1 deletion app/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
@file:Suppress("UnstableApiUsage")

import com.aayushatharva.brotli4j.Brotli4jLoader
import com.aayushatharva.brotli4j.decoder.BrotliInputStream
import com.itsaky.androidide.build.config.BuildConfig
import com.itsaky.androidide.desugaring.utils.JavaIOReplacements.applyJavaIOReplacements
import com.itsaky.androidide.plugins.AndroidIDEAssetsPlugin
import com.itsaky.androidide.plugins.conf.hasBundledAssets
import com.itsaky.androidide.plugins.tasks.AddFileToAssetsTask
import org.adfa.constants.GRADLE_API_NAME_JAR_BR
import org.adfa.constants.GRADLE_API_NAME_JAR_ZIP
import org.adfa.constants.GRADLE_DISTRIBUTION_ARCHIVE_NAME
import org.adfa.constants.GRADLE_DISTRIBUTION_NAME
import org.adfa.constants.KOTLIN_VERSION
import org.gradle.nativeplatform.platform.internal.DefaultNativePlatform
import org.json.JSONObject
import java.io.BufferedOutputStream
Expand All @@ -27,6 +32,7 @@ import java.util.zip.CRC32
import java.util.zip.Deflater
import java.util.zip.ZipEntry
import java.util.zip.ZipFile
import java.util.zip.ZipInputStream
import java.util.zip.ZipOutputStream

plugins {
Expand Down Expand Up @@ -506,16 +512,166 @@ dependencies {
evaluationDependsOn(":quickbuild:runtime")
evaluationDependsOn(":quickbuild:daemon")

// ADFA-4931: jars the daemon loads from the on-device Gradle distribution instead of shipping its
// own copy, since `gradle-dists/` and `quickbuild/` both sit under <ANDROIDIDE_HOME>.
// quickBuildDaemonZip leaves these out and QuickBuildArtifactStager links the distribution's copy
// in under exactly these names, which is what the daemon jar's manifest Class-Path expects.
val quickBuildDistLinkedJars =
listOf(
"kotlin-compiler-embeddable-$KOTLIN_VERSION.jar",
"kotlin-stdlib-$KOTLIN_VERSION.jar",
"kotlin-build-tools-impl-$KOTLIN_VERSION.jar",
"kotlin-daemon-embeddable-$KOTLIN_VERSION.jar",
"kotlin-script-runtime-$KOTLIN_VERSION.jar",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NITPICK: kotlin-reflect could join this list, saving about 3 MB more.

The distribution ships kotlin-reflect-2.3.21.jar in lib/, but the daemon resolves kotlin-reflect:1.6.10, which the POM of kotlin-compiler-embeddable:2.3.21 pins (checked with :quickbuild:daemon:dependencies --configuration runtimeClasspath at this head). So the zip still carries its own copy, of an older version.

Fix: pin kotlin-reflect to KOTLIN_VERSION with a constraint, as for coroutines, then link it. The daemon README says excluding it breaks compiles, so the :quickbuild:daemon suite would need to pass on 2.3.21 first.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in df6e21d. A constraint pins kotlin-reflect to KOTLIN_VERSION (quickbuild/daemon/build.gradle.kts:124), so it resolves 1.6.10 -> 2.3.21, and it joins the linked list (app/build.gradle.kts:526). The full :quickbuild:daemon suite passes on 2.3.21 (234 tests, 0 failures), and both distributions carry lib/kotlin-reflect-2.3.21.jar. The daemon zip drops from 4.4 MB to 1.7 MB. On the A56 it is linked like the other 8, and a Compose edit compiles and reloads through it.

"kotlin-reflect-$KOTLIN_VERSION.jar",
// These three carry their own version rather than KOTLIN_VERSION, pinned in
// libs.versions.toml to what the distribution ships so they can be linked from there.
"kotlinx-coroutines-core-jvm-${libs.kotlinx.coroutines.core.jvm.quickBuildDaemon.get().version}.jar",
"gson-${libs.gson.quickBuildDaemon.get().version}.jar",
"asm-${libs.ow2.asm.get().version}.jar",
)

// A broken link surfaces on device as a NoClassDefFoundError partway into the user's first
// compile, far from the change that caused it, so check both ends at build time: the daemon must
// resolve each name, and the distribution this build packages must carry each one.
val quickBuildReleaseDistribution =
rootProject.file("assets/release/common/data/common/$GRADLE_DISTRIBUTION_ARCHIVE_NAME.br")

val quickBuildDistJarCheck =
tasks.register("quickBuildDistJarCheck") {
// Without the ordering, a build that downloads a new distribution can check the old one.
mustRunAfter("assetsDownloadDebug", "assetsDownloadRelease")
val expected = quickBuildDistLinkedJars
val distName = GRADLE_DISTRIBUTION_NAME
val archiveName = GRADLE_DISTRIBUTION_ARCHIVE_NAME
val debugArchive = rootProject.file("assets/$archiveName")
val releaseArchive = quickBuildReleaseDistribution
val classpath =
files(
rootProject
.project(":quickbuild:daemon")
.configurations
.named("runtimeClasspath"),
)
inputs.files(classpath)
inputs
.files(debugArchive)
.withPropertyName("debugDistribution")
.optional(true)
inputs
.files(releaseArchive)
.withPropertyName("releaseDistribution")
.optional(true)
doLast {
val resolved = classpath.files.associateBy { it.name }
expected.sorted().forEach { name ->
if (name !in resolved) {
throw GradleException(
"$name is linked from the Gradle distribution but is not on the daemon's " +
"runtime classpath, so quickBuildDaemonZip's exclusion of that name " +
"matches nothing and the zip carries a copy the stager then links over. " +
"Point quickBuildDistLinkedJars at the name the daemon now resolves. " +
"Resolved: ${resolved.keys.sorted()}",
)
}
}

/** Fails unless every linked jar sits in the distribution's lib/ inside [archive]. */
fun requireLinkedJars(
entries: Set<String>,
archive: File,
) {
expected.forEach { name ->
val entryName = "$distName/lib/$name"
if (entryName !in entries) {
throw GradleException(
"$entryName is missing from ${archive.name}. quickBuildDaemonZip " +
"excludes $name on the promise that the distribution supplies it, so " +
"the daemon would start with no copy at all. Either drop it from " +
"quickBuildDistLinkedJars and let the zip carry it, or correct the name.",
)
}
}
logger.lifecycle(
"quickBuildDistJarCheck: found ${expected.size} of ${expected.size} linked jars " +
"in $distName/lib of ${archive.name}",
)
}

if (debugArchive.isFile) {
val entries =
ZipFile(debugArchive).use { zip ->
zip
.entries()
.asSequence()
.map { it.name }
.toSet()
}
requireLinkedJars(entries, debugArchive)
}
if (releaseArchive.isFile) {
Brotli4jLoader.ensureAvailability()
val entries = mutableSetOf<String>()
ZipInputStream(BrotliInputStream(releaseArchive.inputStream().buffered())).use { zip ->
generateSequence { zip.nextEntry }.forEach { entries += it.name }
}
requireLinkedJars(entries, releaseArchive)
}
if (!debugArchive.isFile && !releaseArchive.isFile) {
logger.lifecycle(
"quickBuildDistJarCheck: NOT PACKAGED - 0 of ${expected.size} linked jars looked " +
"up. No Gradle distribution archive is on disk; a debug APK gets it from the " +
"assets zip, which ./gradlew :app:assetsDownloadDebug fetches to check.",
)
}
}
}

// A variant that bundles the distribution must not package the daemon zip until the check
// above has read it. Wired per variant below rather than inferred from task names.
val quickBuildBundledDistGate =
tasks.register("quickBuildBundledDistGate") {
dependsOn(quickBuildDistJarCheck)
val releaseArchive = quickBuildReleaseDistribution
doLast {
if (!releaseArchive.isFile) {
throw GradleException(
"${releaseArchive.absolutePath} is absent, so quickBuildDistJarCheck cannot read " +
"the Gradle distribution this variant packages. Run " +
":app:assetsDownloadRelease first.",
)
}
}
}

// The names the stager links, travelling with the zip so the device side reads the build's
// list rather than repeating it.
val quickBuildDistLinkedJarList =
tasks.register("quickBuildDistLinkedJarList") {
val names = quickBuildDistLinkedJars.sorted()
val listFile = layout.buildDirectory.file("intermediates/quickbuild/dist-linked-jars.txt")
inputs.property("names", names)
outputs.file(listFile)
doLast {
listFile.get().asFile.writeText(names.joinToString("\n", postfix = "\n"))
}
}

val quickBuildDaemonZip =
tasks.register<Zip>("quickBuildDaemonZip") {
archiveFileName.set("quickbuild-daemon.zip")
destinationDirectory.set(layout.buildDirectory.dir("intermediates/quickbuild"))
dependsOn(quickBuildDistJarCheck)
val daemonProject = rootProject.project(":quickbuild:daemon")
dependsOn(daemonProject.tasks.named("daemonJar"))
from(daemonProject.tasks.named("daemonJar"))
// The daemon jar's manifest Class-Path names these by file name; they must sit
// next to the jar after extraction.
from(daemonProject.configurations.named("runtimeClasspath"))
// ADFA-4931: except the ones the distribution already carries; the stager links those
// in at provision time. Scoped to this spec so it cannot filter the other from()s.
val linked = quickBuildDistLinkedJars.toSet()
from(daemonProject.configurations.named("runtimeClasspath")) { exclude { it.name in linked } }
from(quickBuildDistLinkedJarList)
// Compose compiler plugin, version-matched to the daemon's compiler; the stable
// name is the contract EnvironmentQuickBuildPaths.composeCompilerPlugin reads.
from(daemonProject.configurations.named("composeCompilerPlugin")) {
Expand Down Expand Up @@ -550,9 +706,14 @@ androidComponents.onVariants { variant ->
AddFileToAssetsTask::outputDirectory,
)

// The same test that adds assets/release, and the distribution in it, to this variant.
val bundlesDistribution = hasBundledAssets(variant)
val copyDaemonZip =
tasks.register<AddFileToAssetsTask>("copy${variantName}QuickBuildDaemonZip") {
dependsOn(quickBuildDaemonZip)
if (bundlesDistribution) {
dependsOn(quickBuildBundledDistGate)
}
inputFile.set(quickBuildDaemonZip.flatMap { it.archiveFile })
baseAssetsPath.set("data/common")
}
Expand Down
Loading
Loading