diff --git a/CHANGELOG.md b/CHANGELOG.md index bf259062..d320e90b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ **Changed** - Replace `com.jakewharton.diffuse.io.Size` with `me.saket.bytesize.ByteSize` in the APIs. - Eliminate `data class` from public APIs. +- Prefer Class-File API on Java 24 or above. **Fixed** - Significantly improve `.jar` diff performance. diff --git a/build.gradle b/build.gradle index eb72ecf4..3cb195a9 100644 --- a/build.gradle +++ b/build.gradle @@ -35,18 +35,18 @@ subprojects { } } compilerOptions { - jvmTarget = JvmTarget.JVM_11 + jvmTarget = JvmTarget.fromTarget(libs.versions.jdkRelease.get()) freeCompilerArgs = [ "-progressive", '-opt-in=kotlin.contracts.ExperimentalContracts', - '-Xjdk-release=11', + "-Xjdk-release=${libs.versions.jdkRelease.get()}", ] } } } tasks.withType(JavaCompile).configureEach { - options.release = 11 + options.release = libs.versions.jdkRelease.get().toInteger() } configurations.configureEach { diff --git a/formats/api/formats.api b/formats/api/formats.api index 18eacb00..9206f4c4 100644 --- a/formats/api/formats.api +++ b/formats/api/formats.api @@ -225,7 +225,6 @@ public abstract interface class com/jakewharton/diffuse/format/BinaryFormat { public final class com/jakewharton/diffuse/format/Class { public static final field Companion Lcom/jakewharton/diffuse/format/Class$Companion; - public synthetic fun (Ljava/lang/String;ILjava/util/List;Ljava/util/List;Lkotlin/jvm/internal/DefaultConstructorMarker;)V public fun equals (Ljava/lang/Object;)Z public final fun getBytecodeVersion ()I public final fun getDeclaredMembers ()Ljava/util/List; diff --git a/formats/build.gradle b/formats/build.gradle index ef6a0f7c..0241f7e9 100644 --- a/formats/build.gradle +++ b/formats/build.gradle @@ -1,8 +1,13 @@ +import org.jetbrains.kotlin.gradle.dsl.JvmTarget +import org.jetbrains.kotlin.gradle.tasks.KotlinJvmCompile + apply plugin: 'org.jetbrains.kotlin.jvm' apply plugin: 'com.vanniktech.maven.publish' apply plugin: 'org.jetbrains.dokka' apply plugin: "dev.drewhamilton.poko" +addMultiReleaseSourceSet(24) + dependencies { api projects.io @@ -22,3 +27,50 @@ dependencies { testImplementation libs.assertk testImplementation projects.testHelpers } + +tasks.named('jar', Jar) { + manifest { + attributes 'Multi-Release': 'true' + } +} + +def addMultiReleaseSourceSet(int version) { + kotlin.target.compilations.create("java${version}") { + // Import main and its classpath as dependencies and establish internal visibility. + associateWith(kotlin.target.compilations.main) + + compileJavaTaskProvider.configure { JavaCompile task -> + task.options.release = version + } + compileTaskProvider.configure { KotlinJvmCompile task -> + task.compilerOptions { + jvmTarget = JvmTarget.fromTarget(version.toString()) + freeCompilerArgs = [ + "-Xjdk-release=$version", + ] + } + } + + tasks.named('jar', Jar) { jar -> + jar.from(output.allOutputs) { + into("META-INF/versions/$version") + } + } + } + + def versionedTest = tasks.register("testJava${version}", Test) { task -> + task.group = LifecycleBasePlugin.VERIFICATION_GROUP + task.description = "Runs test suite using Java ${version} toolchain." + task.testClassesDirs = sourceSets.test.output.classesDirs + def testCpWithoutMainOutput = sourceSets.test.runtimeClasspath - sourceSets.main.output + // Prefer MR classes on the classpath, so remove main output from the test runtime classpath. + task.classpath = files(tasks.named('jar', Jar).flatMap { it.archiveFile }) + testCpWithoutMainOutput + task.javaLauncher = javaToolchains.launcherFor { + languageVersion = JavaLanguageVersion.of(version) + vendor = JvmVendorSpec.AZUL + } + } + tasks.named('check') { + dependsOn(versionedTest) + } +} diff --git a/formats/gradle.properties b/formats/gradle.properties index 55a42657..91409d28 100644 --- a/formats/gradle.properties +++ b/formats/gradle.properties @@ -1,3 +1,8 @@ POM_ARTIFACT_ID=formats POM_NAME=Diffuse format parsing POM_PACKAGING=jar + +# Keep associated Kotlin compilations from depending on archive tasks (e.g., jar), which can create circular task graphs in multi-release setups. +# https://kotlinlang.org/docs/gradle-configure-project.html#disable-use-of-artifact-in-compilation-task +# https://kotlinlang.org/docs/whatsnew2020.html#added-task-dependency-for-rare-cases-when-the-compile-task-lacks-one-on-an-artifact +kotlin.build.archivesTaskOutputAsFriendModule=false diff --git a/formats/src/java24/kotlin/com/jakewharton/diffuse/format/Class.kt b/formats/src/java24/kotlin/com/jakewharton/diffuse/format/Class.kt new file mode 100644 index 00000000..8d1bc47b --- /dev/null +++ b/formats/src/java24/kotlin/com/jakewharton/diffuse/format/Class.kt @@ -0,0 +1,103 @@ +package com.jakewharton.diffuse.format + +import com.jakewharton.diffuse.io.Input +import java.lang.classfile.ClassFile +import java.lang.classfile.ClassModel +import java.lang.classfile.constantpool.MethodHandleEntry +import java.lang.classfile.instruction.FieldInstruction +import java.lang.classfile.instruction.InvokeDynamicInstruction +import java.lang.classfile.instruction.InvokeInstruction +import kotlin.jvm.optionals.getOrNull + +@Suppress("unused") // Used by Multi-Release JARs for Java 24+. +internal fun Input.toClassImpl(): Class { + val classModel = ClassFile.of().parse(toByteArray()) + val type = TypeDescriptor("L${classModel.thisClass().asInternalName()};") + val (declaredMembers, referencedMembers) = classModel.parseMembers(type) + return Class( + descriptor = type, + bytecodeVersion = classModel.majorVersion(), + declaredMembers = declaredMembers.sorted(), + referencedMembers = referencedMembers.sorted(), + ) +} + +private fun ClassModel.parseMembers(type: TypeDescriptor): Pair, Set> { + val declaredMembers = mutableListOf() + val referencedMembers = mutableSetOf() + + for (field in fields()) { + declaredMembers += + Field( + type, + field.fieldName().stringValue(), + TypeDescriptor(field.fieldTypeSymbol().descriptorString()), + ) + } + + for (method in methods()) { + declaredMembers += + parseMethod( + type, + method.methodName().stringValue(), + method.methodTypeSymbol().descriptorString(), + ) + + method.code().getOrNull()?.let { codeModel -> + for (instruction in codeModel) { + when (instruction) { + is FieldInstruction -> { + val ownerType = parseOwner(instruction.owner().name().stringValue()) + val name = instruction.name().stringValue() + val descriptor = instruction.type().stringValue() + referencedMembers += Field(ownerType, name, TypeDescriptor(descriptor)) + } + is InvokeInstruction -> { + val ownerType = parseOwner(instruction.owner().name().stringValue()) + val name = instruction.name().stringValue() + val descriptor = instruction.type().stringValue() + referencedMembers += parseMethod(ownerType, name, descriptor) + } + is InvokeDynamicInstruction -> { + val bootstrapMethodEntry = instruction.invokedynamic().bootstrap() + referencedMembers += parseHandle(bootstrapMethodEntry.bootstrapMethod()) + + if ( + bootstrapMethodEntry.bootstrapMethod().reference().owner().name().stringValue() == + "java/lang/invoke/LambdaMetafactory" && + bootstrapMethodEntry.bootstrapMethod().reference().name().stringValue() == + "metafactory" + ) { + // LambdaMetaFactory.metafactory accepts 6 arguments. The first 3 are + // provided automatically and the latter 3 are supplied as the arguments to + // this method. The second of those is a MethodHandle to the lambda + // implementation which needs to be counted as a method reference. + val implementationHandle = bootstrapMethodEntry.arguments()[1] as MethodHandleEntry + referencedMembers += parseHandle(implementationHandle) + } + } + else -> Unit + } + } + } + } + + return declaredMembers to referencedMembers +} + +private fun parseHandle(handle: MethodHandleEntry): Member { + val ref = handle.reference() + val handlerOwner = parseOwner(ref.owner().name().stringValue()) + val handlerName = ref.name().stringValue() + val handlerDescriptor = ref.type().stringValue() + return if (handlerDescriptor.startsWith('(')) { + parseMethod(handlerOwner, handlerName, handlerDescriptor) + } else { + Field(handlerOwner, handlerName, TypeDescriptor(handlerDescriptor)) + } +} + +private fun parseOwner(owner: String): TypeDescriptor { + val ownerDescriptor = if (owner.startsWith('[')) owner else "L$owner;" + return TypeDescriptor(ownerDescriptor) +} diff --git a/formats/src/main/kotlin/com/jakewharton/diffuse/format/Class.kt b/formats/src/main/kotlin/com/jakewharton/diffuse/format/Class.kt index c45ff9b1..3d94b73f 100644 --- a/formats/src/main/kotlin/com/jakewharton/diffuse/format/Class.kt +++ b/formats/src/main/kotlin/com/jakewharton/diffuse/format/Class.kt @@ -10,7 +10,7 @@ import org.objectweb.asm.MethodVisitor import org.objectweb.asm.Opcodes class Class -private constructor( +internal constructor( val descriptor: TypeDescriptor, val bytecodeVersion: Int, val declaredMembers: List, @@ -29,26 +29,26 @@ private constructor( referencedMembers == other.referencedMembers companion object { - @JvmStatic - @JvmName("parse") - fun Input.toClass(): Class { - val reader = ClassReader(toByteArray()) - val type = TypeDescriptor("L${reader.className};") - - val referencedVisitor = ReferencedMembersVisitor() - val declaredVisitor = DeclaredMembersVisitor(type, referencedVisitor) - reader.accept(declaredVisitor, 0) - - return Class( - type, - declaredVisitor.version, - declaredVisitor.members.sorted(), - referencedVisitor.members.sorted(), - ) - } + @JvmStatic @JvmName("parse") fun Input.toClass(): Class = toClassImpl() } } +internal fun Input.toClassImpl(): Class { + val reader = ClassReader(toByteArray()) + val type = TypeDescriptor("L${reader.className};") + + val referencedVisitor = ReferencedMembersVisitor() + val declaredVisitor = DeclaredMembersVisitor(type, referencedVisitor) + reader.accept(declaredVisitor, 0) + + return Class( + descriptor = type, + bytecodeVersion = declaredVisitor.version, + declaredMembers = declaredVisitor.members.sorted(), + referencedMembers = referencedVisitor.members.sorted(), + ) +} + private class DeclaredMembersVisitor(val type: TypeDescriptor, val methodVisitor: MethodVisitor) : ClassVisitor(Opcodes.ASM9) { var version: Int = 0 @@ -150,31 +150,6 @@ private class ReferencedMembersVisitor : MethodVisitor(Opcodes.ASM9) { } } -private fun parseMethod(owner: TypeDescriptor, name: String, descriptor: String): Method { - val parameterTypes = mutableListOf() - var i = 1 - while (true) { - if (descriptor[i] == ')') { - break - } - var typeIndex = i - while (descriptor[typeIndex] == '[') { - typeIndex++ - } - val end = - if (descriptor[typeIndex] == 'L') { - descriptor.indexOf(';', startIndex = typeIndex) - } else { - typeIndex - } - val parameterDescriptor = descriptor.substring(i, end + 1) - parameterTypes += TypeDescriptor(parameterDescriptor) - i += parameterDescriptor.length - } - val returnType = TypeDescriptor(descriptor.substring(i + 1)) - return Method(owner, name, parameterTypes, returnType) -} - private val lambdaMetaFactory = Handle( Opcodes.H_INVOKESTATIC, diff --git a/formats/src/main/kotlin/com/jakewharton/diffuse/format/util.kt b/formats/src/main/kotlin/com/jakewharton/diffuse/format/util.kt index ca622f77..01cc808d 100644 --- a/formats/src/main/kotlin/com/jakewharton/diffuse/format/util.kt +++ b/formats/src/main/kotlin/com/jakewharton/diffuse/format/util.kt @@ -4,3 +4,32 @@ import com.google.devrel.gmscore.tools.apk.arsc.ResourceFile import com.jakewharton.diffuse.io.Input internal fun Input.toResourceFile() = ResourceFile(toByteArray()) + +/** + * TODO: private this into `Class.kt` once we bump to Java 24+ and can use the new class file + * parser. + */ +internal fun parseMethod(owner: TypeDescriptor, name: String, descriptor: String): Method { + val parameterTypes = mutableListOf() + var i = 1 + while (true) { + if (descriptor[i] == ')') { + break + } + var typeIndex = i + while (descriptor[typeIndex] == '[') { + typeIndex++ + } + val end = + if (descriptor[typeIndex] == 'L') { + descriptor.indexOf(';', startIndex = typeIndex) + } else { + typeIndex + } + val parameterDescriptor = descriptor.substring(i, end + 1) + parameterTypes += TypeDescriptor(parameterDescriptor) + i += parameterDescriptor.length + } + val returnType = TypeDescriptor(descriptor.substring(i + 1)) + return Method(owner, name, parameterTypes, returnType) +} diff --git a/formats/src/test/kotlin/com/jakewharton/diffuse/format/ClassTest.kt b/formats/src/test/kotlin/com/jakewharton/diffuse/format/ClassTest.kt index 91ba7229..e80b1069 100644 --- a/formats/src/test/kotlin/com/jakewharton/diffuse/format/ClassTest.kt +++ b/formats/src/test/kotlin/com/jakewharton/diffuse/format/ClassTest.kt @@ -16,6 +16,7 @@ class ClassTest { val type = TypeDescriptor($$"Lcom/jakewharton/diffuse/format/ClassTest$Dummy;") assertThat(clazz.descriptor).isEqualTo(type) + assertThat(clazz.bytecodeVersion).isEqualTo(55) // Reflects the JVM target 11. val initMethod = Method(type, "", emptyList(), TypeDescriptor("V")) val stringArrayDescriptor = TypeDescriptor("[Ljava/lang/String;") diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 51ba0351..7e7551a8 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -3,6 +3,7 @@ aapt2Proto = "9.1.0-14792394" protobufJava = "4.34.0" guava = "30.1-jre" +jdkRelease = "11" [libraries] dalvikDx = "com.jakewharton.android.repackaged:dalvik-dx:16.0.1" diff --git a/settings.gradle b/settings.gradle index b69be549..04e5da7f 100644 --- a/settings.gradle +++ b/settings.gradle @@ -6,6 +6,11 @@ pluginManagement { } } } + +plugins { + id 'org.gradle.toolchains.foojay-resolver-convention' version '1.0.0' +} + dependencyResolutionManagement { repositories { mavenCentral()