Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
6e6351a
ADFA-5053: Move javac's fs-adjacent leaf classes into java-compiler
davidschachterADFA Aug 7, 2026
87b1581
ADFA-5053: Extract subprojects/javac-fs as its own resident module
davidschachterADFA Aug 7, 2026
73b9919
ADFA-5052: Defer JavaCompilerService/SourceFileManager construction u…
davidschachterADFA Aug 6, 2026
20ddbeb
ADFA-5052: Serialize the deferred javac reset against concurrent acce…
davidschachterADFA Aug 6, 2026
6227c61
ADFA-5052: Fix exception handling, analyze() bypass, and a narrow pos…
davidschachterADFA Aug 7, 2026
e3b96be
ADFA-5053: Add lsp/java-api bridge module
davidschachterADFA Aug 7, 2026
a9b4bf6
ADFA-5053: Split lsp/java into a resident shell and lsp/java-compiler…
davidschachterADFA Aug 7, 2026
881dabf
ADFA-5053: Add the java-compiler-carrier module, fix real duplicate-c…
davidschachterADFA Aug 7, 2026
861d411
ADFA-5053: Wire the javac carrier into app's assets
davidschachterADFA Aug 7, 2026
da6b1ad
ADFA-5053: Remove app's now-pointless openjdk keep rule, add ADR 0012
davidschachterADFA Aug 7, 2026
7b3daf9
ADFA-5053: Widen 3 cross-classloader protected/package-private member…
davidschachterADFA Aug 7, 2026
7e1377b
ADFA-5053: Document the cross-classloader protected-access hazard in …
davidschachterADFA Aug 7, 2026
147f04f
ADFA-5053: Apply spotless/ktlint formatting to relocated javac files
davidschachterADFA Aug 7, 2026
561b793
ADFA-5053: Fix ADR 0012's javapoet duplicate-class-identity claim
davidschachterADFA Aug 7, 2026
a80de78
Merge branch 'stage' into task/ADFA-5053-lazy-load-javac-carrier
davidschachterADFA Aug 7, 2026
513a2b2
ADFA-5053: Stop duplicating tree-sitter/sora-editor into the javac ca…
davidschachterADFA Aug 7, 2026
41abcc7
ADFA-5053: Close concurrency and lifecycle gaps in the carrier isolation
davidschachterADFA Aug 7, 2026
c54d59c
ADFA-5053: Fix correctness bugs in edit/rewrite handlers and debug paths
davidschachterADFA Aug 7, 2026
8494693
ADFA-5053: Add missing test coverage found during code review
davidschachterADFA Aug 7, 2026
1d1313e
ADFA-5053: Fix ADR 0012's ADR 0011 precedent claim, document Reusable…
davidschachterADFA Aug 7, 2026
e0826ac
Merge branch 'task/ADFA-5053-lazy-load-javac-carrier' of github.com:a…
davidschachterADFA Aug 7, 2026
d28f1ad
ADFA-5053: Fix flaky copyJavaCompilerCarrierToAssets CI failures
davidschachterADFA Aug 8, 2026
17e1465
ADFA-5053: Stop declaring 5 resident libraries as implementation in j…
davidschachterADFA Aug 8, 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 .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,7 @@ assets/gradle-*.zip
app/src/release/assets/database/documentation.db
app/src/main/assets/database/documentation
app/src/main/assets/database/documentation.db
app/src/main/assets/data/common/java-compiler-carrier.apk

# AI plugin development artifacts (moved to plugin-examples repo)
*.cgp
Expand Down
Original file line number Diff line number Diff line change
@@ -1,52 +1,60 @@
/*
* This file is part of AndroidIDE.
*
* AndroidIDE is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* AndroidIDE is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
*/

package com.itsaky.androidide.actions.locations

import android.content.Context
import android.graphics.drawable.Drawable
import androidx.core.content.ContextCompat
import com.itsaky.androidide.actions.ActionData
import com.itsaky.androidide.actions.ActionItem
import com.itsaky.androidide.actions.ActionMenu
import com.itsaky.androidide.idetooltips.TooltipTag
import com.itsaky.androidide.resources.R

