From c3e98344b57c2f36ccae8414f21037e69890195f Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Thu, 22 Oct 2026 13:41:29 +0200 Subject: [PATCH 1/6] fix(build): Strip MethodParameters so AGP 8.0 and older can dex the SDK javac 21 writes nameless MethodParameters entries for synthetic and mandated parameters (JDK-8292275), which crash D8 older than 8.1 with a NullPointerException. Remove the attribute from the main Java classes of published modules after compiling, restoring the javac 17 output. Fixes #6355 Co-Authored-By: Claude Opus 5.5 --- build-logic/build.gradle.kts | 1 + .../io/sentry/gradle/JdkTargetConvention.kt | 7 +++ .../io/sentry/gradle/StripMethodParameters.kt | 57 +++++++++++++++++++ gradle/libs.versions.toml | 2 + 4 files changed, 67 insertions(+) create mode 100644 build-logic/src/main/kotlin/io/sentry/gradle/StripMethodParameters.kt diff --git a/build-logic/build.gradle.kts b/build-logic/build.gradle.kts index d02ab58488..60a340c9b8 100644 --- a/build-logic/build.gradle.kts +++ b/build-logic/build.gradle.kts @@ -5,6 +5,7 @@ plugins { dependencies { implementation(libs.animalsniffer.gradle.plugin) implementation(libs.spotlessLib) + implementation(libs.asm) compileOnly(libs.kotlin.gradle.plugin) compileOnly(libs.android.gradle.plugin) } diff --git a/build-logic/src/main/kotlin/io/sentry/gradle/JdkTargetConvention.kt b/build-logic/src/main/kotlin/io/sentry/gradle/JdkTargetConvention.kt index c92c83bbac..dea3a08b50 100644 --- a/build-logic/src/main/kotlin/io/sentry/gradle/JdkTargetConvention.kt +++ b/build-logic/src/main/kotlin/io/sentry/gradle/JdkTargetConvention.kt @@ -27,6 +27,7 @@ private val PUBLISHED_JAVA_VERSION = JavaVersion.VERSION_1_8 * - sample apps target the build JDK (21) * - unit test code will target the build JDK (21) (required for Mockito 5) * - published JVM modules main code pins the Java 1.8 release, for Java and for Kotlin + * - published modules main Java code drops the MethodParameters attribute javac 21 adds * - published Android modules main code targets Java 1.8 * - minSdk is set for Android libraries */ @@ -39,6 +40,12 @@ class JdkTargetConvention : Plugin { } project.plugins.withId("com.vanniktech.maven.publish") { + // Published Java classes must stay dexable by older D8 versions, see StripMethodParameters + project.tasks.withType(JavaCompile::class.java).configureEach { + if (!name.contains("Test")) { + doLast(StripMethodParameters()) + } + } // Only works for non-android modules // See https://issuetracker.google.com/issues/278800528 project.plugins.withId("java-library") { diff --git a/build-logic/src/main/kotlin/io/sentry/gradle/StripMethodParameters.kt b/build-logic/src/main/kotlin/io/sentry/gradle/StripMethodParameters.kt new file mode 100644 index 0000000000..2701baf1a3 --- /dev/null +++ b/build-logic/src/main/kotlin/io/sentry/gradle/StripMethodParameters.kt @@ -0,0 +1,57 @@ +package io.sentry.gradle + +import org.gradle.api.Action +import org.gradle.api.Task +import org.gradle.api.tasks.compile.JavaCompile +import org.objectweb.asm.ClassReader +import org.objectweb.asm.ClassVisitor +import org.objectweb.asm.ClassWriter +import org.objectweb.asm.MethodVisitor +import org.objectweb.asm.Opcodes + +/** + * Removes the MethodParameters attribute from the classes a JavaCompile task produced. + * + * Since JDK 21 (JDK-8292275) javac writes MethodParameters for every method with synthetic or + * mandated parameters, e.g. bridge methods, even without `-parameters`. Those entries carry flags + * but no name, and the D8 of AGP 7.x (used e.g. by React Native 0.71 apps) crashes on them with + * `NullPointerException: Cannot invoke "String.length()"` while dexing. Without `-parameters` this + * attribute holds nothing the SDK relies on, so dropping it restores the class files javac 17 + * produced. + */ +internal class StripMethodParameters : Action { + override fun execute(task: Task) { + val classesDir = (task as JavaCompile).destinationDirectory.get().asFile + classesDir + .walkTopDown() + .filter { it.isFile && it.extension == "class" } + .forEach { file -> + val reader = ClassReader(file.readBytes()) + val writer = ClassWriter(reader, 0) + val stripper = MethodParametersStripper(writer) + reader.accept(stripper, 0) + if (stripper.stripped) { + file.writeBytes(writer.toByteArray()) + } + } + } + + private class MethodParametersStripper(next: ClassVisitor) : ClassVisitor(Opcodes.ASM9, next) { + var stripped = false + + override fun visitMethod( + access: Int, + name: String?, + descriptor: String?, + signature: String?, + exceptions: Array?, + ): MethodVisitor? { + val next = super.visitMethod(access, name, descriptor, signature, exceptions) + return object : MethodVisitor(Opcodes.ASM9, next) { + override fun visitParameter(name: String?, access: Int) { + stripped = true + } + } + } + } +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index e5c2f9e217..30b60fde68 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -157,6 +157,8 @@ lottie-compose = { module = "com.airbnb.android:lottie-compose", version = "6.7. logback-classic = { module = "ch.qos.logback:logback-classic", version.ref = "logback" } kotlin-gradle-plugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" } android-gradle-plugin = { module = "com.android.tools.build:gradle", version.ref = "agp" } +# Matches the ASM that AGP brings, used by build-logic to post-process published class files +asm = { module = "org.ow2.asm:asm", version = "9.9" } nopen-annotations = { module = "com.jakewharton.nopen:nopen-annotations", version.ref = "nopen" } nopen-checker = { module = "com.jakewharton.nopen:nopen-checker", version.ref = "nopen" } nullaway = { module = "com.uber.nullaway:nullaway", version = "0.14.2" } From 79da542c1087effc059dfb25d38272219ce0cce0 Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Thu, 22 Oct 2026 13:41:55 +0200 Subject: [PATCH 2/6] changelog --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index cfffb41a11..ebe808c535 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## Unreleased + +### Fixes + +- Fix a D8 crash (`NullPointerException: Cannot invoke "String.length()"`) when dexing the SDK with AGP 8.0 and older, a regression from 8.61.1 ([#6356](https://github.com/getsentry/sentry-java/pull/6356)) + ## 8.61.1 ### Fixes From e5e54aee40a83fdfb810572caaf2cba6b2868c07 Mon Sep 17 00:00:00 2001 From: Markus Hintersteiner Date: Thu, 22 Oct 2026 16:26:51 +0200 Subject: [PATCH 3/6] fix(build): Strip MethodParameters from every module, not only published main code --- .../kotlin/io/sentry/gradle/JdkTargetConvention.kt | 13 ++++++------- .../io/sentry/gradle/StripMethodParameters.kt | 12 +++++++----- gradle/libs.versions.toml | 1 - 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/build-logic/src/main/kotlin/io/sentry/gradle/JdkTargetConvention.kt b/build-logic/src/main/kotlin/io/sentry/gradle/JdkTargetConvention.kt index dea3a08b50..67d37ce6f9 100644 --- a/build-logic/src/main/kotlin/io/sentry/gradle/JdkTargetConvention.kt +++ b/build-logic/src/main/kotlin/io/sentry/gradle/JdkTargetConvention.kt @@ -27,7 +27,7 @@ private val PUBLISHED_JAVA_VERSION = JavaVersion.VERSION_1_8 * - sample apps target the build JDK (21) * - unit test code will target the build JDK (21) (required for Mockito 5) * - published JVM modules main code pins the Java 1.8 release, for Java and for Kotlin - * - published modules main Java code drops the MethodParameters attribute javac 21 adds + * - every module's Java code drops the MethodParameters attribute javac 21 adds * - published Android modules main code targets Java 1.8 * - minSdk is set for Android libraries */ @@ -39,13 +39,12 @@ class JdkTargetConvention : Plugin { } } + // Java classes must stay dexable by older D8 versions, see StripMethodParameters + project.tasks.withType(JavaCompile::class.java).configureEach { + doLast(StripMethodParameters()) + } + project.plugins.withId("com.vanniktech.maven.publish") { - // Published Java classes must stay dexable by older D8 versions, see StripMethodParameters - project.tasks.withType(JavaCompile::class.java).configureEach { - if (!name.contains("Test")) { - doLast(StripMethodParameters()) - } - } // Only works for non-android modules // See https://issuetracker.google.com/issues/278800528 project.plugins.withId("java-library") { diff --git a/build-logic/src/main/kotlin/io/sentry/gradle/StripMethodParameters.kt b/build-logic/src/main/kotlin/io/sentry/gradle/StripMethodParameters.kt index 2701baf1a3..3a13fc3f54 100644 --- a/build-logic/src/main/kotlin/io/sentry/gradle/StripMethodParameters.kt +++ b/build-logic/src/main/kotlin/io/sentry/gradle/StripMethodParameters.kt @@ -10,14 +10,16 @@ import org.objectweb.asm.MethodVisitor import org.objectweb.asm.Opcodes /** - * Removes the MethodParameters attribute from the classes a JavaCompile task produced. + * Removes the + * [MethodParameters](https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html#jvms-4.7.24) + * attribute from the classes a JavaCompile task produced. * * Since JDK 21 (JDK-8292275) javac writes MethodParameters for every method with synthetic or * mandated parameters, e.g. bridge methods, even without `-parameters`. Those entries carry flags - * but no name, and the D8 of AGP 7.x (used e.g. by React Native 0.71 apps) crashes on them with - * `NullPointerException: Cannot invoke "String.length()"` while dexing. Without `-parameters` this - * attribute holds nothing the SDK relies on, so dropping it restores the class files javac 17 - * produced. + * but no name, and the D8 of AGP 8.0 and older fails the build while dexing. See the + * [R8 fix](https://r8.googlesource.com/r8/+/9da58a26e22f195d8187492fac350a90e3315e92) for more + * details. An alternative would be adding the `-parameters` compile flag, but this could lead to + * other binary incompatibility issues along the road. */ internal class StripMethodParameters : Action { override fun execute(task: Task) { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 30b60fde68..0e8b0c790d 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -157,7 +157,6 @@ lottie-compose = { module = "com.airbnb.android:lottie-compose", version = "6.7. logback-classic = { module = "ch.qos.logback:logback-classic", version.ref = "logback" } kotlin-gradle-plugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" } android-gradle-plugin = { module = "com.android.tools.build:gradle", version.ref = "agp" } -# Matches the ASM that AGP brings, used by build-logic to post-process published class files asm = { module = "org.ow2.asm:asm", version = "9.9" } nopen-annotations = { module = "com.jakewharton.nopen:nopen-annotations", version.ref = "nopen" } nopen-checker = { module = "com.jakewharton.nopen:nopen-checker", version.ref = "nopen" } From cc937fd7ad335995065ddbfe2cf284192b1c1d43 Mon Sep 17 00:00:00 2001 From: Markus Hintersteiner Date: Thu, 22 Oct 2026 16:54:28 +0200 Subject: [PATCH 4/6] fix(build): Strip MethodParameters only in published modules again --- .../kotlin/io/sentry/gradle/JdkTargetConvention.kt | 11 +++++------ .../kotlin/io/sentry/gradle/StripMethodParameters.kt | 4 ++++ 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/build-logic/src/main/kotlin/io/sentry/gradle/JdkTargetConvention.kt b/build-logic/src/main/kotlin/io/sentry/gradle/JdkTargetConvention.kt index 67d37ce6f9..38d186e66d 100644 --- a/build-logic/src/main/kotlin/io/sentry/gradle/JdkTargetConvention.kt +++ b/build-logic/src/main/kotlin/io/sentry/gradle/JdkTargetConvention.kt @@ -27,7 +27,7 @@ private val PUBLISHED_JAVA_VERSION = JavaVersion.VERSION_1_8 * - sample apps target the build JDK (21) * - unit test code will target the build JDK (21) (required for Mockito 5) * - published JVM modules main code pins the Java 1.8 release, for Java and for Kotlin - * - every module's Java code drops the MethodParameters attribute javac 21 adds + * - published modules Java code drops the MethodParameters attribute javac 21 adds * - published Android modules main code targets Java 1.8 * - minSdk is set for Android libraries */ @@ -39,12 +39,11 @@ class JdkTargetConvention : Plugin { } } - // Java classes must stay dexable by older D8 versions, see StripMethodParameters - project.tasks.withType(JavaCompile::class.java).configureEach { - doLast(StripMethodParameters()) - } - project.plugins.withId("com.vanniktech.maven.publish") { + // Published Java classes must stay dexable by older D8 versions, see StripMethodParameters + project.tasks.withType(JavaCompile::class.java).configureEach { + doLast(StripMethodParameters()) + } // Only works for non-android modules // See https://issuetracker.google.com/issues/278800528 project.plugins.withId("java-library") { diff --git a/build-logic/src/main/kotlin/io/sentry/gradle/StripMethodParameters.kt b/build-logic/src/main/kotlin/io/sentry/gradle/StripMethodParameters.kt index 3a13fc3f54..21b8fb4c89 100644 --- a/build-logic/src/main/kotlin/io/sentry/gradle/StripMethodParameters.kt +++ b/build-logic/src/main/kotlin/io/sentry/gradle/StripMethodParameters.kt @@ -20,6 +20,10 @@ import org.objectweb.asm.Opcodes * [R8 fix](https://r8.googlesource.com/r8/+/9da58a26e22f195d8187492fac350a90e3315e92) for more * details. An alternative would be adding the `-parameters` compile flag, but this could lead to * other binary incompatibility issues along the road. + * + * The action removes every MethodParameters entry, also the named ones that `-parameters` writes. + * Keep it away from code that compiles with `-parameters`, e.g. the Spring Boot samples, where + * Spring resolves bean and handler parameters by name at runtime. */ internal class StripMethodParameters : Action { override fun execute(task: Task) { From f8a5b2b19e141200a0db80e444d72b6f17f86ae5 Mon Sep 17 00:00:00 2001 From: Markus Hintersteiner Date: Fri, 23 Oct 2026 16:19:01 +0200 Subject: [PATCH 5/6] fix(build): Strip MethodParameters only for methods with an unnamed parameter --- .../io/sentry/gradle/StripMethodParameters.kt | 54 ++++++++++++++----- 1 file changed, 41 insertions(+), 13 deletions(-) diff --git a/build-logic/src/main/kotlin/io/sentry/gradle/StripMethodParameters.kt b/build-logic/src/main/kotlin/io/sentry/gradle/StripMethodParameters.kt index 21b8fb4c89..3ffc2d3999 100644 --- a/build-logic/src/main/kotlin/io/sentry/gradle/StripMethodParameters.kt +++ b/build-logic/src/main/kotlin/io/sentry/gradle/StripMethodParameters.kt @@ -12,7 +12,7 @@ import org.objectweb.asm.Opcodes /** * Removes the * [MethodParameters](https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html#jvms-4.7.24) - * attribute from the classes a JavaCompile task produced. + * attribute from the methods of a JavaCompile task that have a parameter without a name. * * Since JDK 21 (JDK-8292275) javac writes MethodParameters for every method with synthetic or * mandated parameters, e.g. bridge methods, even without `-parameters`. Those entries carry flags @@ -21,9 +21,10 @@ import org.objectweb.asm.Opcodes * details. An alternative would be adding the `-parameters` compile flag, but this could lead to * other binary incompatibility issues along the road. * - * The action removes every MethodParameters entry, also the named ones that `-parameters` writes. - * Keep it away from code that compiles with `-parameters`, e.g. the Spring Boot samples, where - * Spring resolves bean and handler parameters by name at runtime. + * `parameters_count` must match the parameters of the method descriptor, so a single entry cannot + * be removed. The attribute goes away for the whole method instead, as soon as one of its entries + * has no name. Methods whose entries all have a name, e.g. the canonical constructor of a record + * or anything compiled with `-parameters`, keep the attribute and their names. */ internal class StripMethodParameters : Action { override fun execute(task: Task) { @@ -33,17 +34,42 @@ internal class StripMethodParameters : Action { .filter { it.isFile && it.extension == "class" } .forEach { file -> val reader = ClassReader(file.readBytes()) - val writer = ClassWriter(reader, 0) - val stripper = MethodParametersStripper(writer) - reader.accept(stripper, 0) - if (stripper.stripped) { + val methods = methodsWithUnnamedParameters(reader) + if (methods.isNotEmpty()) { + val writer = ClassWriter(reader, 0) + reader.accept(MethodParametersStripper(writer, methods), 0) file.writeBytes(writer.toByteArray()) } } } - private class MethodParametersStripper(next: ClassVisitor) : ClassVisitor(Opcodes.ASM9, next) { - var stripped = false + /** Returns `name + descriptor` of every method that has a parameter entry without a name. */ + private fun methodsWithUnnamedParameters(reader: ClassReader): Set { + val methods = mutableSetOf() + val scanner = + object : ClassVisitor(Opcodes.ASM9) { + override fun visitMethod( + access: Int, + name: String?, + descriptor: String?, + signature: String?, + exceptions: Array?, + ): MethodVisitor = + object : MethodVisitor(Opcodes.ASM9) { + override fun visitParameter(parameterName: String?, access: Int) { + if (parameterName == null) { + methods.add(name + descriptor) + } + } + } + } + // no SKIP_DEBUG here, ASM counts MethodParameters as debug info and then skips it + reader.accept(scanner, ClassReader.SKIP_CODE) + return methods + } + + private class MethodParametersStripper(next: ClassVisitor, private val methods: Set) : + ClassVisitor(Opcodes.ASM9, next) { override fun visitMethod( access: Int, @@ -53,10 +79,12 @@ internal class StripMethodParameters : Action { exceptions: Array?, ): MethodVisitor? { val next = super.visitMethod(access, name, descriptor, signature, exceptions) + // every other method keeps the byte for byte copy ASM does for an untouched method + if (name + descriptor !in methods) { + return next + } return object : MethodVisitor(Opcodes.ASM9, next) { - override fun visitParameter(name: String?, access: Int) { - stripped = true - } + override fun visitParameter(name: String?, access: Int) = Unit } } } From 43330c2333244889d05ecb5460c6e8326e9c0b55 Mon Sep 17 00:00:00 2001 From: Markus Hintersteiner Date: Fri, 23 Oct 2026 16:38:18 +0200 Subject: [PATCH 6/6] build: Verify published jars and aars have no unnamed MethodParameters entries --- .../io/sentry/gradle/JdkTargetConvention.kt | 25 +++++- .../io/sentry/gradle/MethodParameters.kt | 35 ++++++++ .../io/sentry/gradle/StripMethodParameters.kt | 27 +------ .../sentry/gradle/VerifyMethodParameters.kt | 80 +++++++++++++++++++ 4 files changed, 140 insertions(+), 27 deletions(-) create mode 100644 build-logic/src/main/kotlin/io/sentry/gradle/MethodParameters.kt create mode 100644 build-logic/src/main/kotlin/io/sentry/gradle/VerifyMethodParameters.kt diff --git a/build-logic/src/main/kotlin/io/sentry/gradle/JdkTargetConvention.kt b/build-logic/src/main/kotlin/io/sentry/gradle/JdkTargetConvention.kt index 38d186e66d..ca2999f878 100644 --- a/build-logic/src/main/kotlin/io/sentry/gradle/JdkTargetConvention.kt +++ b/build-logic/src/main/kotlin/io/sentry/gradle/JdkTargetConvention.kt @@ -1,5 +1,7 @@ package io.sentry.gradle +import com.android.build.api.artifact.SingleArtifact +import com.android.build.api.variant.LibraryAndroidComponentsExtension import com.android.build.gradle.BaseExtension import org.gradle.api.JavaVersion import org.gradle.api.Plugin @@ -27,7 +29,8 @@ private val PUBLISHED_JAVA_VERSION = JavaVersion.VERSION_1_8 * - sample apps target the build JDK (21) * - unit test code will target the build JDK (21) (required for Mockito 5) * - published JVM modules main code pins the Java 1.8 release, for Java and for Kotlin - * - published modules Java code drops the MethodParameters attribute javac 21 adds + * - published modules Java code drops the MethodParameters attribute javac 21 adds, and their + * archives are checked for it * - published Android modules main code targets Java 1.8 * - minSdk is set for Android libraries */ @@ -44,6 +47,26 @@ class JdkTargetConvention : Plugin { project.tasks.withType(JavaCompile::class.java).configureEach { doLast(StripMethodParameters()) } + val verifyMethodParameters = + project.tasks.register("verifyMethodParameters", VerifyMethodParameters::class.java) { + description = "Checks the published archives for MethodParameters entries without a name" + group = "verification" + report.set(project.layout.buildDirectory.file("reports/method-parameters.txt")) + } + project.tasks.matching { it.name == "check" }.configureEach { + dependsOn(verifyMethodParameters) + } + project.plugins.withId("java-library") { + verifyMethodParameters.configure { archives.from(project.tasks.named("jar")) } + } + project.plugins.withId("com.android.library") { + project.extensions.getByType(LibraryAndroidComponentsExtension::class.java).onVariants { + variant -> + verifyMethodParameters.configure { + archives.from(variant.artifacts.get(SingleArtifact.AAR)) + } + } + } // Only works for non-android modules // See https://issuetracker.google.com/issues/278800528 project.plugins.withId("java-library") { diff --git a/build-logic/src/main/kotlin/io/sentry/gradle/MethodParameters.kt b/build-logic/src/main/kotlin/io/sentry/gradle/MethodParameters.kt new file mode 100644 index 0000000000..49662c59c7 --- /dev/null +++ b/build-logic/src/main/kotlin/io/sentry/gradle/MethodParameters.kt @@ -0,0 +1,35 @@ +package io.sentry.gradle + +import org.objectweb.asm.ClassReader +import org.objectweb.asm.ClassVisitor +import org.objectweb.asm.MethodVisitor +import org.objectweb.asm.Opcodes + +/** + * Returns `name + descriptor` of every method of the class that has a MethodParameters entry + * without a name. Only those entries make the D8 of AGP 8.0 and older fail, see + * [StripMethodParameters]. + */ +internal fun ClassReader.methodsWithUnnamedParameters(): Set { + val methods = mutableSetOf() + val scanner = + object : ClassVisitor(Opcodes.ASM9) { + override fun visitMethod( + access: Int, + name: String?, + descriptor: String?, + signature: String?, + exceptions: Array?, + ): MethodVisitor = + object : MethodVisitor(Opcodes.ASM9) { + override fun visitParameter(parameterName: String?, access: Int) { + if (parameterName == null) { + methods.add(name + descriptor) + } + } + } + } + // no SKIP_DEBUG here, ASM counts MethodParameters as debug info and then skips it + accept(scanner, ClassReader.SKIP_CODE) + return methods +} diff --git a/build-logic/src/main/kotlin/io/sentry/gradle/StripMethodParameters.kt b/build-logic/src/main/kotlin/io/sentry/gradle/StripMethodParameters.kt index 3ffc2d3999..94460086a9 100644 --- a/build-logic/src/main/kotlin/io/sentry/gradle/StripMethodParameters.kt +++ b/build-logic/src/main/kotlin/io/sentry/gradle/StripMethodParameters.kt @@ -34,7 +34,7 @@ internal class StripMethodParameters : Action { .filter { it.isFile && it.extension == "class" } .forEach { file -> val reader = ClassReader(file.readBytes()) - val methods = methodsWithUnnamedParameters(reader) + val methods = reader.methodsWithUnnamedParameters() if (methods.isNotEmpty()) { val writer = ClassWriter(reader, 0) reader.accept(MethodParametersStripper(writer, methods), 0) @@ -43,31 +43,6 @@ internal class StripMethodParameters : Action { } } - /** Returns `name + descriptor` of every method that has a parameter entry without a name. */ - private fun methodsWithUnnamedParameters(reader: ClassReader): Set { - val methods = mutableSetOf() - val scanner = - object : ClassVisitor(Opcodes.ASM9) { - override fun visitMethod( - access: Int, - name: String?, - descriptor: String?, - signature: String?, - exceptions: Array?, - ): MethodVisitor = - object : MethodVisitor(Opcodes.ASM9) { - override fun visitParameter(parameterName: String?, access: Int) { - if (parameterName == null) { - methods.add(name + descriptor) - } - } - } - } - // no SKIP_DEBUG here, ASM counts MethodParameters as debug info and then skips it - reader.accept(scanner, ClassReader.SKIP_CODE) - return methods - } - private class MethodParametersStripper(next: ClassVisitor, private val methods: Set) : ClassVisitor(Opcodes.ASM9, next) { diff --git a/build-logic/src/main/kotlin/io/sentry/gradle/VerifyMethodParameters.kt b/build-logic/src/main/kotlin/io/sentry/gradle/VerifyMethodParameters.kt new file mode 100644 index 0000000000..48ff0664e8 --- /dev/null +++ b/build-logic/src/main/kotlin/io/sentry/gradle/VerifyMethodParameters.kt @@ -0,0 +1,80 @@ +package io.sentry.gradle + +import java.io.ByteArrayInputStream +import java.util.zip.ZipInputStream +import org.gradle.api.DefaultTask +import org.gradle.api.GradleException +import org.gradle.api.file.ConfigurableFileCollection +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.tasks.CacheableTask +import org.gradle.api.tasks.InputFiles +import org.gradle.api.tasks.OutputFile +import org.gradle.api.tasks.PathSensitive +import org.gradle.api.tasks.PathSensitivity +import org.gradle.api.tasks.TaskAction +import org.objectweb.asm.ClassReader + +/** + * Fails the build if an archive still holds a MethodParameters entry without a name, the entries + * [StripMethodParameters] removes. It reads the final jar and aar of a module, so it also covers + * classes that no JavaCompile task of this build produced, e.g. repackaged ones. + */ +@CacheableTask +abstract class VerifyMethodParameters : DefaultTask() { + + /** The jar and aar files to read. An aar is opened down to its nested jars. */ + @get:InputFiles + @get:PathSensitive(PathSensitivity.RELATIVE) + abstract val archives: ConfigurableFileCollection + + @get:OutputFile abstract val report: RegularFileProperty + + @TaskAction + fun verify() { + val offenders = mutableListOf() + var classes = 0 + archives.files + .filter { it.isFile } + .forEach { archive -> + archive.inputStream().use { stream -> + classes += scan(ZipInputStream(stream), archive.name, offenders) + } + } + + val summary = + if (offenders.isEmpty()) "$classes classes checked, no unnamed MethodParameters entry" + else offenders.joinToString("\n") + val reportFile = report.get().asFile + reportFile.parentFile.mkdirs() + reportFile.writeText(summary + "\n") + + if (offenders.isNotEmpty()) { + throw GradleException( + "${offenders.size} method(s) keep a MethodParameters entry without a name, which makes " + + "the D8 of AGP 8.0 and older fail. See StripMethodParameters.\n$summary" + ) + } + logger.info("verifyMethodParameters: $summary") + } + + /** Adds `archive!/entry method` for every offending method and returns the classes read. */ + private fun scan(zip: ZipInputStream, path: String, offenders: MutableList): Int { + var classes = 0 + while (true) { + val entry = zip.nextEntry ?: break + val name = entry.name + when { + name.endsWith(".class") -> { + val reader = ClassReader(zip.readBytes()) + reader.methodsWithUnnamedParameters().forEach { offenders.add("$path!/$name $it") } + classes++ + } + name.endsWith(".jar") -> { + val nested = ZipInputStream(ByteArrayInputStream(zip.readBytes())) + classes += scan(nested, "$path!/$name", offenders) + } + } + } + return classes + } +}