diff --git a/CHANGELOG.md b/CHANGELOG.md index 5ee40346..848b8fcc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,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 d4b0aacd..c6f4fe5f 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;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 ef6a0f7c..39476bef 100644 --- a/formats/build.gradle +++ b/formats/build.gradle @@ -1,8 +1,19 @@ +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" +// 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 @@ -22,3 +33,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/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..21b28593 --- /dev/null +++ b/formats/src/java24/kotlin/com/jakewharton/diffuse/format/Class.kt @@ -0,0 +1,98 @@ +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) +} 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..6c3f3bc5 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) : @@ -129,31 +129,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/gradle/libs.versions.toml b/gradle/libs.versions.toml index 332b8185..af7ef84b 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()