Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
6 changes: 3 additions & 3 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
1 change: 0 additions & 1 deletion formats/api/formats.api
Original file line number Diff line number Diff line change
Expand Up @@ -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 <init> (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;
Expand Down
56 changes: 56 additions & 0 deletions formats/build.gradle
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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)
}
}
124 changes: 124 additions & 0 deletions formats/src/java24/kotlin/com/jakewharton/diffuse/format/Class.kt
Original file line number Diff line number Diff line change
@@ -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<List<Member>, Set<Member>> {
val declaredMembers = mutableListOf<Member>()
val referencedMembers = mutableSetOf<Member>()

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<TypeDescriptor>()
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)
}
24 changes: 12 additions & 12 deletions formats/src/main/kotlin/com/jakewharton/diffuse/format/Class.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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<Member>,
val referencedMembers: List<Member>,
Expand All @@ -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) :
Expand Down
1 change: 1 addition & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
5 changes: 5 additions & 0 deletions settings.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@ pluginManagement {
}
}
}

plugins {
id 'org.gradle.toolchains.foojay-resolver-convention' version '1.0.0'
}

dependencyResolutionManagement {
repositories {
mavenCentral()
Expand Down