From a453cc833f52cf1ccd52288edb73c6f0fbc0c7cc Mon Sep 17 00:00:00 2001 From: Linnea Gräf Date: Thu, 14 Dec 2023 18:11:17 +0100 Subject: Add support for jar merging --- archenemyexample/build.gradle.kts | 18 +- archenemyexample/src/fabricMain/kotlin/doStuff.kt | 2 + plugin/build.gradle.kts | 6 + .../mojang/AbstractTransformerRepository.kt | 69 +++ .../archenemy/mojang/ArchenemyMojangExtension.kt | 23 +- .../kotlin/moe/nea/archenemy/mojang/CHashable.kt | 7 + .../kotlin/moe/nea/archenemy/mojang/ClassMerger.kt | 508 +++++++++++++++++++++ .../archenemy/mojang/MergedRepositoryProvider.kt | 98 ++++ .../kotlin/moe/nea/archenemy/util/DigestUtils.kt | 8 + .../main/kotlin/moe/nea/archenemy/util/EnvType.kt | 9 + .../main/kotlin/moe/nea/archenemy/util/FSUtil.kt | 33 ++ 11 files changed, 776 insertions(+), 5 deletions(-) create mode 100644 plugin/src/main/kotlin/moe/nea/archenemy/mojang/AbstractTransformerRepository.kt create mode 100644 plugin/src/main/kotlin/moe/nea/archenemy/mojang/CHashable.kt create mode 100644 plugin/src/main/kotlin/moe/nea/archenemy/mojang/ClassMerger.kt create mode 100644 plugin/src/main/kotlin/moe/nea/archenemy/mojang/MergedRepositoryProvider.kt create mode 100644 plugin/src/main/kotlin/moe/nea/archenemy/util/EnvType.kt create mode 100644 plugin/src/main/kotlin/moe/nea/archenemy/util/FSUtil.kt diff --git a/archenemyexample/build.gradle.kts b/archenemyexample/build.gradle.kts index fae9a49..7ae9646 100644 --- a/archenemyexample/build.gradle.kts +++ b/archenemyexample/build.gradle.kts @@ -9,7 +9,8 @@ repositories { mavenCentral() maven("https://maven.fabricmc.net") } -val minecraft = mojang.minecraft("1.20.2", MCSide.CLIENT) as ModuleDependency +val minecraftClient = mojang.minecraft("1.20.2", MCSide.CLIENT) as ModuleDependency +val minecraftServer = mojang.minecraft("1.20.2", MCSide.CLIENT) as ModuleDependency val officialMappings = mojang.officialMappings( "1.20.2", MCSide.CLIENT ) @@ -30,7 +31,7 @@ kotlin { this.dependencies { implementation( mojang.mapJar( - minecraft, + minecraftClient, officialMappings, "official", "named" @@ -44,12 +45,21 @@ kotlin { compilations.named("main").get().run { defaultSourceSet.dependsOn(allJvm) this.dependencies { - val thingy = mojang.mapJar( - minecraft, + val intermediaryClient = mojang.mapJar( + minecraftClient, intermediaryMappings, "official", "intermediary" ) + val intermediaryServer = mojang.mapJar( + minecraftServer, + intermediaryMappings, + "official", + "intermediary" + ) + val thingy = mojang.mergeJar( + intermediaryClient, intermediaryServer + ) implementation( mojang.mapJar( thingy as ModuleDependency, diff --git a/archenemyexample/src/fabricMain/kotlin/doStuff.kt b/archenemyexample/src/fabricMain/kotlin/doStuff.kt index d88a17e..fc163c2 100644 --- a/archenemyexample/src/fabricMain/kotlin/doStuff.kt +++ b/archenemyexample/src/fabricMain/kotlin/doStuff.kt @@ -1,5 +1,7 @@ import net.minecraft.client.MinecraftClient +import net.minecraft.server.dedicated.DedicatedServer actual fun doStuff(args: Int) { val client = MinecraftClient.getInstance() + val dedicated: DedicatedServer = TODO() } \ No newline at end of file diff --git a/plugin/build.gradle.kts b/plugin/build.gradle.kts index f3d7c28..35f1832 100644 --- a/plugin/build.gradle.kts +++ b/plugin/build.gradle.kts @@ -18,6 +18,12 @@ dependencies { implementation("net.fabricmc:mapping-io:0.1.8") implementation("net.fabricmc:tiny-remapper:0.8.6") + implementation("org.ow2.asm:asm:9.5") + implementation("org.ow2.asm:asm-commons:9.5") + implementation("org.ow2.asm:asm-tree:9.5") + implementation("org.ow2.asm:asm-analysis:9.5") + implementation("org.ow2.asm:asm-util:9.5") + testImplementation("org.jetbrains.kotlin:kotlin-test") testImplementation("org.jetbrains.kotlin:kotlin-test-junit") } diff --git a/plugin/src/main/kotlin/moe/nea/archenemy/mojang/AbstractTransformerRepository.kt b/plugin/src/main/kotlin/moe/nea/archenemy/mojang/AbstractTransformerRepository.kt new file mode 100644 index 0000000..601cb19 --- /dev/null +++ b/plugin/src/main/kotlin/moe/nea/archenemy/mojang/AbstractTransformerRepository.kt @@ -0,0 +1,69 @@ +package moe.nea.archenemy.mojang + +import moe.nea.archenemy.util.DownloadUtils +import moe.nea.archenemy.util.getNullsafeIdentifier +import net.minecraftforge.artifactural.api.artifact.Artifact +import net.minecraftforge.artifactural.api.artifact.ArtifactIdentifier +import net.minecraftforge.artifactural.api.artifact.Streamable +import net.minecraftforge.artifactural.api.repository.Repository +import org.gradle.api.artifacts.Dependency +import java.io.File +import java.security.MessageDigest + +abstract class AbstractTransformerRepository( + val repoIdentifier: String, + val extension: ArchenemyMojangExtension, +) : Repository { + init { + require(!repoIdentifier.contains(".")) + } + + data class HashContainer(val t: T) { + val transformerHash = kotlin.run { + val digest = MessageDigest.getInstance("SHA-256") + t.updateHash(digest) + DownloadUtils.bytesToHex(digest.digest()) + } + } + + fun resolveDirect(vararg dependency: Dependency): Set { + return extension.project.configurations.detachedConfiguration(*dependency).also { it.isTransitive = false } + .resolve() + } + + fun lazyTransform(file: File, function: (file: File) -> Unit): Streamable { + return Streamable { + if (!file.exists()) function(file) + file.inputStream() + } + } + + private val providers: MutableMap> = mutableMapOf() + val baseDirectory = extension.getLocalCacheDirectory().resolve("precache-$repoIdentifier") + fun getCoordinate(t: T): String { + val container = HashContainer(t) + providers[container.transformerHash] = container + val c = getClassifier(t) + return "archenemy.$repoIdentifier.${container.transformerHash}:${getName(t)}:${getVersion(t)}" + if (c != null) ":$c" else "" + } + + protected abstract fun getName(t: T): String + protected abstract fun getVersion(t: T): String + protected open fun getClassifier(t: T): String? = null + override fun getArtifact(identifier: ArtifactIdentifier?): Artifact { + if (identifier == null) return Artifact.none() + if (!identifier.group.startsWith("archenemy.${this.repoIdentifier}.")) return Artifact.none() + val hash = identifier.group.split(".")[2] + val provider = providers[hash] ?: error("Unregistered archenemy ${this.repoIdentifier} dependeny") + val coordinate = getCoordinate(provider.t) + require(coordinate.startsWith(identifier.group + ":" + identifier.name + ":" + identifier.version)) + if (identifier.extension == "pom") return Artifact.none() + return getArtifact( + getNullsafeIdentifier(identifier), + provider.t, + baseDirectory.resolve("$hash-${identifier.classifier}.${identifier.extension}") + ) ?: Artifact.none() + } + + protected abstract fun getArtifact(identifier: ArtifactIdentifier, value: T, file: File): Artifact? +} \ No newline at end of file diff --git a/plugin/src/main/kotlin/moe/nea/archenemy/mojang/ArchenemyMojangExtension.kt b/plugin/src/main/kotlin/moe/nea/archenemy/mojang/ArchenemyMojangExtension.kt index 8125900..e1a8bfe 100644 --- a/plugin/src/main/kotlin/moe/nea/archenemy/mojang/ArchenemyMojangExtension.kt +++ b/plugin/src/main/kotlin/moe/nea/archenemy/mojang/ArchenemyMojangExtension.kt @@ -21,9 +21,15 @@ abstract class ArchenemyMojangExtension(val project: Project) { GradleRepositoryAdapter.add( project.repositories, "Minecraft Mapped Provider", - getLocalCacheDirectory().resolve("minecraft-transformation-provider"), + getLocalCacheDirectory().resolve("minecraft-mapped-provider"), mappedRepositoryProvider ) + GradleRepositoryAdapter.add( + project.repositories, + "Minecraft Merged Provider", + getLocalCacheDirectory().resolve("minecraft-merged-provider"), + mergedRepositoryProvider + ) project.repositories.maven { it.name = "Minecraft Libraries" it.url = URI("https://libraries.minecraft.net/") @@ -31,6 +37,7 @@ abstract class ArchenemyMojangExtension(val project: Project) { } private val mappedRepositoryProvider = MappedRepositoryProvider(this) + private val mergedRepositoryProvider = MergedRepositoryProvider(this) fun yarnMappings(dependency: Dependency): MappingDependency { dependency as ModuleDependency @@ -56,6 +63,20 @@ abstract class ArchenemyMojangExtension(val project: Project) { return yarnMappings(project.dependencies.create("net.fabricmc:intermediary:$version:v2")) } + fun mergeJar( + base: Dependency, + overlay: Dependency, + ): Dependency { + base as ModuleDependency + overlay as ModuleDependency + _registerMinecraftProvider + return project.dependencies.create( + mergedRepositoryProvider.getCoordinate( + MergedRepositoryProvider.Coordinate(base, overlay) + ) + ) + } + fun mapJar( dependency: Dependency, mappings: MappingDependency, diff --git a/plugin/src/main/kotlin/moe/nea/archenemy/mojang/CHashable.kt b/plugin/src/main/kotlin/moe/nea/archenemy/mojang/CHashable.kt new file mode 100644 index 0000000..bfac9c7 --- /dev/null +++ b/plugin/src/main/kotlin/moe/nea/archenemy/mojang/CHashable.kt @@ -0,0 +1,7 @@ +package moe.nea.archenemy.mojang + +import java.security.MessageDigest + +interface CHashable { + fun updateHash(digest: MessageDigest) +} diff --git a/plugin/src/main/kotlin/moe/nea/archenemy/mojang/ClassMerger.kt b/plugin/src/main/kotlin/moe/nea/archenemy/mojang/ClassMerger.kt new file mode 100644 index 0000000..655cf85 --- /dev/null +++ b/plugin/src/main/kotlin/moe/nea/archenemy/mojang/ClassMerger.kt @@ -0,0 +1,508 @@ +package moe.nea.archenemy.mojang + +import moe.nea.archenemy.util.EnvType +import org.objectweb.asm.Attribute +import org.objectweb.asm.Opcodes +import org.objectweb.asm.tree.* + +// One to one copied from unimined. Definitely needs improvement to be better suited to my needs, but for now this will do and is much better than writing a barebones thing myself. +class ClassMerger( + val toMergedClass: (ClassNode, EnvType) -> Unit = { _, _ -> }, + val toMergedFields: (FieldNode, EnvType) -> Unit = { _, _ -> }, + val toMergedMethods: (MethodNode, EnvType) -> Unit = { _, _ -> } +) { + fun accept(client: ClassNode?, server: ClassNode?): ClassNode { + if (client == null) { + if (server!!.access and Opcodes.ACC_INTERFACE == 0) { + toMergedClass(server, EnvType.SERVER) + return server + } else { + // make interface methods & fields sided + server.methods?.forEach { toMergedMethods(it, EnvType.SERVER) } + server.fields?.forEach { toMergedFields(it, EnvType.SERVER) } + return server + } + } + if (server == null) { + if (client.access and Opcodes.ACC_INTERFACE == 0) { + toMergedClass(client, EnvType.CLIENT) + return client + } else { + // make interface methods & fields sided + client.methods?.forEach { toMergedMethods(it, EnvType.CLIENT) } + client.fields?.forEach { toMergedFields(it, EnvType.CLIENT) } + return client + } + } + if (!areClassMetadataEqual(client, server)) { + throw IllegalArgumentException("Class metadata is not equal!") + } + // weaker access + val merged = ClassNode() + merged.version = client.version.coerceAtLeast(server.version) + merged.access = selectWeakerAccess(client.access, server.access) + merged.name = client.name + merged.signature = client.signature + merged.superName = client.superName + merged.interfaces = client.interfaces?.toMutableSet()?.apply { addAll(server.interfaces ?: setOf()) }?.toList() + ?: server.interfaces + merged.sourceFile = client.sourceFile + merged.sourceDebug = client.sourceDebug + merged.module = client.module + merged.outerClass = client.outerClass + merged.outerMethod = client.outerMethod + merged.outerMethodDesc = client.outerMethodDesc + // merge client and server annotations + merged.visibleAnnotations = client.visibleAnnotations?.toMutableList()?.apply { + for (a in server.visibleAnnotations ?: listOf()) { + if (!any { areAnnotationNodesEqual(it, a) }) { + add(a) + } + } + } ?: server.visibleAnnotations + merged.invisibleAnnotations = client.invisibleAnnotations?.toMutableList()?.apply { + for (a in server.invisibleAnnotations ?: listOf()) { + if (!any { areAnnotationNodesEqual(it, a) }) { + add(a) + } + } + } ?: server.invisibleAnnotations + merged.visibleTypeAnnotations = client.visibleTypeAnnotations?.toMutableList()?.apply { + for (a in server.visibleTypeAnnotations ?: listOf()) { + if (!any { areAnnotationNodesEqual(it, a) }) { + add(a) + } + } + } ?: server.visibleTypeAnnotations + merged.invisibleTypeAnnotations = client.invisibleTypeAnnotations?.toMutableList()?.apply { + for (a in server.invisibleTypeAnnotations ?: listOf()) { + if (!any { areAnnotationNodesEqual(it, a) }) { + add(a) + } + } + } ?: server.invisibleTypeAnnotations + merged.attrs = client.attrs?.toMutableList()?.apply { + for (a in server.attrs ?: listOf()) { + if (!any { areAttributesEqual(it, a) }) { + add(a) + } + } + } ?: server.attrs + // merge inner classes + merged.innerClasses = client.innerClasses?.toMutableList()?.apply { + for (a in server.innerClasses ?: listOf()) { + if (!any { areInnerClassNodesEqual(it, a) }) { + add(a) + } + } + } ?: server.innerClasses + merged.nestHostClass = client.nestHostClass + merged.nestMembers = client.nestMembers?.toMutableSet() + ?.apply { addAll(server.nestMembers ?: setOf()) } + ?.toList() ?: server.nestMembers + merged.permittedSubclasses = client.permittedSubclasses?.toMutableSet() + ?.apply { addAll(server.permittedSubclasses ?: setOf()) } + ?.toList() ?: server.permittedSubclasses + // merge record components + merged.recordComponents = client.recordComponents?.toMutableList()?.apply { + for (a in server.recordComponents ?: listOf()) { + if (!any { areRecordComponentNodesEqual(it, a) }) { + add(a) + } + } + } ?: server.recordComponents + + // merge fields + val fields = client.fields.map { it to EnvType.CLIENT }.toMutableList() + outer@ for (field in server.fields) { + for (f in fields) { + if (areFieldNodesEqual(f.first, field)) { + fields.remove(f) + field.access = selectWeakerAccess(f.first.access, field.access) + fields.add(field to EnvType.COMBINED) + continue@outer + } + } + fields.add(field to EnvType.SERVER) + } + fields.forEach { toMergedFields(it.first, it.second) } + merged.fields = mutableListOf() + for (f in fields) { + merged.fields.add(f.first) + } + + // merge methods + val methods = client.methods.map { it to EnvType.CLIENT }.toMutableList() + outer@ for (method in server.methods) { + for (m in methods) { + if (areMethodNodesEqual(m.first, method)) { + methods.remove(m) + method.access = selectWeakerAccess(m.first.access, method.access) + methods.add(method to EnvType.COMBINED) + continue@outer + } + } + methods.add(method to EnvType.SERVER) + } + methods.forEach { toMergedMethods(it.first, it.second) } + merged.methods = mutableListOf() + for (m in methods) { + merged.methods.add(m.first) + } + toMergedClass(merged, EnvType.COMBINED) + return merged + } + + companion object { + fun areFieldNodesEqual(a: FieldNode, b: FieldNode): Boolean { + if (a.name != b.name) return false + if (a.desc != b.desc) return false + if (a.value != b.value) return false + // check static part of access + if (a.access and Opcodes.ACC_STATIC != b.access and Opcodes.ACC_STATIC) return false + val aVisibleAnnotations = a.visibleAnnotations?.toMutableList() ?: mutableListOf() + val bVisibleAnnotations = b.visibleAnnotations?.toMutableList() ?: mutableListOf() + if (aVisibleAnnotations.size != bVisibleAnnotations.size) return false + aVisibleAnnotations.removeIf { + bVisibleAnnotations.any { it2 -> + if (areAnnotationNodesEqual(it, it2)) { + bVisibleAnnotations.remove(it2) + true + } else { + false + } + } + } + if (aVisibleAnnotations.isNotEmpty()) return false + if (bVisibleAnnotations.isNotEmpty()) return false + val aInvisibleAnnotations = a.invisibleAnnotations?.toMutableList() ?: mutableListOf() + val bInvisibleAnnotations = b.invisibleAnnotations?.toMutableList() ?: mutableListOf() + if (aInvisibleAnnotations.size != bInvisibleAnnotations.size) return false + aInvisibleAnnotations.removeIf { + bInvisibleAnnotations.any { it2 -> + if (areAnnotationNodesEqual(it, it2)) { + bInvisibleAnnotations.remove(it2) + true + } else { + false + } + } + } + if (aInvisibleAnnotations.isNotEmpty()) return false + if (bInvisibleAnnotations.isNotEmpty()) return false + val aVisibleTypeAnnotations = a.visibleTypeAnnotations?.toMutableList() ?: mutableListOf() + val bVisibleTypeAnnotations = b.visibleTypeAnnotations?.toMutableList() ?: mutableListOf() + if (aVisibleTypeAnnotations.size != bVisibleTypeAnnotations.size) return false + aVisibleTypeAnnotations.removeIf { + bVisibleTypeAnnotations.any { it2 -> + if (areAnnotationNodesEqual(it, it2)) { + bVisibleTypeAnnotations.remove(it2) + true + } else { + false + } + } + } + if (aVisibleTypeAnnotations.isNotEmpty()) return false + if (bVisibleTypeAnnotations.isNotEmpty()) return false + val aInvisibleTypeAnnotations = a.invisibleTypeAnnotations?.toMutableList() ?: mutableListOf() + val bInvisibleTypeAnnotations = b.invisibleTypeAnnotations?.toMutableList() ?: mutableListOf() + if (aInvisibleTypeAnnotations.size != bInvisibleTypeAnnotations.size) return false + aInvisibleTypeAnnotations.removeIf { + bInvisibleTypeAnnotations.any { it2 -> + if (areAnnotationNodesEqual(it, it2)) { + bInvisibleTypeAnnotations.remove(it2) + true + } else { + false + } + } + } + if (aInvisibleTypeAnnotations.isNotEmpty()) return false + if (bInvisibleTypeAnnotations.isNotEmpty()) return false + val aAttributes = a.attrs?.toMutableList() ?: mutableListOf() + val bAttributes = b.attrs?.toMutableList() ?: mutableListOf() + if (aAttributes.size != bAttributes.size) return false + aAttributes.removeIf { + bAttributes.any { it2 -> + if (areAttributesEqual(it, it2)) { + bAttributes.remove(it2) + true + } else { + false + } + } + } + if (aAttributes.isNotEmpty()) return false + if (bAttributes.isNotEmpty()) return false + return true + } + + fun areMethodNodesEqual(a: MethodNode, b: MethodNode): Boolean { + if (a.name != b.name) return false + if (a.desc != b.desc) return false + // check static part of access + if (a.access and Opcodes.ACC_STATIC != b.access and Opcodes.ACC_STATIC) return false +// val aParameters = a.parameters?.toMutableList() ?: mutableListOf() +// val bParameters = b.parameters?.toMutableList() ?: mutableListOf() +// if (aParameters.size != bParameters.size) return false +// aParameters.removeIf { +// bParameters.any { it2 -> +// areParamsEqual(it, it2) +// } +// } +// if (aParameters.isNotEmpty()) return false + val aVisibleAnnotations = a.visibleAnnotations?.toMutableList() ?: mutableListOf() + val bVisibleAnnotations = b.visibleAnnotations?.toMutableList() ?: mutableListOf() + if (aVisibleAnnotations.size != bVisibleAnnotations.size) return false + aVisibleAnnotations.removeIf { + bVisibleAnnotations.any { it2 -> + if (areAnnotationNodesEqual(it, it2)) { + bVisibleAnnotations.remove(it2) + true + } else { + false + } + } + } + if (aVisibleAnnotations.isNotEmpty()) return false + if (bVisibleAnnotations.isNotEmpty()) return false + val aInvisibleAnnotations = a.invisibleAnnotations?.toMutableList() ?: mutableListOf() + val bInvisibleAnnotations = b.invisibleAnnotations?.toMutableList() ?: mutableListOf() + if (aInvisibleAnnotations.size != bInvisibleAnnotations.size) return false + aInvisibleAnnotations.removeIf { + bInvisibleAnnotations.any { it2 -> + if (areAnnotationNodesEqual(it, it2)) { + bInvisibleAnnotations.remove(it2) + true + } else { + false + } + } + } + if (aInvisibleAnnotations.isNotEmpty()) return false + if (bInvisibleAnnotations.isNotEmpty()) return false + val aVisibleTypeAnnotations = a.visibleTypeAnnotations?.toMutableList() ?: mutableListOf() + val bVisibleTypeAnnotations = b.visibleTypeAnnotations?.toMutableList() ?: mutableListOf() + if (aVisibleTypeAnnotations.size != bVisibleTypeAnnotations.size) return false + aVisibleTypeAnnotations.removeIf { + bVisibleTypeAnnotations.any { it2 -> + if (areAnnotationNodesEqual(it, it2)) { + bVisibleTypeAnnotations.remove(it2) + true + } else { + false + } + } + } + if (aVisibleTypeAnnotations.isNotEmpty()) return false + if (bVisibleTypeAnnotations.isNotEmpty()) return false + val aInvisibleTypeAnnotations = a.invisibleTypeAnnotations?.toMutableList() ?: mutableListOf() + val bInvisibleTypeAnnotations = b.invisibleTypeAnnotations?.toMutableList() ?: mutableListOf() + if (aInvisibleTypeAnnotations.size != bInvisibleTypeAnnotations.size) return false + aInvisibleTypeAnnotations.removeIf { + bInvisibleTypeAnnotations.any { it2 -> + if (areAnnotationNodesEqual(it, it2)) { + bInvisibleTypeAnnotations.remove(it2) + true + } else { + false + } + } + } + if (aInvisibleTypeAnnotations.isNotEmpty()) return false + if (bInvisibleTypeAnnotations.isNotEmpty()) return false + val aAttributes = a.attrs?.toMutableList() ?: mutableListOf() + val bAttributes = b.attrs?.toMutableList() ?: mutableListOf() + if (aAttributes.size != bAttributes.size) return false + aAttributes.removeIf { + bAttributes.any { it2 -> + if (areAttributesEqual(it, it2)) { + bAttributes.remove(it2) + true + } else { + false + } + } + } + if (aAttributes.isNotEmpty()) return false + if (bAttributes.isNotEmpty()) return false + + // check content + if (a.instructions.size() != b.instructions.size()) return false + val aInstructions = a.instructions + val bInstructions = b.instructions + for (i in 0 until aInstructions.size()) { + if (!areInstructionsEqual( + aInstructions[i], + bInstructions[i] + ) + ) throw IllegalStateException("Instructions are not equal: ${aInstructions[i]} != ${bInstructions[i]} at index $i in ${a.name} ${a.desc} (${a.instructions.size()} instructions) and ${b.name} ${b.desc} (${b.instructions.size()} instructions)") + } + + return true + } + + fun areAnnotationNodesEqual(a: AnnotationNode, b: AnnotationNode): Boolean { + return a.desc == b.desc // TODO: check values, maybe? + } + + fun areAttributesEqual(a: Attribute, b: Attribute): Boolean { + return a.type == b.type // TODO: check values, maybe? + } + + fun areInstructionsEqual(a: AbstractInsnNode, b: AbstractInsnNode): Boolean { + if (a.opcode != b.opcode) return false + if (a.type != b.type) return false + return true // TODO: contents of instructions + } + + fun selectWeakerAccess(a: Int, b: Int): Int { + if (a == b) return a + // should be final + var access = a and Opcodes.ACC_FINAL.inv() + if (a and Opcodes.ACC_FINAL != 0 && b and Opcodes.ACC_FINAL != 0) { + access = access or Opcodes.ACC_FINAL + } + + val aAccess = Access.from(a) + val bAccess = Access.from(b) + access = access and (Opcodes.ACC_PUBLIC or Opcodes.ACC_PROTECTED or Opcodes.ACC_PRIVATE).inv() + access = access or Access.max(aAccess, bAccess).value + + // all other flags must be the same + val other = (Opcodes.ACC_FINAL or Opcodes.ACC_PUBLIC or Opcodes.ACC_PROTECTED or Opcodes.ACC_PRIVATE).inv() + if ((a and other) != (b and other)) { + throw IllegalStateException("Other access is not the same: ${a.toString(16)} != ${b.toString(16)}") + } + return access + } + + fun areClassMetadataEqual(a: ClassNode, b: ClassNode): Boolean { + // require equal static + if (a.access and Opcodes.ACC_STATIC != b.access and Opcodes.ACC_STATIC) return false + if (a.name != b.name) return false + if (a.superName != b.superName) return false + if (a.outerClass != b.outerClass) return false + if (a.outerMethod != b.outerMethod) return false + if (a.outerMethodDesc != b.outerMethodDesc) return false + if (a.nestHostClass != b.nestHostClass) return false + return true + } + + fun areInnerClassNodesEqual(a: InnerClassNode, b: InnerClassNode): Boolean { + if (a.name != b.name) return false + if (a.outerName != b.outerName) return false + if (a.innerName != b.innerName) return false + if (a.access != b.access) return false + return true + } + + fun areRecordComponentNodesEqual(a: RecordComponentNode, b: RecordComponentNode): Boolean { + if (a.name != b.name) return false + if (a.descriptor != b.descriptor) return false + val aVisibleAnnotations = a.visibleAnnotations?.toMutableList() ?: mutableListOf() + val bVisibleAnnotations = b.visibleAnnotations?.toMutableList() ?: mutableListOf() + if (aVisibleAnnotations.size != bVisibleAnnotations.size) return false + aVisibleAnnotations.removeIf { + bVisibleAnnotations.any { it2 -> + if (areAnnotationNodesEqual(it, it2)) { + bVisibleAnnotations.remove(it2) + true + } else { + false + } + } + } + if (aVisibleAnnotations.isNotEmpty()) return false + if (bVisibleAnnotations.isNotEmpty()) return false + val aInvisibleAnnotations = a.invisibleAnnotations?.toMutableList() ?: mutableListOf() + val bInvisibleAnnotations = b.invisibleAnnotations?.toMutableList() ?: mutableListOf() + if (aInvisibleAnnotations.size != bInvisibleAnnotations.size) return false + aInvisibleAnnotations.removeIf { + bInvisibleAnnotations.any { it2 -> + if (areAnnotationNodesEqual(it, it2)) { + bInvisibleAnnotations.remove(it2) + true + } else { + false + } + } + } + if (aInvisibleAnnotations.isNotEmpty()) return false + if (bInvisibleAnnotations.isNotEmpty()) return false + val aVisibleTypeAnnotations = a.visibleTypeAnnotations?.toMutableList() ?: mutableListOf() + val bVisibleTypeAnnotations = b.visibleTypeAnnotations?.toMutableList() ?: mutableListOf() + if (aVisibleTypeAnnotations.size != bVisibleTypeAnnotations.size) return false + aVisibleTypeAnnotations.removeIf { + bVisibleTypeAnnotations.any { it2 -> + if (areAnnotationNodesEqual(it, it2)) { + bVisibleTypeAnnotations.remove(it2) + true + } else { + false + } + } + } + if (aVisibleTypeAnnotations.isNotEmpty()) return false + if (bVisibleTypeAnnotations.isNotEmpty()) return false + val aInvisibleTypeAnnotations = a.invisibleTypeAnnotations?.toMutableList() ?: mutableListOf() + val bInvisibleTypeAnnotations = b.invisibleTypeAnnotations?.toMutableList() ?: mutableListOf() + if (aInvisibleTypeAnnotations.size != bInvisibleTypeAnnotations.size) return false + aInvisibleTypeAnnotations.removeIf { + bInvisibleTypeAnnotations.any { it2 -> + if (areAnnotationNodesEqual(it, it2)) { + bInvisibleTypeAnnotations.remove(it2) + true + } else { + false + } + } + } + if (aInvisibleTypeAnnotations.isNotEmpty()) return false + if (bInvisibleTypeAnnotations.isNotEmpty()) return false + val aAttributes = a.attrs?.toMutableList() ?: mutableListOf() + val bAttributes = b.attrs?.toMutableList() ?: mutableListOf() + if (aAttributes.size != bAttributes.size) return false + aAttributes.removeIf { + bAttributes.any { it2 -> + if (areAttributesEqual(it, it2)) { + bAttributes.remove(it2) + true + } else { + false + } + } + } + if (aAttributes.isNotEmpty()) return false + if (bAttributes.isNotEmpty()) return false + return true + } + } + + + private enum class Access(val value: Int) { + PRIVATE(Opcodes.ACC_PRIVATE), + PACKAGE_PRIVATE(0), + PROTECTED(Opcodes.ACC_PROTECTED), + PUBLIC(Opcodes.ACC_PUBLIC), + ; + + companion object { + fun from(access: Int): Access { + return when (access and (Opcodes.ACC_PRIVATE or Opcodes.ACC_PROTECTED or Opcodes.ACC_PUBLIC)) { + Opcodes.ACC_PRIVATE -> PRIVATE + Opcodes.ACC_PROTECTED -> PROTECTED + Opcodes.ACC_PUBLIC -> PUBLIC + else -> PACKAGE_PRIVATE + } + } + + fun max(a: Access, b: Access): Access { + return values().last { it == a || it == b } + } + } + + } + +} diff --git a/plugin/src/main/kotlin/moe/nea/archenemy/mojang/MergedRepositoryProvider.kt b/plugin/src/main/kotlin/moe/nea/archenemy/mojang/MergedRepositoryProvider.kt new file mode 100644 index 0000000..e1c1b65 --- /dev/null +++ b/plugin/src/main/kotlin/moe/nea/archenemy/mojang/MergedRepositoryProvider.kt @@ -0,0 +1,98 @@ +package moe.nea.archenemy.mojang + +import moe.nea.archenemy.util.readZipFs +import moe.nea.archenemy.util.update +import moe.nea.archenemy.util.updateGMV +import moe.nea.archenemy.util.zipFs +import net.minecraftforge.artifactural.api.artifact.Artifact +import net.minecraftforge.artifactural.api.artifact.ArtifactIdentifier +import net.minecraftforge.artifactural.api.artifact.ArtifactType +import net.minecraftforge.artifactural.base.artifact.StreamableArtifact +import org.gradle.api.artifacts.ModuleDependency +import org.objectweb.asm.ClassReader +import org.objectweb.asm.ClassWriter +import org.objectweb.asm.tree.ClassNode +import java.io.ByteArrayInputStream +import java.io.File +import java.io.InputStream +import java.nio.file.Path +import java.security.MessageDigest +import kotlin.io.path.createDirectories +import kotlin.io.path.outputStream + +class MergedRepositoryProvider(archenemyMojangExtension: ArchenemyMojangExtension) : + AbstractTransformerRepository("merged", archenemyMojangExtension) { + data class Coordinate(val base: ModuleDependency, val overlay: ModuleDependency) : CHashable { + override fun updateHash(digest: MessageDigest) { + digest.update("merged") + digest.updateGMV("base", base) + digest.updateGMV("overlay", overlay) + } + } + + override fun getName(t: Coordinate): String { + return t.base.name + } + + override fun getVersion(t: Coordinate): String { + return t.base.version!! + } + + override fun getArtifact(identifier: ArtifactIdentifier, value: Coordinate, file: File): Artifact? { + if (!identifier.classifier.isNullOrBlank()) + return null + if (identifier.extension != "jar") return null + val baseJar = resolveDirect(value.base).singleOrNull() ?: return null + val overlayJar = resolveDirect(value.overlay).singleOrNull() ?: return null + return StreamableArtifact.ofStreamable( + identifier, + ArtifactType.BINARY, + lazyTransform(file) { mergeJar(baseJar, overlayJar, it) }) + } + + private fun readClasses( + path: Path, + readClassFile: (String, ClassNode) -> Unit, + readResource: (String, InputStream) -> Unit + ) { + path.readZipFs { path, inputStream -> + if (path.startsWith("META-INF/")) return@readZipFs + if (path.endsWith(".class")) { + val classReader = ClassReader(inputStream) + val classNode = ClassNode() + classReader.accept(classNode, 0) + readClassFile(path, classNode) + } else { + readResource(path, inputStream) + } + } + } + + fun mergeClass(base: ClassNode, overlay: ClassNode): ClassNode { + return ClassMerger().accept(base, overlay) + } + + private fun mergeJar(baseJar: File, overlayJar: File, target: File) { + target.toPath().zipFs().use { fs -> + val classNodes = mutableMapOf() + fun readResource(path: String, inputStream: InputStream) { + val p = fs.getPath(path) + p.parent?.createDirectories() + p.outputStream().use { outputStream -> + inputStream.copyTo(outputStream) + } + } + + fun readClass(path: String, classNode: ClassNode) { + classNodes.merge(path, classNode, ::mergeClass) + } + readClasses(baseJar.toPath(), ::readClass, ::readResource) + readClasses(overlayJar.toPath(), ::readClass, ::readResource) + classNodes.forEach { (path, node) -> + val writer = ClassWriter(0) + node.accept(writer) + readResource(path, ByteArrayInputStream(writer.toByteArray())) + } + } + } +} diff --git a/plugin/src/main/kotlin/moe/nea/archenemy/util/DigestUtils.kt b/plugin/src/main/kotlin/moe/nea/archenemy/util/DigestUtils.kt index 4f50000..d2de936 100644 --- a/plugin/src/main/kotlin/moe/nea/archenemy/util/DigestUtils.kt +++ b/plugin/src/main/kotlin/moe/nea/archenemy/util/DigestUtils.kt @@ -1,5 +1,6 @@ package moe.nea.archenemy.util +import org.gradle.api.artifacts.ModuleDependency import java.security.MessageDigest fun MessageDigest.updateField(text: String, value: String) { @@ -11,4 +12,11 @@ fun MessageDigest.updateField(text: String, value: String) { fun MessageDigest.update(text: String) { this.update(text.encodeToByteArray()) +} + +fun MessageDigest.updateGMV(name: String, moduleDependency: ModuleDependency) { + this.updateField( + name, + (moduleDependency.group ?: "") + ":" + moduleDependency.name + ":" + (moduleDependency.version ?: "") + ) } \ No newline at end of file diff --git a/plugin/src/main/kotlin/moe/nea/archenemy/util/EnvType.kt b/plugin/src/main/kotlin/moe/nea/archenemy/util/EnvType.kt new file mode 100644 index 0000000..72d18b6 --- /dev/null +++ b/plugin/src/main/kotlin/moe/nea/archenemy/util/EnvType.kt @@ -0,0 +1,9 @@ +package moe.nea.archenemy.util + +enum class EnvType(val classifier: String?, val mcp: Int) { + CLIENT("client", 0), + SERVER("server", 1), + COMBINED(null, 2), + DATAGEN("server", 1) + ; +} \ No newline at end of file diff --git a/plugin/src/main/kotlin/moe/nea/archenemy/util/FSUtil.kt b/plugin/src/main/kotlin/moe/nea/archenemy/util/FSUtil.kt new file mode 100644 index 0000000..0b2f70e --- /dev/null +++ b/plugin/src/main/kotlin/moe/nea/archenemy/util/FSUtil.kt @@ -0,0 +1,33 @@ +package moe.nea.archenemy.util + +import java.io.InputStream +import java.net.URI +import java.nio.file.FileSystem +import java.nio.file.FileSystems +import java.nio.file.Path +import java.util.zip.ZipInputStream +import java.util.zip.ZipOutputStream +import kotlin.io.path.createDirectories +import kotlin.io.path.exists +import kotlin.io.path.inputStream +import kotlin.io.path.outputStream + +// TODO: Figure out license shit with unimined cause i copied some of these utils? +fun Path.zipFs(): FileSystem { + if (!exists()) { + parent.createDirectories() + ZipOutputStream(this.outputStream()).use { } + } + return FileSystems.newFileSystem(URI.create("jar:${toUri()}"), mapOf("create" to true), null) +} + +fun Path.readZipFs(reader: (String, InputStream) -> Unit) { + ZipInputStream(this.inputStream()).use { stream -> + for (entry in generateSequence { stream.nextEntry }) { + if (entry.isDirectory) { + continue + } + reader(entry.name, stream) + } + } +} -- cgit