/** @author Akash Yadav */
object CodeActionsMenu : ActionMenu {

const val ID = "ide.editor.code.actions"

override val children: MutableSet<ActionItem> = mutableSetOf()
override val id: String = ID

override var label: String = "Code actions"
override var visible = true
override var enabled: Boolean = true
override var icon: Drawable? = null
override fun retrieveTooltipTag(isAlternateContext: Boolean) =
TooltipTag.EDITOR_TOOLBAR_CODE_ACTIONS
override var requiresUIThread: Boolean = false
override var location: ActionItem.Location = ActionItem.Location.EDITOR_TEXT_ACTIONS

override fun prepare(data: ActionData) {
super.prepare(data)
if (icon == null) {
icon = ContextCompat.getDrawable(data[Context::class.java]!!, R.drawable.ic_code)
}
}
}
/*
* This file is part of AndroidIDE.
*
* AndroidIDE is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* AndroidIDE is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
*/

package com.itsaky.androidide.actions.locations

import android.content.Context
import android.graphics.drawable.Drawable
import androidx.core.content.ContextCompat
import com.itsaky.androidide.actions.ActionData
import com.itsaky.androidide.actions.ActionItem
import com.itsaky.androidide.actions.ActionMenu
import com.itsaky.androidide.idetooltips.TooltipTag
import com.itsaky.androidide.resources.R
import java.util.concurrent.CopyOnWriteArraySet

/** @author Akash Yadav */
object CodeActionsMenu : ActionMenu {
const val ID = "ide.editor.code.actions"

// Registered/unregistered from LSP-dispatch threads (e.g. JavaCompilerSessionImpl's
// registerCodeActions()/unregisterCodeActions(), called from ensureProjectReset()/
// shutdown()) concurrently with the UI thread reading it every time the code-actions menu
// is rendered (ActionMenu.prepare()/isAtLeastOneChildVisible()) -- CopyOnWriteArraySet avoids
// both a ConcurrentModificationException on a plain set and needing external synchronization
// around every read, at the cost of a full backing-array copy per add/remove (cheap: this
// set is small and mutated only on session register/unregister, not per keystroke).
override val children: MutableSet<ActionItem> = CopyOnWriteArraySet()
override val id: String = ID

override var label: String = "Code actions"
override var visible = true
override var enabled: Boolean = true
override var icon: Drawable? = null

override fun retrieveTooltipTag(isAlternateContext: Boolean) = TooltipTag.EDITOR_TOOLBAR_CODE_ACTIONS

override var requiresUIThread: Boolean = false
override var location: ActionItem.Location = ActionItem.Location.EDITOR_TEXT_ACTIONS

override fun prepare(data: ActionData) {
super.prepare(data)
if (icon == null) {
icon = ContextCompat.getDrawable(data[Context::class.java]!!, R.drawable.ic_code)
}
}
}
48 changes: 47 additions & 1 deletion app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,15 @@ plugins {
alias(libs.plugins.google.services)
}

// Forces :subprojects:java-compiler-carrier to configure eagerly, ahead of app's own
// configuration. Without this, org.gradle.configureondemand=true (gradle.properties) only
// reaches that project lazily via copyJavaCompilerCarrierToAssets's cross-project dependsOn,
// which trips "DefaultClassLoaderScope must be locked before it can be used to compute a
// classpath" for this (com.android.application) target specifically -- plugin-api, referenced
// the same way by copyPluginApiJarToAssets below, doesn't hit this because app already
// configures it naturally as a real compile dependency.
evaluationDependsOn(":subprojects:java-compiler-carrier")

fun propOrEnv(name: String): String =
project.findProperty(name) as String?
?: System.getenv(name)
Expand Down Expand Up @@ -288,7 +297,6 @@ dependencies {
implementation(projects.eventbusEvents)
implementation(projects.gradlePluginConfig)
implementation(projects.subprojects.aaptcompiler)
implementation(projects.subprojects.javacServices)
implementation(projects.subprojects.kotlinAnalysisApi)
implementation(projects.subprojects.shizukuApi)
implementation(projects.subprojects.shizukuManager)
Expand Down Expand Up @@ -409,6 +417,44 @@ tasks.register("downloadDocDb") {
}
}

