AGP work
diff --git a/libraries/tools/kotlin-compose-compiler/build.gradle.kts b/libraries/tools/kotlin-compose-compiler/build.gradle.kts
index 80258ff..2824ea5 100644
--- a/libraries/tools/kotlin-compose-compiler/build.gradle.kts
+++ b/libraries/tools/kotlin-compose-compiler/build.gradle.kts
@@ -1,4 +1,5 @@
import gradle.GradlePluginVariant
+import org.jetbrains.kotlin.gradle.tasks.KotlinJvmCompile
plugins {
id("gradle-plugin-common-configuration")
@@ -6,10 +7,23 @@
id("gradle-plugin-api-reference")
}
+project.updateJvmTarget("11")
+
+tasks.withType<KotlinJvmCompile>().configureEach {
+ compilerOptions {
+ freeCompilerArgs.add("-Xskip-metadata-version-check")
+ }
+}
+
dependencies {
commonApi(platform(project(":kotlin-gradle-plugins-bom")))
commonApi(project(":kotlin-gradle-plugin-model"))
commonApi(project(":kotlin-gradle-plugin"))
+
+ // TODO: figure out AGP dependency story
+ commonCompileOnly("com.android.tools.build:gradle-api:8.8.1") { isTransitive = false }
+ commonCompileOnly("com.android.tools.build:gradle:8.8.1") { isTransitive = false }
+ commonApi(project(":plugins:compose-compiler-plugin:mapping-generator"))
}
gradlePlugin {
diff --git a/libraries/tools/kotlin-compose-compiler/src/common/kotlin/org/jetbrains/kotlin/compose/compiler/gradle/ComposeCompilerSubplugin.kt b/libraries/tools/kotlin-compose-compiler/src/common/kotlin/org/jetbrains/kotlin/compose/compiler/gradle/ComposeCompilerSubplugin.kt
index 3a12889..9804536 100644
--- a/libraries/tools/kotlin-compose-compiler/src/common/kotlin/org/jetbrains/kotlin/compose/compiler/gradle/ComposeCompilerSubplugin.kt
+++ b/libraries/tools/kotlin-compose-compiler/src/common/kotlin/org/jetbrains/kotlin/compose/compiler/gradle/ComposeCompilerSubplugin.kt
@@ -10,6 +10,7 @@
import org.gradle.api.provider.Provider
import org.gradle.tooling.provider.model.ToolingModelBuilderRegistry
import org.jetbrains.kotlin.compose.compiler.gradle.internal.ComposeWithAgpConfig
+import org.jetbrains.kotlin.compose.compiler.gradle.internal.configureComposeMappingFile
import org.jetbrains.kotlin.compose.compiler.gradle.model.builder.ComposeCompilerModelBuilder
import org.jetbrains.kotlin.gradle.plugin.*
import javax.inject.Inject
@@ -38,6 +39,7 @@
override fun apply(target: Project) {
composeExtension = target.extensions.create("composeCompiler", ComposeCompilerGradlePluginExtension::class.java)
registry.register(ComposeCompilerModelBuilder())
+ target.configureComposeMappingFile()
}
override fun isApplicable(kotlinCompilation: KotlinCompilation<*>): Boolean {
diff --git a/libraries/tools/kotlin-compose-compiler/src/common/kotlin/org/jetbrains/kotlin/compose/compiler/gradle/internal/ComposeAgpMappingFile.kt b/libraries/tools/kotlin-compose-compiler/src/common/kotlin/org/jetbrains/kotlin/compose/compiler/gradle/internal/ComposeAgpMappingFile.kt
new file mode 100644
index 0000000..8130d02
--- /dev/null
+++ b/libraries/tools/kotlin-compose-compiler/src/common/kotlin/org/jetbrains/kotlin/compose/compiler/gradle/internal/ComposeAgpMappingFile.kt
@@ -0,0 +1,164 @@
+/*
+ * Copyright 2010-2025 JetBrains s.r.o. and Kotlin Programming Language contributors.
+ * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
+ */
+
+package org.jetbrains.kotlin.compose.compiler.gradle.internal
+
+import androidx.compose.compiler.mapping.ComposeMapping
+import androidx.compose.compiler.mapping.ErrorReporter
+import com.android.build.api.artifact.ScopedArtifact
+import com.android.build.api.artifact.SingleArtifact
+import com.android.build.api.variant.ApplicationAndroidComponentsExtension
+import com.android.build.api.variant.ScopedArtifacts
+import org.gradle.api.DefaultTask
+import org.gradle.api.Project
+import org.gradle.api.file.Directory
+import org.gradle.api.file.RegularFile
+import org.gradle.api.file.RegularFileProperty
+import org.gradle.api.internal.file.FileOperations
+import org.gradle.api.problems.ProblemGroup
+import org.gradle.api.problems.ProblemId
+import org.gradle.api.problems.Problems
+import org.gradle.api.problems.Severity
+import org.gradle.api.provider.ListProperty
+import org.gradle.api.tasks.*
+import org.gradle.internal.extensions.core.get
+import org.gradle.kotlin.dsl.findByType
+import org.gradle.kotlin.dsl.register
+import java.util.Locale.getDefault
+import javax.inject.Inject
+
+internal fun Project.configureComposeMappingFile() {
+ plugins.withId("com.android.application") {
+ project.extensions.findByType<ApplicationAndroidComponentsExtension>()?.onVariants { variant ->
+ if (!variant.isMinifyEnabled) return@onVariants
+
+ val produceTaskName = "produce${variant.name.capitalize()}ComposeMapping"
+ val taskProvider = project.tasks.register<ProduceMappingFileTask>(produceTaskName) {
+ output.set(project.layout.buildDirectory.file("intermediates/compose_mapping/${variant.name}/compose-mapping.txt"))
+ }
+
+ variant.artifacts
+ .forScope(ScopedArtifacts.Scope.ALL)
+ .use(taskProvider)
+ .toGet(
+ ScopedArtifact.CLASSES,
+ ProduceMappingFileTask::projectJars,
+ ProduceMappingFileTask::projectDirectories
+ )
+
+ val mergeTaskName = "merge${variant.name.capitalize()}ComposeMapping"
+ val mergeTaskProvider = project.tasks.register<MergeMappingFileTask>(mergeTaskName) {
+ composeMapping.set(taskProvider.map { it.output.get() })
+ }
+
+ variant.artifacts
+ .use(mergeTaskProvider)
+ .wiredWithFiles(MergeMappingFileTask::originalFile, MergeMappingFileTask::output)
+ .toTransform(uncheckedCast(SingleArtifact.OBFUSCATION_MAPPING_FILE))
+ }
+ }
+}
+
+
+@Suppress("UNCHECKED_CAST", "NOTHING_TO_INLINE")
+private inline fun <T> uncheckedCast(value: Any): T = value as T
+
+private fun String.capitalize(): String =
+ replaceFirstChar { if (it.isLowerCase()) it.titlecase(getDefault()) else it.toString() }
+
+@CacheableTask
+internal abstract class MergeMappingFileTask : DefaultTask() {
+
+ @get:InputFile
+ @get:PathSensitive(PathSensitivity.RELATIVE)
+ abstract val originalFile: RegularFileProperty
+
+ @get:InputFile
+ @get:PathSensitive(PathSensitivity.RELATIVE)
+ abstract val composeMapping: RegularFileProperty
+
+ @get:OutputFile
+ abstract val output: RegularFileProperty
+
+ @TaskAction
+ fun taskAction() {
+ // todo: fix previous mapping file hash
+ val outputFile = output.get().asFile
+ outputFile.parentFile.mkdirs()
+ outputFile.bufferedWriter().use { writer ->
+ originalFile.orNull?.let { writer.write(it.asFile.readText()) }
+ composeMapping.orNull?.let { writer.write(it.asFile.readText()) }
+ }
+ }
+}
+
+@CacheableTask
+internal abstract class ProduceMappingFileTask @Inject constructor(
+ private val problems: Problems
+) : DefaultTask() {
+ @get:OutputFile
+ abstract val output: RegularFileProperty
+
+ @get:InputFiles
+ @get:PathSensitive(PathSensitivity.RELATIVE)
+ abstract val projectDirectories: ListProperty<Directory>
+
+ @get:InputFiles
+ @get:PathSensitive(PathSensitivity.RELATIVE)
+ abstract val projectJars: ListProperty<RegularFile>
+
+ private val files by lazy {
+ services.get<FileOperations>()
+ }
+
+ @TaskAction
+ fun taskAction() {
+ val reporter = object : ErrorReporter {
+ override fun reportError(e: Throwable) {
+ problems.reporter.report(MappingGenerationFailedProblemId) { spec ->
+ spec.withException(e)
+ .severity(Severity.WARNING)
+ }
+ }
+ }
+
+ val mappings = buildList {
+ projectJars.get().forEach { jar ->
+ val contents = files.zipTree(jar)
+ contents.forEach { file ->
+ if (file.name.endsWith(".class")) {
+ val mapping = ComposeMapping.fromBytecode(reporter, file.readBytes())
+ add(mapping)
+ }
+ }
+ }
+
+ projectDirectories.get().forEach {
+ val contents = files.fileTree(it)
+ contents.forEach { file ->
+ if (file.name.endsWith(".class")) {
+ val mapping = ComposeMapping.fromBytecode(reporter, file.readBytes())
+ add(mapping)
+ }
+ }
+ }
+ }
+
+ output.get().asFile.bufferedWriter().use { writer ->
+ writer.write("ComposeStackTrace -> ${"$$"}compose:\n")
+ mappings.forEach {
+ writer.write(it.asProguardMapping())
+ }
+ }
+ }
+}
+
+private val Group = ProblemGroup.create("compose-mapping", "Compose Mapping Generator Group")
+
+private val MappingGenerationFailedProblemId = ProblemId.create(
+ "compose-mapping-fail",
+ "Failed to generate Compose mapping entry.",
+ Group
+)
diff --git a/plugins/compose/mapping-generator/src/main/kotlin/androidx/compose/compiler/mapping/ComposeMapping.kt b/plugins/compose/mapping-generator/src/main/kotlin/androidx/compose/compiler/mapping/ComposeMapping.kt
new file mode 100644
index 0000000..e5d8396
--- /dev/null
+++ b/plugins/compose/mapping-generator/src/main/kotlin/androidx/compose/compiler/mapping/ComposeMapping.kt
@@ -0,0 +1,115 @@
+/*
+ * Copyright 2010-2025 JetBrains s.r.o. and Kotlin Programming Language contributors.
+ * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
+ */
+
+package androidx.compose.compiler.mapping
+
+import androidx.compose.compiler.mapping.group.GroupInfo
+
+class ComposeMapping private constructor(
+ private val entries: List<Entry>
+) {
+ private class Entry(
+ val cls: ClassInfo,
+ val method: MethodInfo,
+ val group: GroupInfo
+ )
+
+ fun asProguardMapping(): String = buildString {
+ entries.forEach { entry ->
+ appendEntry(entry.cls, entry.method, entry.group)
+ appendLine()
+ }
+ }
+
+ private fun StringBuilder.appendEntry(
+ cls: ClassInfo,
+ method: MethodInfo,
+ group: GroupInfo
+ ) {
+ if (group.key == null) return
+
+ append(" ")
+ append("1:1:")
+ append(descriptorToProguardString("${cls.classId.fqName}.${method.id.methodName}", method.id.methodDescriptor))
+ append(":")
+ append(group.line)
+ append(":")
+ append(group.line)
+ append(" -> ")
+ append("m$")
+ append(group.key.toString())
+ }
+
+ private fun descriptorToProguardString(name: String, descriptor: String): String {
+ if (descriptor.isEmpty()) return "$name()"
+
+ fun descriptorToJavaType(d: String): String =
+ when (d) {
+ "V" -> "void"
+ "Z" -> "boolean"
+ "B" -> "byte"
+ "I" -> "int"
+ "J" -> "long"
+ "S" -> "short"
+ "F" -> "float"
+ "D" -> "double"
+ "C" -> "char"
+ else -> {
+ if (d.startsWith('L')) {
+ d.substring(1, d.length - 1).replace('/', '.')
+ } else if (d.startsWith('[')) {
+ descriptorToJavaType(d.drop(1)) + "[]"
+ } else {
+ error("Unknown descriptor $d")
+ }
+ }
+ }
+
+ val parameterString = descriptor.takeWhile { it != ')' }.dropWhile { it == '(' }
+ val parameters = sequence {
+ var i = 0
+ while (i < parameterString.length) {
+ val start = i
+ var current = parameterString[i]
+ while (current == '[') {
+ i++
+ current = parameterString[i]
+ }
+ val end = if (current == 'L') {
+ parameterString.indexOf(';', i) + 1
+ } else {
+ i + 1
+ }
+ yield(parameterString.substring(start, end))
+ i = end
+ }
+ }.map {
+ descriptorToJavaType(it)
+ }
+ val returnType = descriptor.takeLastWhile { it != ')' }
+
+ return parameters.joinToString(
+ separator = ",",
+ prefix = "${descriptorToJavaType(returnType)} $name(",
+ postfix = ")",
+ )
+ }
+
+ companion object {
+ fun fromBytecode(reporter: ErrorReporter, bytecode: ByteArray): ComposeMapping {
+ val cls = with(reporter) { ClassInfo(bytecode) }
+ val entries = buildList {
+ cls.methods.forEach { method ->
+ method.groups.forEach { group ->
+ if (group.key != null) {
+ add(Entry(cls, method, group))
+ }
+ }
+ }
+ }
+ return ComposeMapping(entries)
+ }
+ }
+}
\ No newline at end of file