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+ }
0 commit comments