diff --git a/CHANGELOG.md b/CHANGELOG.md index 465da8d957..097d29cda6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ ### 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)) - Always remove the first-draw listener after the first frame, even if no layout pass follows it ([#6152](https://github.com/getsentry/sentry-java/pull/6152)) ## 8.61.1 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..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,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 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,10 @@ 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 { + 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..21b8fb4c89 --- /dev/null +++ b/build-logic/src/main/kotlin/io/sentry/gradle/StripMethodParameters.kt @@ -0,0 +1,63 @@ +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](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 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. + * + * 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) { + 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..0e8b0c790d 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -157,6 +157,7 @@ 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" } +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" }