// Copies the isolated javac carrier APK -- built by :subprojects:java-compiler-carrier, which
// bundles lsp:java-compiler-impl (the vendored javac fork + its LSP consumers) -- straight into
// app's own assets, so it ships in the base APK and D8 never merges it into app's own
// classes*.dex. JavaCompilerLoader (lsp:java) extracts and DexClassLoader-loads it lazily on
// first real .java-file interaction (ADFA-5053, mirrors ADR 0011's Kotlin precedent). No PNG
// optimization needed here (unlike the Kotlin carrier) -- this module has no resources at all.
tasks.register("copyJavaCompilerCarrierToAssets") {
// The source directory below is AGP's own Provider<Directory> for the release variant's real
// APK output (see releaseApkOutputDir in java-compiler-carrier's build.gradle.kts) --
// wiring inputs.dir() to it, rather than a hardcoded path guessing the output filename
// ("-unsigned", versioned, etc.), ties Gradle's dependency tracking to the actual producing
// task (packageV8Release) instead of just dependsOn ordering, which intermittently raced the
// file's own write-to-disk on some CI runs (ADFA-5053) even though locally it usually won
// the race. See evaluationDependsOn(":subprojects:java-compiler-carrier") above for why this
// avoids project(":subprojects:java-compiler-carrier").layout... here.
dependsOn(":subprojects:java-compiler-carrier:assembleV8Release")
@Suppress("UNCHECKED_CAST")
val sourceDir =
project(":subprojects:java-compiler-carrier").extensions.extraProperties["releaseApkOutputDir"]
as Provider<Directory>
val destFile = layout.projectDirectory.file("src/main/assets/data/common/java-compiler-carrier.apk")
inputs.dir(sourceDir)
outputs.file(destFile)
doLast {
val apkFile =
sourceDir
.get()
.asFileTree
.matching { include("*.apk") }
.singleFile
apkFile.copyTo(destFile.asFile, overwrite = true)
}
}

tasks.named("preBuild") {
dependsOn("copyJavaCompilerCarrierToAssets")
}

