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..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 @@ -14,10 +14,18 @@ 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.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 import javax.inject.Inject private fun Project.externalAssetsCacheDir(): Provider = @@ -76,8 +84,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.removeSuffix(".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 +103,48 @@ abstract class ExternalAssetsExtension @Inject constructor( } } +private fun stripJar(source: File, dest: File, excludePrefixes: List, logger: Logger) { + 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}…") + 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 + } + } + } + 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)") +} + /** * 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", + ) } }