diff --git a/CHANGELOG.md b/CHANGELOG.md index b260f267..61379804 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,9 @@ **Added** - Add `--summary-only` flag. +**Changed** +- Prefer Class-File API on Java 24 or above. + **Fixed** - Significantly improve `.jar` diff performance. diff --git a/build.gradle b/build.gradle index e9799b9f..71e00dc4 100644 --- a/build.gradle +++ b/build.gradle @@ -34,18 +34,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 9d248958..2708ed73 100644 --- a/formats/api/formats.api +++ b/formats/api/formats.api @@ -234,7 +234,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;Ljava/util/List;Ljava/util/List;Lkotlin/jvm/internal/DefaultConstructorMarker;)V public fun equals (Ljava/lang/Object;)Z public final fun getDeclaredMembers ()Ljava/util/List; public final fun getDescriptor-BeHrSHk ()Ljava/lang/String; diff --git a/formats/build.gradle b/formats/build.gradle index 4e351213..08f6f7ed 100644 --- a/formats/build.gradle +++ b/formats/build.gradle @@ -1,7 +1,18 @@ +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' +// 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 +ext['kotlin.build.archivesTaskOutputAsFriendModule'] = false + +addMultiReleaseSourceSet(24) + dependencies { api projects.io @@ -21,3 +32,48 @@ 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 + task.classpath = sourceSets.test.runtimeClasspath + task.javaLauncher = javaToolchains.launcherFor { + languageVersion = JavaLanguageVersion.of(version) + vendor = JvmVendorSpec.AZUL + } + } + tasks.named('check') { + dependsOn(versionedTest) + } +} 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..c1a5933f --- /dev/null +++ b/formats/src/java24/kotlin/com/jakewharton/diffuse/format/Class.kt @@ -0,0 +1,124 @@ +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(type, declaredMembers.sorted(), 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) +} + +@Suppress("DuplicatedCode") // Reuse this function by internal will cause NoSuchMethodError. +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) +} 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 df6a67d1..71dde6f2 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 declaredMembers: List, val referencedMembers: List, @@ -26,19 +26,19 @@ private constructor( referencedMembers == other.referencedMembers companion object { - @JvmStatic - @JvmName("parse") - fun Input.toClass(): Class { - val reader = ClassReader(toByteArray()) - val type = TypeDescriptor("L${reader.className};") + @JvmStatic @JvmName("parse") fun Input.toClass(): Class = toClassImpl() + } +} - val referencedVisitor = ReferencedMembersVisitor() - val declaredVisitor = DeclaredMembersVisitor(type, referencedVisitor) - reader.accept(declaredVisitor, 0) +internal fun Input.toClassImpl(): Class { + val reader = ClassReader(toByteArray()) + val type = TypeDescriptor("L${reader.className};") - return Class(type, declaredVisitor.members.sorted(), referencedVisitor.members.sorted()) - } - } + val referencedVisitor = ReferencedMembersVisitor() + val declaredVisitor = DeclaredMembersVisitor(type, referencedVisitor) + reader.accept(declaredVisitor, 0) + + return Class(type, declaredVisitor.members.sorted(), referencedVisitor.members.sorted()) } private class DeclaredMembersVisitor(val type: TypeDescriptor, val methodVisitor: MethodVisitor) : diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 7a588809..688a1774 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()