tasks.register("copyPluginApiJarToAssets") {
dependsOn(":plugin-api:createPluginApiJar")
val sourceFile = project(":plugin-api").layout.buildDirectory.file("libs/plugin-api-1.0.0.jar")
Expand Down
3 changes: 0 additions & 3 deletions app/proguard-rules.pro
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,6 @@
-keep class javax.** { *; }
-keep class jdkx.** { *; }

# keep javac classes
-keep class openjdk.** { *; }

# Android builder model interfaces
-keep class com.android.** { *; }

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,6 @@ import com.itsaky.androidide.idetooltips.TooltipTag
import com.itsaky.androidide.lookup.Lookup
import com.itsaky.androidide.lsp.IDELanguageClientImpl
import com.itsaky.androidide.lsp.debug.DebugClientConnectionResult
import com.itsaky.androidide.lsp.java.utils.CancelChecker
import com.itsaky.androidide.models.Position
import com.itsaky.androidide.models.Range
import com.itsaky.androidide.models.SearchResult
Expand Down Expand Up @@ -195,6 +194,11 @@ abstract class ProjectHandlerActivity : BaseEditorActivity() {
const val STATE_KEY_SHOULD_INITIALIZE = "ide.editor.isInitializing"

private const val PLUGIN_SEARCH_TIMEOUT_SECONDS = 10L

private fun isCancellation(err: Throwable?): Boolean {
err ?: return false
return err is CancellationException || isCancellation(err.cause)
}
}

abstract fun doCloseAll()
Expand Down Expand Up @@ -613,7 +617,7 @@ abstract class ProjectHandlerActivity : BaseEditorActivity() {
releaseServerListener()

if (result == null || !result.isSuccessful || error != null) {
if (!CancelChecker.isCancelled(error)) {
if (!isCancellation(error)) {
log.error("An error occurred initializing the project with Tooling API", error)
}

Expand Down
27 changes: 17 additions & 10 deletions composite-builds/build-deps/google-java-format/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -16,20 +16,27 @@
*/

plugins {
id("com.android.library")
id("com.itsaky.androidide.build")
id("com.android.library")
id("com.itsaky.androidide.build")
}

android {
namespace = "com.google.googlejavaformat"
namespace = "com.google.googlejavaformat"
}

dependencies {
implementation(libs.google.guava)
implementation(libs.google.auto.value.annotations)
implementation(libs.google.auto.service.annotations)
implementation(projects.buildDeps.javac)
implementation(libs.google.guava)
implementation(libs.google.auto.value.annotations)
implementation(libs.google.auto.service.annotations)

annotationProcessor(libs.google.auto.value.ap)
annotationProcessor(libs.google.auto.service)
}
// NOT projects.buildDeps.javac (the aggregate): java-compiler must stay resident-only when
// this module is consumed by the isolated javac carrier (ADFA-5053) -- see
// composite-builds/build-deps/jdk-compiler's identical fix for the full rationale. This
// module genuinely runs javac's own parser at runtime (that's how it reformats source), so
// jdk-compiler itself stays a real, bundled dependency.
implementation(projects.buildDeps.jdkCompiler)
compileOnly(projects.buildDeps.javaCompiler)

annotationProcessor(libs.google.auto.value.ap)
annotationProcessor(libs.google.auto.service)
}
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,14 @@ public List<Path> getJarClassPath(Path file) throws IOException {
}
}

protected Optional<BasicFileAttributes> getAttributes(Path file) {
// public, not protected: JavacFileManager (openjdk.tools.javac.file, isolated in the javac
// carrier per ADFA-5053) calls this directly on a CacheFSInfo instance, which is resident
// (loaded by the parent classloader). ART treats the two as different runtime packages
// despite the identical package name, since same-package/protected access is resolved by
// classloader identity, not just the package string -- protected access across that
// boundary throws IllegalAccessError at runtime, caught by on-device testing, not by the
// build or unit tests.
public Optional<BasicFileAttributes> getAttributes(Path file) {
return attributeCache.computeIfAbsent(file, this::maybeReadAttributes);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,10 @@ public String getPath() {
*/
public static class RelativeDirectory extends RelativePath {

static RelativeDirectory forPackage(CharSequence packageName) {
// public, not package-private: JavacFileManager/JRTIndex (isolated in the javac carrier
// per ADFA-5053) call this on RelativeDirectory, which is resident -- same
// cross-classloader access rationale as CacheFSInfo.getAttributes above.
public static RelativeDirectory forPackage(CharSequence packageName) {
return new RelativeDirectory(packageName.toString().replace('.', '/'));
}

Expand Down Expand Up @@ -158,7 +161,10 @@ public String toString() {
* Internally, the file separator is always '/'. It never ends in '/'.
*/
public static class RelativeFile extends RelativePath {
static RelativeFile forClass(CharSequence className, JavaFileObject.Kind kind) {
// public, not package-private: JavacFileManager (isolated in the javac carrier per
// ADFA-5053) calls this on RelativeFile, which is resident -- same cross-classloader
// access rationale as CacheFSInfo.getAttributes above.
public static RelativeFile forClass(CharSequence className, JavaFileObject.Kind kind) {
return new RelativeFile(className.toString().replace('.', '/') + kind.extension);
}

Expand Down
10 changes: 7 additions & 3 deletions composite-builds/build-deps/javapoet/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,13 @@
*/

plugins {
kotlin("jvm")
kotlin("jvm")
}

dependencies {
api(projects.buildDeps.javaCompiler)
}
// javapoet itself stays fully resident (see lsp/java-compiler-impl/build.gradle.kts) --
// templates-api/templates-impl (the "New Project" wizard) need it unconditionally, unlike
// javac. So unlike jdk-compiler's identical-looking dependency, this one stays `api`: there's
// no isolated consumer to duplicate java-compiler's classes into.
api(projects.buildDeps.javaCompiler)
}
8 changes: 7 additions & 1 deletion composite-builds/build-deps/jdk-compiler/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -30,5 +30,11 @@ tasks.withType<JavaCompile>().configureEach {
}

dependencies {
api(projects.buildDeps.javaCompiler)
// compileOnly, not api: java-compiler must stay resident-only when jdk-compiler is consumed
// by the isolated javac carrier (ADFA-5053) -- an `api` dependency here would propagate to
// every consumer's runtime/packaging classpath regardless of how *they* declare their own
// dependency on this module, duplicating CacheFSInfo/Context/etc. into the carrier dex. The
// `javac` aggregate module (composite-builds/build-deps/javac) still `api`s both modules
// itself for its own (resident-only) consumers.
compileOnly(projects.buildDeps.javaCompiler)
}
Loading
Loading