Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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 docs/adr/0012-lazy-load-javac-via-dexclassloader.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ Investigation found this coupling narrower than it first looked: none of `CacheF
- First `.java`-file interaction in a session now pays a one-time synchronous latency spike (asset extraction on first run + `DexClassLoader` construction + `JavaCompilerService`/`SourceFileManager` bootstrap) on top of ADFA-5052's own deferred-reset cost.
- A third resident/isolated classloader boundary to reason about (after Kotlin's and the plugin system's). The same rule as ADR 0011 applies and now has two worked examples of getting it wrong: an `api` dependency anywhere in a vendored composite build's *own* `build.gradle.kts` propagates to every consumer's runtime classpath regardless of how the consumer declares its dependency — `compileOnly` has to be applied at the source of the leak, not just where it's consumed. A second, distinct rule this ADR adds: every resident member the isolated fork calls across the boundary must be `public` — `protected`/package-private access throws `IllegalAccessError` at runtime even when both classes share a package name, since ART checks classloader identity, not just the package string, and this has no build-time or unit-test signal at all.
- The debugger's breakpoint/stack-frame source-path resolution (`JavaDebugAdapter`/`ModelUtils.asLspLocation`) took on a narrow, real dependency on `JavaCompilerProvider`/`SourceFileObject` that the isolation boundary can't ignore; it now resolves through `IJavaCompilerSession.findSourceFilePath` (returning a plain path, not the isolated `SourceFileObject` type) instead, and degrades to filename-only when no session exists yet.
- A third rule (ADFA-5068): converting a resident dependency from `implementation` to `compileOnly` can trip AGP's `compileClasspath.shouldResolveConsistentlyWith(runtimeClasspath)` check for any `com.android.library` module, if that dependency was incidentally anchoring a high-enough transitive version (of e.g. `androidx.annotation`, `kotlin-stdlib`, `org.jetbrains:annotations`) against AGP's own unconditionally-injected `androidx.databinding:viewbinding`, which pins those same artifacts much lower on the runtime side once nothing else pulls them in. The fix isn't to re-add the dependency (that reintroduces the duplication this rule exists to avoid) but a `constraints {}` block on `implementation` bumping just the conflicting artifact(s) to the version already used everywhere else — a constraint, unlike a dependency, only rescopes an edge that's already reachable (here, via viewbinding), so it doesn't add anything new to the carrier beyond a version bump on a few KB of annotation classes. See `lsp/java-compiler-impl/build.gradle.kts`'s `constraints` block and `subprojects/javac-services/build.gradle.kts`.

## Alternatives considered

Expand Down
2 changes: 2 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,8 @@ common-javaparser = { module = "com.github.javaparser:javaparser-symbol-solver-c
common-lang3 = { module = "org.apache.commons:commons-lang3", version = "3.14.0" }
common-io = { module = "commons-io:commons-io", version = "2.15.1" }
common-kotlin = { module = "org.jetbrains.kotlin:kotlin-stdlib-jdk8", version.ref = "kotlin" }
common-kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kotlin" }
common-jetbrains-annotations = { module = "org.jetbrains:annotations", version = "24.1.0" }
common-kotlin-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlin-coroutines" }
common-kotlin-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "kotlin-coroutines" }
common-jkotlin = { module = "org.jetbrains.kotlin:kotlin-stdlib-jdk8", version.ref = "kotlin" }
Expand Down
17 changes: 17 additions & 0 deletions lsp/java-compiler-impl/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,23 @@ kapt {
}

dependencies {
// javac-services' (and this module's own) compileOnly androidx-heavy deps (sora-editor,
// appcompat, material, lsp:indexing, etc.) pull androidx.annotation/kotlin-stdlib/
// org.jetbrains:annotations at high versions on the compile classpath, but being compileOnly
// they're absent from the runtime classpath -- which then only sees AGP's unconditionally
// injected viewbinding's much lower transitive pins for the same artifacts. AGP's
// compile/runtime consistency check fails to reconcile the two. Constraining these three (not
// adding a dependency: each is already reachable via viewbinding, just at the wrong version)
// harmonizes both classpaths at the version already used everywhere else in the project,
// without adding a new edge. All three are a few KB of interfaces/annotations with no
// resources, unlike the androidx.core duplication this whole compileOnly effort exists to
// avoid -- see ADFA-5068.
constraints {
implementation(libs.androidx.annotation)
implementation(libs.common.kotlin.stdlib)
implementation(libs.common.jetbrains.annotations)
}

kapt(projects.annotationProcessors)

// Resident (bundled in the main app dex via editor/editor-api/lsp:java/etc.) -- like the
Expand Down
13 changes: 9 additions & 4 deletions subprojects/javac-services/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,15 @@ android {
}

dependencies {
implementation(libs.common.kotlin)
implementation(libs.google.guava)
implementation(projects.common)
implementation(projects.logger)
// Resident (see docs/adr/0012): kotlin-stdlib, :shared (ReflectUtils, VMUtils) and :logger
// (ILogger) are all already loaded by the parent classloader -- implementation here would
// duplicate their bytecode (and :common's androidx/guava graph, previously reached via
// projects.common) into the isolated carrier dex. compileOnly for the same reason as the
// block below. libs.google.guava was dropped entirely: this module's own code never
// references it directly -- it only ever arrived transitively through :common's api(guava).
compileOnly(libs.common.kotlin)
compileOnly(projects.shared)
compileOnly(projects.logger)

// The actual javac fork this module wraps -- bundled with this module wherever it ends up
// (isolated carrier, per ADFA-5053).
Expand Down
Loading