From d557b2644a37c47618d373c4a71eaf39c0beb676 Mon Sep 17 00:00:00 2001 From: David Schachter Date: Fri, 10 Jul 2026 18:09:23 -0700 Subject: [PATCH 1/2] ADFA-4636: Remove Kotlin/JS and Kotlin/Native backends from kt-android.jar Adds excludeEntryPrefixes support to ExternalJarDependencyConfiguration and ExternalAssetsPlugin so that ZIP entries matching listed prefixes are stripped from a cached copy of the downloaded JAR before it is added as a dependency. Configures kotlin-analysis-api to strip all org/jetbrains/kotlin/js/ and org/jetbrains/kotlin/konan/ classes (794 entries, ~3.4 MB) since neither backend has any place in an Android IDE. Co-Authored-By: Claude Sonnet 4.6 --- .../plugins/ExternalAssetsPlugin.kt | 40 ++++++++++++++++++- .../extension/ExternalAssetsExtension.kt | 7 ++++ .../kotlin-analysis-api/build.gradle.kts | 22 ++++++++++ 3 files changed, 68 insertions(+), 1 deletion(-) diff --git a/composite-builds/build-logic/plugins/src/main/java/com/itsaky/androidide/plugins/ExternalAssetsPlugin.kt b/composite-builds/build-logic/plugins/src/main/java/com/itsaky/androidide/plugins/ExternalAssetsPlugin.kt index fd53b18156..d7993c29be 100644 --- a/composite-builds/build-logic/plugins/src/main/java/com/itsaky/androidide/plugins/ExternalAssetsPlugin.kt +++ b/composite-builds/build-logic/plugins/src/main/java/com/itsaky/androidide/plugins/ExternalAssetsPlugin.kt @@ -14,10 +14,15 @@ import org.gradle.api.ExtensiblePolymorphicDomainObjectContainer import org.gradle.api.Plugin import org.gradle.api.Project import org.gradle.api.file.Directory +import org.gradle.api.logging.Logger import org.gradle.api.model.ObjectFactory import org.gradle.api.provider.Provider import org.gradle.kotlin.dsl.dependencies import org.gradle.kotlin.dsl.newInstance +import java.io.File +import java.util.zip.ZipEntry +import java.util.zip.ZipInputStream +import java.util.zip.ZipOutputStream import javax.inject.Inject private fun Project.externalAssetsCacheDir(): Provider = @@ -76,8 +81,17 @@ abstract class ExternalAssetsExtension @Inject constructor( logger = project.logger ) + val jarFileName = if (config.excludeEntryPrefixes.isEmpty()) { + config.jarName + } else { + val original = cacheDir.resolve(config.jarName) + val stripped = cacheDir.resolve(config.jarName.replace(".jar", "-stripped.jar")) + stripJar(original, stripped, config.excludeEntryPrefixes, project.logger) + stripped.name + } + val dep = project.dependencies.create(project.fileTree(cacheDir) { - include(config.jarName) + include(jarFileName) }) project.dependencies { @@ -86,6 +100,30 @@ abstract class ExternalAssetsExtension @Inject constructor( } } +private fun stripJar(source: File, dest: File, excludePrefixes: List, logger: Logger) { + if (dest.exists() && dest.lastModified() >= source.lastModified()) { + logger.lifecycle("Skipping strip of ${source.name}: stripped copy is up-to-date") + return + } + logger.lifecycle("Stripping ${excludePrefixes.size} prefix(es) from ${source.name}…") + ZipInputStream(source.inputStream().buffered()).use { zin -> + ZipOutputStream(dest.outputStream().buffered()).use { zout -> + var entry: ZipEntry? = zin.nextEntry + while (entry != null) { + if (excludePrefixes.none { entry!!.name.startsWith(it) }) { + zout.putNextEntry(ZipEntry(entry.name)) + zin.copyTo(zout) + zout.closeEntry() + } + zin.closeEntry() + entry = zin.nextEntry + } + } + } + val savedKb = (source.length() - dest.length()) / 1024 + logger.lifecycle("Stripped ${source.name}: saved ${savedKb} KB (${source.length()} → ${dest.length()} bytes)") +} + /** * Plugin used to download files from external sources and package them as assets. * diff --git a/composite-builds/build-logic/plugins/src/main/java/com/itsaky/androidide/plugins/extension/ExternalAssetsExtension.kt b/composite-builds/build-logic/plugins/src/main/java/com/itsaky/androidide/plugins/extension/ExternalAssetsExtension.kt index e32f05df27..6f202ee4db 100644 --- a/composite-builds/build-logic/plugins/src/main/java/com/itsaky/androidide/plugins/extension/ExternalAssetsExtension.kt +++ b/composite-builds/build-logic/plugins/src/main/java/com/itsaky/androidide/plugins/extension/ExternalAssetsExtension.kt @@ -123,4 +123,11 @@ abstract class ExternalJarDependencyConfiguration @Inject constructor( * The name of the JAR file to be added to the dependencies. */ var jarName: String = "${name}.jar" + + /** + * Entry name prefixes to strip from the JAR before adding it as a dependency. + * Any ZIP entry whose name starts with one of these prefixes is excluded from the + * stripped copy (e.g. "org/jetbrains/kotlin/js/"). + */ + var excludeEntryPrefixes: List = emptyList() } \ No newline at end of file diff --git a/subprojects/kotlin-analysis-api/build.gradle.kts b/subprojects/kotlin-analysis-api/build.gradle.kts index 34e3e05a07..d674958397 100644 --- a/subprojects/kotlin-analysis-api/build.gradle.kts +++ b/subprojects/kotlin-analysis-api/build.gradle.kts @@ -23,5 +23,27 @@ externalAssets { url = uri("$ktAndroidRepo/releases/download/$ktAndroidTag/$ktAndroidJarName"), sha256Checksum = "2069ed685dafd6eed36ebe242004ed5e24e28360293117323e2c988afefa6767", ) + excludeEntryPrefixes = listOf( + // Kotlin/JS backend — not needed in an Android IDE + "org/jetbrains/kotlin/js/", + "META-INF/ir.serialization.js.kotlin_module", + "META-INF/checkers.js.kotlin_module", + "META-INF/compiler.common.js.kotlin_module", + "META-INF/js.ast.kotlin_module", + "META-INF/js.config.kotlin_module", + "META-INF/js.frontend.common.kotlin_module", + "META-INF/js.frontend.kotlin_module", + "META-INF/js.parser.kotlin_module", + "META-INF/js.serializer.kotlin_module", + // Kotlin/Native (konan) backend — not needed in an Android IDE + "org/jetbrains/kotlin/konan/", + "META-INF/checkers.native.kotlin_module", + "META-INF/compiler.common.native.kotlin_module", + "META-INF/decompiler-native.kotlin_module", + "META-INF/fir-native.kotlin_module", + "META-INF/frontend.native.kotlin_module", + "META-INF/ir.serialization.native.kotlin_module", + "META-INF/kotlin-native-utils.kotlin_module", + ) } } From 48ad48fb185f6a1ad2b0d6aeb18437894e5769a4 Mon Sep 17 00:00:00 2001 From: David Schachter Date: Fri, 10 Jul 2026 18:25:11 -0700 Subject: [PATCH 2/2] ADFA-4636: Fix three correctness issues in stripJar - Use removeSuffix(".jar") instead of replace(".jar", ...) so only the trailing extension is affected, not any earlier occurrence in the name - Add a .prefixes companion file alongside the stripped JAR so changes to excludeEntryPrefixes invalidate the cached output even when the source JAR mtime is unchanged - Write to a .tmp file and atomically rename to dest on success; delete the temp file on any failure to prevent partial output from being treated as valid on subsequent builds Co-Authored-By: Claude Sonnet 4.6 --- .../plugins/ExternalAssetsPlugin.kt | 45 ++++++++++++++----- 1 file changed, 33 insertions(+), 12 deletions(-) diff --git a/composite-builds/build-logic/plugins/src/main/java/com/itsaky/androidide/plugins/ExternalAssetsPlugin.kt b/composite-builds/build-logic/plugins/src/main/java/com/itsaky/androidide/plugins/ExternalAssetsPlugin.kt index d7993c29be..c263fa308b 100644 --- a/composite-builds/build-logic/plugins/src/main/java/com/itsaky/androidide/plugins/ExternalAssetsPlugin.kt +++ b/composite-builds/build-logic/plugins/src/main/java/com/itsaky/androidide/plugins/ExternalAssetsPlugin.kt @@ -20,6 +20,9 @@ import org.gradle.api.provider.Provider import org.gradle.kotlin.dsl.dependencies import org.gradle.kotlin.dsl.newInstance import java.io.File +import java.nio.file.AtomicMoveNotSupportedException +import java.nio.file.Files +import java.nio.file.StandardCopyOption import java.util.zip.ZipEntry import java.util.zip.ZipInputStream import java.util.zip.ZipOutputStream @@ -85,7 +88,7 @@ abstract class ExternalAssetsExtension @Inject constructor( config.jarName } else { val original = cacheDir.resolve(config.jarName) - val stripped = cacheDir.resolve(config.jarName.replace(".jar", "-stripped.jar")) + val stripped = cacheDir.resolve("${config.jarName.removeSuffix(".jar")}-stripped.jar") stripJar(original, stripped, config.excludeEntryPrefixes, project.logger) stripped.name } @@ -101,24 +104,42 @@ abstract class ExternalAssetsExtension @Inject constructor( } private fun stripJar(source: File, dest: File, excludePrefixes: List, logger: Logger) { - if (dest.exists() && dest.lastModified() >= source.lastModified()) { + val prefixesFile = File(dest.parentFile, "${dest.name}.prefixes") + val currentPrefixContent = excludePrefixes.sorted().joinToString("\n") + val upToDate = dest.exists() + && dest.lastModified() >= source.lastModified() + && prefixesFile.exists() + && prefixesFile.readText() == currentPrefixContent + if (upToDate) { logger.lifecycle("Skipping strip of ${source.name}: stripped copy is up-to-date") return } logger.lifecycle("Stripping ${excludePrefixes.size} prefix(es) from ${source.name}…") - ZipInputStream(source.inputStream().buffered()).use { zin -> - ZipOutputStream(dest.outputStream().buffered()).use { zout -> - var entry: ZipEntry? = zin.nextEntry - while (entry != null) { - if (excludePrefixes.none { entry!!.name.startsWith(it) }) { - zout.putNextEntry(ZipEntry(entry.name)) - zin.copyTo(zout) - zout.closeEntry() + val tmp = File(dest.parentFile, "${dest.name}.tmp") + try { + ZipInputStream(source.inputStream().buffered()).use { zin -> + ZipOutputStream(tmp.outputStream().buffered()).use { zout -> + var entry: ZipEntry? = zin.nextEntry + while (entry != null) { + if (excludePrefixes.none { entry!!.name.startsWith(it) }) { + zout.putNextEntry(ZipEntry(entry.name)) + zin.copyTo(zout) + zout.closeEntry() + } + zin.closeEntry() + entry = zin.nextEntry } - zin.closeEntry() - entry = zin.nextEntry } } + try { + Files.move(tmp.toPath(), dest.toPath(), StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE) + } catch (_: AtomicMoveNotSupportedException) { + Files.move(tmp.toPath(), dest.toPath(), StandardCopyOption.REPLACE_EXISTING) + } + prefixesFile.writeText(currentPrefixContent) + } catch (e: Exception) { + tmp.delete() + throw e } val savedKb = (source.length() - dest.length()) / 1024 logger.lifecycle("Stripped ${source.name}: saved ${savedKb} KB (${source.length()} → ${dest.length()} bytes)")