Skip to content
Merged
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
181 changes: 134 additions & 47 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -517,53 +517,140 @@ tasks.register<Zip>("createPluginArtifactsZip") {
// Merges the API surface plugins already compile against (plugin-api + common +
// eventbus-events + idetooltips) into one coordinate. The three add-ons are
// v7/v8-flavored (unlike plugin-api); their classes are ABI-neutral so v8 is used.
tasks.register<Jar>("assemblePluginApiFatJar") {
dependsOn(
":plugin-api:assembleRelease",
":common:assembleV8Release",
":eventbus-events:assembleV8Release",
":idetooltips:assembleV8Release",
)
archiveFileName.set("plugin-api-1.0.0.jar")
destinationDirectory.set(layout.buildDirectory.dir("plugin-maven-repo-staging"))
duplicatesStrategy = DuplicatesStrategy.EXCLUDE

from(
zipTree(
project(":plugin-api")
.layout.buildDirectory
.file("intermediates/aar_main_jar/release/syncReleaseLibJars/classes.jar")
.get()
.asFile,
),
)
from(
zipTree(
project(":common")
.layout.buildDirectory
.file("intermediates/aar_main_jar/v8Release/syncV8ReleaseLibJars/classes.jar")
.get()
.asFile,
),
)
from(
zipTree(
project(":eventbus-events")
.layout.buildDirectory
.file("intermediates/aar_main_jar/v8Release/syncV8ReleaseLibJars/classes.jar")
.get()
.asFile,
),
)
from(
zipTree(
project(":idetooltips")
.layout.buildDirectory
.file("intermediates/aar_main_jar/v8Release/syncV8ReleaseLibJars/classes.jar")
.get()
.asFile,
),
)
val pluginApiFatJar =
tasks.register<Jar>("assemblePluginApiFatJar") {
dependsOn(
":plugin-api:assembleRelease",
":common:assembleV8Release",
":eventbus-events:assembleV8Release",
":idetooltips:assembleV8Release",
)
archiveFileName.set("plugin-api-1.0.0.jar")
destinationDirectory.set(layout.buildDirectory.dir("plugin-maven-repo-staging"))
duplicatesStrategy = DuplicatesStrategy.EXCLUDE

from(
zipTree(
project(":plugin-api")
.layout.buildDirectory
.file("intermediates/aar_main_jar/release/syncReleaseLibJars/classes.jar")
.get()
.asFile,
),
)
from(
zipTree(
project(":common")
.layout.buildDirectory
.file("intermediates/aar_main_jar/v8Release/syncV8ReleaseLibJars/classes.jar")
.get()
.asFile,
),
)
from(
zipTree(
project(":eventbus-events")
.layout.buildDirectory
.file("intermediates/aar_main_jar/v8Release/syncV8ReleaseLibJars/classes.jar")
.get()
.asFile,
),
)
from(
zipTree(
project(":idetooltips")
.layout.buildDirectory
.file("intermediates/aar_main_jar/v8Release/syncV8ReleaseLibJars/classes.jar")
.get()
.asFile,
),
)
}

// Plugins load through DexClassLoader, so R8 never sees a reference to the ABI
// they call. Emitting one -keep per class in the fat jar published above makes
// the rules exactly the published ABI, so they cannot drift as classes move
// between the four merged modules.
abstract class GeneratePluginApiKeepRules : DefaultTask() {
@get:InputFile
abstract val fatJar: RegularFileProperty

// Module path -> a class that module must contribute. The fat jar merges four
// hardcoded AGP intermediate paths with DuplicatesStrategy.EXCLUDE, so a path
// that stops resolving (AGP layout change, dropped from(...)) drops that
// module's classes silently instead of failing.
@get:Input
abstract val sentinelClasses: MapProperty<String, String>

@get:OutputFile
abstract val keepRules: RegularFileProperty

@TaskAction
fun generate() {
val jar = fatJar.get().asFile
val classes =
ZipFile(jar).use { zip ->
zip
.entries()
.asSequence()
.map { it.name }
.filter { it.endsWith(".class") }
.map { it.removeSuffix(".class").replace('/', '.') }
.sorted()
.toList()
}

check(classes.isNotEmpty()) {
"$jar holds no classes; generating keep rules from it would ship the plugin ABI unprotected."
}

val present = classes.toSet()
val missing = sentinelClasses.get().filterValues { it !in present }
check(missing.isEmpty()) {
missing.entries.joinToString(
prefix = "$jar is missing whole modules, so their ABI would ship unprotected: ",
) { "${it.key} (no ${it.value})" }
}

val header =
"""
# Generated by :app:generatePluginApiKeepRules -- do not edit.
# One -keep per class in the published plugin-api fat jar: plugins resolve the
# ABI parent-first through DexClassLoader, which R8 cannot see.
#
# These cover the ABI only. Plugins also resolve kotlin.** from the app's dex,
# which app/proguard-rules.pro keeps separately (ADFA-5156).

""".trimIndent()

val file = keepRules.get().asFile
file.parentFile.mkdirs()
file.writeText(
classes.joinToString("\n", prefix = header, postfix = "\n") { "-keep class $it { *; }" },
)
logger.lifecycle("Wrote ${classes.size} plugin-ABI keep rules to ${file.absolutePath}")
}
}

val pluginApiKeepRules =
tasks.register<GeneratePluginApiKeepRules>("generatePluginApiKeepRules") {
fatJar.set(pluginApiFatJar.flatMap { it.archiveFile })
sentinelClasses.set(
mapOf(
":plugin-api" to "com.itsaky.androidide.plugins.IPlugin",
":common" to "com.itsaky.androidide.utils.Environment",
":eventbus-events" to "com.itsaky.androidide.eventbus.events.Event",
":idetooltips" to "com.itsaky.androidide.idetooltips.IDETooltipItem",
),
)
keepRules.set(layout.buildDirectory.file("generated/proguard/plugin-api-keep.pro"))
}

// proguardFiles (not dependsOn) so Gradle infers the dependency for every
// consumer: R8 and lint both read this list, and wiring only R8 fails validation
// in a real release build. Debug variants stay off the fat jar entirely.
androidComponents.onVariants(androidComponents.selector().withBuildType("release")) { variant ->
variant.proguardFiles.add(pluginApiKeepRules.flatMap { it.keepRules })
Comment thread
Daniel-ADFA marked this conversation as resolved.
}

// Dependency-free POM for the fat plugin-api coordinate: it is compile-only/provided,
Expand Down
29 changes: 19 additions & 10 deletions app/proguard-rules.pro
Original file line number Diff line number Diff line change
Expand Up @@ -193,16 +193,25 @@
-keep class io.sentry.** { *; }
-dontwarn io.sentry.**

# ADFA-5156: TEMPORARY ROLLBACK of the R8 shrinking re-enabled in ADFA-3604.
# Plugins load parent-first through a stock DexClassLoader, so they resolve
# kotlin.** from the app's dex rather than their own bundled stdlib. R8 cannot
# see plugin call sites, so it strips every stdlib member the IDE itself does
# not call and plugins die with NoSuchMethodError at runtime (Sketch to UI:
# ArraysKt.maxOrNull([F)). With -dontobfuscate and -dontoptimize already set,
# this restores R8 to a pass-through and returns the release build to the
# configuration shipped before ADFA-3604. Revert once ADFA-5156 lands a
# targeted fix (keep rules for kotlin.**/kotlinx.coroutines.**).
-dontshrink
# ADFA-5156: plugins load parent-first through a stock DexClassLoader, so they
# resolve kotlin.** from the app's dex rather than their own bundled stdlib. R8
# cannot see plugin call sites, so it strips every stdlib member the IDE itself
# does not call and plugins die with NoSuchMethodError at runtime (Sketch to UI:
# ArraysKt.maxOrNull([F)). ADFA-5156 first fixed this with -dontshrink, which
# made R8 a pass-through over the whole app to protect one library; ADFA-5195
# replaced it with the targeted keeps below, restoring dead-code removal
# everywhere else.
#
# ADFA-5164 narrows this: freeze the stdlib subset plugins may rely on and have
# plugins bundle the rest. Until then the whole stdlib is the contract, so these
# stay broad -- and note the keeps further up cover only kotlin.reflect,
# kotlin.script and kotlinx.coroutines.internal, not kotlin.collections/text/io/
# sequences, which is the hole maxOrNull([F) fell through.
-keep class kotlin.** { *; }
-keep class kotlinx.coroutines.** { *; }

-keep class com.google.firebase.** { *; }
-keep class com.google.android.gms.** { *; }

## Plugin SPI
## Plugins are loaded dynamically via DexClassLoader, so R8 cannot see their
Expand Down
Loading