Skip to content

Commit 9e2f1e9

Browse files
committed
finally get everything to compile
1 parent a57d135 commit 9e2f1e9

38 files changed

Lines changed: 862 additions & 500 deletions

File tree

buildSrc/build.gradle.kts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,11 @@ plugins {
55
repositories {
66
mavenCentral()
77
maven("https://repo.polyfrost.org/releases")
8+
}
9+
10+
dependencies {
11+
// Mirroring https://github.com/EssentialGG/essential-gradle-toolkit/blob/master/build.gradle.kts
12+
implementation("org.ow2.asm:asm-commons:9.6")
13+
implementation("com.google.guava:guava:33.0.0-jre")
14+
implementation("org.jetbrains.kotlinx:kotlinx-metadata-jvm:0.9.0")
815
}
Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
/*
2+
* This file is part of essential-gradle-toolkit, licensed under the GPL-3.0.
3+
*
4+
* Copyright (C) 2025 EssentialGG and contributors
5+
*
6+
* This program is free software: you can redistribute it and/or modify
7+
* it under the terms of the GNU General Public License as published by
8+
* the Free Software Foundation, either version 3 of the License, or
9+
* (at your option) any later version.
10+
*
11+
* This program is distributed in the hope that it will be useful,
12+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14+
* GNU General Public License for more details.
15+
*
16+
* You should have received a copy of the GNU General Public License
17+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
18+
*/
19+
20+
package gg.essential.gradle.util
21+
22+
import gg.essential.gradle.util.relocate.KotlinMetadataRemappingClassVisitor
23+
import org.gradle.api.Project
24+
import org.gradle.api.artifacts.transform.InputArtifact
25+
import org.gradle.api.artifacts.transform.TransformAction
26+
import org.gradle.api.artifacts.transform.TransformOutputs
27+
import org.gradle.api.artifacts.transform.TransformParameters
28+
import org.gradle.api.attributes.Attribute
29+
import org.gradle.api.file.FileSystemLocation
30+
import org.gradle.api.provider.Provider
31+
import org.gradle.api.provider.SetProperty
32+
import org.gradle.api.tasks.Input
33+
import org.objectweb.asm.ClassReader
34+
import org.objectweb.asm.ClassWriter
35+
import org.objectweb.asm.commons.ClassRemapper
36+
import org.objectweb.asm.commons.Remapper
37+
import java.io.Closeable
38+
import java.io.File
39+
import java.io.Serializable
40+
import java.util.jar.JarInputStream
41+
import java.util.jar.JarOutputStream
42+
import java.util.zip.ZipEntry
43+
44+
/**
45+
* Relocates packages and single files in an artifact.
46+
*
47+
* If a package is relocated, the folder containing it will be relocated as a whole.
48+
* File renames take priority over package relocations.
49+
*
50+
* The packages do not have to be part of the artifact, e.g. a completely valid use case would be relocating guava
51+
* packages in an artifact using guava (for actual use, you'd of course also have to relocate the guava artifact itself,
52+
* otherwise the classes referred to after the relocation will not exist). This can be used together with [prebundle] to
53+
* create fat jars which apply at dev time (to e.g. use two different versions of the same library).
54+
*
55+
* To simplify setup, use [registerRelocationAttribute].
56+
*/
57+
abstract class RelocationTransform : TransformAction<RelocationTransform.Parameters> {
58+
interface Parameters : TransformParameters {
59+
@get:Input
60+
val relocations: SetProperty<Relocation>
61+
62+
@get:Input
63+
val remapStringsIn: SetProperty<String>
64+
65+
@get:Input
66+
val renames: SetProperty<Rename>
67+
68+
fun relocate(sourcePackage: String, targetPackage: String) =
69+
relocations.add(Relocation(sourcePackage, targetPackage))
70+
71+
fun remapStringsIn(cls: String) =
72+
remapStringsIn.add(cls)
73+
74+
fun rename(sourceFile: String, targetFile: String) =
75+
renames.add(Rename(sourceFile, targetFile))
76+
}
77+
78+
data class Relocation(val sourcePackage: String, val targetPackage: String) : Serializable
79+
data class Rename(val sourceFile: String, val targetFile: String) : Serializable
80+
81+
@get:InputArtifact
82+
abstract val input: Provider<FileSystemLocation>
83+
84+
override fun transform(outputs: TransformOutputs) {
85+
val fileMap = parameters.renames.get().associate { it.sourceFile to it.targetFile }
86+
87+
val jvmPackageMap = parameters.relocations.get().associate {
88+
it.sourcePackage.replace('.', '/') + '/' to it.targetPackage.replace('.', '/') + '/'
89+
}
90+
val javaPackageMap = jvmPackageMap.map { (source, target) ->
91+
source.replace('/', '.') to target.replace('/', '.')
92+
}.toMap()
93+
val absoluteFolderMap = jvmPackageMap.map { (source, target) ->
94+
"/$source" to "/$target"
95+
}.toMap()
96+
97+
val remapStringsInFiles = parameters.remapStringsIn.get().map {
98+
it.replace('.', '/') + ".class"
99+
}
100+
101+
open class ClassPrefixRemapper : Remapper() {
102+
override fun map(typeName: String): String = map(jvmPackageMap, typeName)
103+
104+
protected fun map(mappings: Map<String, String>, typeName: String): String {
105+
for ((sourcePackage, targetPackage) in mappings) {
106+
if (typeName.startsWith(sourcePackage)) {
107+
return targetPackage + typeName.substring(sourcePackage.length)
108+
}
109+
}
110+
return typeName
111+
}
112+
}
113+
val baseRemapper = ClassPrefixRemapper()
114+
115+
class ClassPrefixAndStringsRemapper : ClassPrefixRemapper() {
116+
override fun mapValue(value: Any?): Any {
117+
if (value is String) {
118+
return map(absoluteFolderMap, map(jvmPackageMap, map(javaPackageMap, value)))
119+
}
120+
return super.mapValue(value)
121+
}
122+
}
123+
val stringRemapper = ClassPrefixAndStringsRemapper()
124+
125+
val input = input.get().asFile
126+
val output = outputs.file(input.nameWithoutExtension + "-relocated.jar")
127+
(input to output).useInOut { jarIn, jarOut ->
128+
while (true) {
129+
val entry = jarIn.nextJarEntry ?: break
130+
val originalBytes = jarIn.readBytes()
131+
val remapper = if (entry.name in remapStringsInFiles) stringRemapper else baseRemapper
132+
133+
val modifiedBytes = if (entry.name.endsWith(".class")) {
134+
val reader = ClassReader(originalBytes)
135+
// Not copying the constant pool cause that leaves references to the old classes which, while any
136+
// lazy tool will never end up resolving them, do get resolved by e.g. proguard.
137+
val writer = ClassWriter(0)
138+
reader.accept(ClassRemapper(KotlinMetadataRemappingClassVisitor(remapper, writer), remapper), 0)
139+
writer.toByteArray()
140+
} else if (entry.name.startsWith("META-INF/services/")) {
141+
String(originalBytes).replace('.', '/').lines().map(remapper::map).joinToString("\n").replace('/','.').encodeToByteArray()
142+
} else {
143+
originalBytes
144+
}
145+
146+
jarOut.putNextEntry(ZipEntry(fileMap[entry.name] ?: remapper.map(entry.name)))
147+
jarOut.write(modifiedBytes)
148+
jarOut.closeEntry()
149+
}
150+
}
151+
}
152+
153+
private inline fun Pair<File, File>.useInOut(block: (jarIn: JarInputStream, jarOut: JarOutputStream) -> Unit) =
154+
first.inputStream().nestedUse(::JarInputStream) { jarIn ->
155+
second.outputStream().nestedUse(::JarOutputStream) { jarOut ->
156+
block(jarIn, jarOut)
157+
}
158+
}
159+
160+
private inline fun <T: Closeable, U: Closeable> T.nestedUse(nest: (T) -> U, block: (U) -> Unit) =
161+
use { nest(it).use(block) }
162+
163+
companion object {
164+
fun Project.registerRelocationAttribute(name: String, configure: Parameters.() -> Unit): Attribute<Boolean> {
165+
val attribute = Attribute.of(name, Boolean::class.javaObjectType)
166+
167+
dependencies.registerTransform(RelocationTransform::class.java) {
168+
from.attribute(attribute, false)
169+
to.attribute(attribute, true)
170+
parameters(configure)
171+
}
172+
173+
dependencies.artifactTypes.all {
174+
attributes.attribute(attribute, false)
175+
}
176+
177+
return attribute
178+
}
179+
}
180+
}
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
/*
2+
* This file is part of essential-gradle-toolkit, licensed under the GPL-3.0.
3+
*
4+
* Copyright (C) 2025 EssentialGG and contributors
5+
*
6+
* This program is free software: you can redistribute it and/or modify
7+
* it under the terms of the GNU General Public License as published by
8+
* the Free Software Foundation, either version 3 of the License, or
9+
* (at your option) any later version.
10+
*
11+
* This program is distributed in the hope that it will be useful,
12+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14+
* GNU General Public License for more details.
15+
*
16+
* You should have received a copy of the GNU General Public License
17+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
18+
*/
19+
20+
package gg.essential.gradle.util
21+
22+
import com.google.common.base.Stopwatch
23+
import org.gradle.api.Project
24+
import org.gradle.api.artifacts.Configuration
25+
import org.gradle.api.file.FileCollection
26+
import org.gradle.api.logging.Logger
27+
import org.gradle.api.tasks.util.PatternFilterable
28+
import org.gradle.api.tasks.util.PatternSet
29+
import java.io.File
30+
import java.io.OutputStream
31+
import java.security.MessageDigest
32+
import java.util.jar.JarOutputStream
33+
import java.util.zip.ZipEntry
34+
35+
/**
36+
* Bundles all dependencies from the given [configuration] into a single, dedicated jar and returns a file collection
37+
* containing that jar.
38+
* Primarily for use in dependency declarations, so fat jars of certain dependencies (with potentially relocated
39+
* transitive dependencies) can be created and then depended upon as usual. Compared to simply relocating in a later
40+
* shadow task, this has the advantage that IDEA will see the relocated dependency rather than the original, which e.g.
41+
* allows one to use two different versions of the same dependency at dev time.
42+
*
43+
* If [jijName] is provided, the fat jar will additionally be wrapped in an outer jar, such that the classes are not
44+
* actually visible if the file collection is put onto the classpath. This may be useful when the jar is never meant to
45+
* directly be on the classpath but rather only in a dedicated class loader or JVM.
46+
* The given [jijName] determines the path+name of the inner jar within the outer jar.
47+
*/
48+
fun Project.prebundle(configuration: Configuration, jijName: String? = null, configure: PatternFilterable.() -> Unit = {}): FileCollection {
49+
val output = projectDir
50+
.resolve(".gradle")
51+
.resolve("prebundled-jars")
52+
.resolve("${configuration.name}.jar")
53+
54+
val filter = PatternSet().apply(configure)
55+
56+
// Delay resolving the configuration in case it is not yet fully configured
57+
afterEvaluate {
58+
bundle(configuration, filter, jijName, output, logger)
59+
}
60+
61+
return files(output)
62+
}
63+
64+
private fun Project.bundle(configuration: Configuration, filter: PatternSet, jijName: String?, output: File, logger: Logger) {
65+
output.parentFile.mkdirs()
66+
67+
val hash = configuration.computeHash().apply {
68+
update(filter.hashCode().toBigInteger().toByteArray())
69+
update(jijName?.toByteArray() ?: byteArrayOf())
70+
update(byteArrayOf(0, 0, 0, 2)) // code version, incremented with each semantic change
71+
}.digest()
72+
val hashFile = output.resolveSibling(output.name + ".md5")
73+
if (hashFile.exists() && hashFile.readBytes().contentEquals(hash) && output.exists()) {
74+
return
75+
}
76+
hashFile.delete()
77+
output.delete()
78+
79+
val stopwatch = Stopwatch.createStarted()
80+
logger.lifecycle(":preparing ${configuration.name} jar")
81+
82+
val spec = filter.asSpec
83+
val visitedEntries = mutableSetOf<String>()
84+
output.outputStream().use { fileOut_ ->
85+
var fileOut: OutputStream = fileOut_
86+
if (jijName != null) {
87+
fileOut = JarOutputStream(fileOut).apply {
88+
putNextEntry(ZipEntry(jijName))
89+
}
90+
}
91+
JarOutputStream(fileOut).use { jarOut ->
92+
for (sourceFile in configuration.files) {
93+
project.zipTree(sourceFile).visit {
94+
if (!visitedEntries.add(path)) return@visit
95+
if (!spec.isSatisfiedBy(this)) return@visit
96+
97+
jarOut.putNextEntry(ZipEntry(if (isDirectory) "$path/" else path).apply { time = CONSTANT_TIME_FOR_ZIP_ENTRIES })
98+
open().use { copyTo(jarOut) }
99+
jarOut.closeEntry()
100+
}
101+
}
102+
}
103+
}
104+
hashFile.writeBytes(hash)
105+
106+
logger.lifecycle(":prepared ${configuration.name} jar in $stopwatch")
107+
}
108+
109+
private fun Configuration.computeHash(): MessageDigest = files
110+
.sortedBy { it.name }
111+
.fold(MessageDigest.getInstance("MD5")) { digest, file ->
112+
// if the file path already contains a hash, that's good enough, otherwise we need to read its contents
113+
digest.update(file.findHashInPath()?.toByteArray() ?: file.readBytes())
114+
digest
115+
}
116+
117+
private fun File.findHashInPath(): String? {
118+
val path = absolutePath.replace('\\', '/')
119+
if ("/caches/modules-2/files-2.1/" in path && parentFile.name.length == 40) {
120+
return parentFile.name
121+
}
122+
if ("/caches/transforms-3/" in path && parentFile.parentFile.name.length == 32) {
123+
return parentFile.parentFile.name
124+
}
125+
return null
126+
}

0 commit comments

Comments
 (0)