diff --git a/build.gradle.kts b/build.gradle.kts index 50cf1fe..c96af4b 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -13,6 +13,8 @@ version = providers.environmentVariable("VERSION").getOrElse("9999-local") repositories { mavenCentral() maven("https://maven.fabricmc.net/") + maven("https://maven.minecraftforge.net/") + maven("https://libraries.minecraft.net/") } configurations { @@ -20,11 +22,34 @@ configurations { } dependencies { + val shade by configurations + fun compileOnlyShade(artifact: String, action: ExternalModuleDependency.() -> Unit = {}) { + compileOnly(artifact, action) + shade(artifact, action) + } + implementation("com.google.guava:guava:32.1.2-jre") + implementation("org.apache.maven:maven-artifact:3.8.1") + implementation("com.electronwill.night-config:core:3.7.3") + implementation("com.electronwill.night-config:toml:3.7.3") + implementation("org.apache.logging.log4j:log4j-api:2.22.1") + implementation("org.apache.logging.log4j:log4j-core:2.22.1") + implementation("org.slf4j:slf4j-api:2.0.16") + implementation("com.google.code.gson:gson:2.11.0") + + compileOnlyShade("net.fabricmc:fabric-loader:0.15.10") + shade("net.fabricmc:access-widener:2.1.0") - compileOnly("net.fabricmc:fabric-loader:0.14.22") - "shade"("net.fabricmc:fabric-loader:0.14.22") - "shade"("net.fabricmc:access-widener:2.1.0") + compileOnlyShade("com.mojang:logging:1.2.7") { isTransitive = false } + compileOnlyShade("net.minecraftforge:fmlloader:1.20.4-49.0.49") { isTransitive = false } + compileOnlyShade("net.minecraftforge:modlauncher:10.2.1") { isTransitive = false } + compileOnlyShade("net.minecraftforge:securemodules:2.2.12") { isTransitive = false } + compileOnlyShade("net.minecraftforge:forgespi:7.1.5") { isTransitive = false } + compileOnlyShade("net.minecraftforge:JarJarFileSystems:0.3.26") { isTransitive = false } + compileOnlyShade("net.minecraftforge:JarJarMetadata:0.3.26") { isTransitive = false } + compileOnlyShade("net.minecraftforge:JarJarSelector:0.3.26") { isTransitive = false } + compileOnlyShade("net.minecraftforge:unsafe:0.9.2") { isTransitive = false } + compileOnlyShade("net.minecraftforge:mergetool-api:1.0") testImplementation(kotlin("test")) } @@ -34,7 +59,7 @@ tasks.test { } kotlin { - jvmToolchain(8) + jvmToolchain(17) } gradlePlugin { @@ -46,14 +71,32 @@ gradlePlugin { } } +tasks.processResources { + val meta = "${project.group}:${project.name}:${project.version}" + inputs.property("meta", meta) + + filesMatching("__meta.txt") { + expand("meta" to meta) + } +} + tasks.shadowJar { configurations = listOf(project.configurations["shade"]) archiveClassifier.set("") - relocate("net.fabricmc", "lol.bai.explosion.internal.lib.fabricloader") - minimize() + mergeServiceFiles() + + relocate("net.fabricmc", "lol.bai.explosion.internal.lib.net.fabricmc") + relocate("net.minecraftforge", "lol.bai.explosion.internal.lib.net.minecraftforge") + relocate("cpw", "lol.bai.explosion.internal.lib.cpw") + relocate("com.mojang", "lol.bai.explosion.internal.lib.com.mojang") + exclude("ui/**") exclude("assets/**") exclude("fabric*.json") + exclude("log4j2*") + exclude("LICENSE_fabric-loader") + exclude("META-INF/jars/**") + exclude("META-INF/org/apache/logging/**") } tasks.build { diff --git a/src/main/kotlin/lol/bai/explosion/ExplosionExt.kt b/src/main/kotlin/lol/bai/explosion/ExplosionExt.kt index 5ececd8..3e7ae61 100644 --- a/src/main/kotlin/lol/bai/explosion/ExplosionExt.kt +++ b/src/main/kotlin/lol/bai/explosion/ExplosionExt.kt @@ -3,11 +3,23 @@ package lol.bai.explosion import groovy.lang.Closure import groovy.lang.DelegatesTo import org.gradle.api.Action +import org.gradle.api.Transformer import org.gradle.api.artifacts.ExternalModuleDependency import org.gradle.api.provider.Provider +import java.io.File +import java.nio.file.Path interface ExplosionExt { + fun withTransformer(id: String, transformer: Transformer): ExplosionExt + + fun withTransformer(id: String, @DelegatesTo(File::class) closure: Closure) = withTransformer(id) r@{ + closure.delegate = this + return@r closure.call(this) + } + + // --- + fun fabric(action: Action): Provider fun fabric(notation: String) = fabric { @@ -23,4 +35,21 @@ interface ExplosionExt { closure.call(this) } + // --- + + fun forge(action: Action): Provider + + fun forge(notation: String) = forge { + maven(notation) + } + + fun forge(notation: Provider) = forge { + maven(notation) + } + + fun forge(@DelegatesTo(ExplosionDesc::class) closure: Closure<*>) = forge { + closure.delegate = this + closure.call(this) + } + } \ No newline at end of file diff --git a/src/main/kotlin/lol/bai/explosion/ExplosionPlugin.kt b/src/main/kotlin/lol/bai/explosion/ExplosionPlugin.kt index 2c7c105..44b86b5 100644 --- a/src/main/kotlin/lol/bai/explosion/ExplosionPlugin.kt +++ b/src/main/kotlin/lol/bai/explosion/ExplosionPlugin.kt @@ -1,10 +1,10 @@ package lol.bai.explosion import lol.bai.explosion.internal.ExplosionExtImpl +import lol.bai.explosion.internal.resolver.ResolverTask import org.gradle.api.Plugin import org.gradle.api.Project -import org.gradle.kotlin.dsl.create -import org.gradle.kotlin.dsl.maven +import org.gradle.kotlin.dsl.* import java.io.File import kotlin.io.path.createDirectories @@ -24,6 +24,12 @@ class ExplosionPlugin : Plugin { repositories.maven(outputDir.toFile()) { name = "ExplodedPluginCache" } + + val resolver = configurations.create(ResolverTask.CONFIGURATION) + dependencies { + resolver(embeddedKotlin("stdlib")) + resolver(ExplosionPlugin::class.java.classLoader.getResource("__meta.txt")!!.readText().trim()) + } } } diff --git a/src/main/kotlin/lol/bai/explosion/internal/ExplosionExtImpl.kt b/src/main/kotlin/lol/bai/explosion/internal/ExplosionExtImpl.kt index e264952..62694e6 100644 --- a/src/main/kotlin/lol/bai/explosion/internal/ExplosionExtImpl.kt +++ b/src/main/kotlin/lol/bai/explosion/internal/ExplosionExtImpl.kt @@ -3,46 +3,69 @@ package lol.bai.explosion.internal import com.google.common.hash.Hashing import lol.bai.explosion.ExplosionDesc import lol.bai.explosion.ExplosionExt -import lol.bai.explosion.internal.fabric.FakeGameProvider -import net.fabricmc.api.EnvType -import net.fabricmc.loader.impl.FabricLoaderImpl -import net.fabricmc.loader.impl.discovery.ModResolver -import net.fabricmc.loader.impl.discovery.createModDiscoverer -import net.fabricmc.loader.impl.launch.FabricLauncherBase -import net.fabricmc.loader.impl.launch.knot.Knot +import lol.bai.explosion.internal.resolver.ResolverTask import org.gradle.api.Action import org.gradle.api.Project +import org.gradle.api.Transformer +import org.gradle.kotlin.dsl.create import org.gradle.kotlin.dsl.invoke import java.io.File import java.nio.file.Path import kotlin.io.path.* -abstract class ExplosionExtImpl( +private class BomDependency( + val group: String, + val name: String, + val version: String +) + +open class ExplosionExtImpl( private val project: Project, - private val outputDir: Path + private val outputDir: Path, + private val transformerId: String?, + private val transformer: Transformer? ) : ExplosionExt { + @Suppress("unused") + constructor(project: Project, outputDir: Path) : this(project, outputDir, null, null) + private fun Path.resolve(vararg path: String): Path { return resolve(path.joinToString(File.separator)) } - private fun sanitizeVersion(version: String): String { - return version.replace(Regex("[^A-Za-z0-9.]"), "_") - } + private fun createPom(loader: String, name: String, version: String, jarPlacer: (Path) -> Unit): BomDependency { + val group = "exploded" + + var sanitizedVersion = loader + "_" + version.replace(Regex("[^A-Za-z0-9.]"), "_") + if (transformerId != null) sanitizedVersion = "${sanitizedVersion}_transformed_$transformerId" - private fun createPom(name: String, version: String): Path { val pom = this.javaClass.classLoader.getResource("artifact.xml")!!.readText() - .replace("%GROUP_ID%", "exploded") + .replace("%GROUP_ID%", group) .replace("%ARTIFACT_ID%", name) - .replace("%VERSION%", version) + .replace("%VERSION%", sanitizedVersion) + + val dir = outputDir.resolve("exploded", name, sanitizedVersion) - val dir = outputDir.resolve("exploded", name, version) dir.createDirectories() - dir.resolve("${name}-${version}.pom").writeText(pom) - return dir.resolve("${name}-${version}.jar") + dir.resolve("${name}-${sanitizedVersion}.pom").writeText(pom) + + val jarPath = dir.resolve("${name}-${sanitizedVersion}.jar") + jarPlacer(jarPath) + + if (transformer != null) { + val originalJarPath = dir.resolve("__original-${transformerId}.jar") + jarPath.moveTo(originalJarPath, overwrite = true) + + val transformed = transformer.transform(originalJarPath) + transformed.moveTo(jarPath, overwrite = true) + + originalJarPath.deleteIfExists() + } + + return BomDependency(group, name, sanitizedVersion) } - private fun getOrCreateBom(hash: String, deps: () -> List>): String { + private fun getOrCreateBom(hash: String, deps: () -> List): String { val bom = "exploded-bom:${hash}:1" val dir = outputDir.resolve("exploded-bom", hash, "1") dir.createDirectories() @@ -57,19 +80,16 @@ abstract class ExplosionExtImpl( val depTemplate = this.javaClass.classLoader.getResource("bom_dependency.xml")!!.readText() - fun createDependency(group: String, name: String, version: String): String { - val lines = depTemplate.replace("%GROUP_ID%", group) - .replace("%ARTIFACT_ID%", name) - .replace("%VERSION%", version) - .lines() - - return lines.joinToString(separator = "\n ", prefix = " ") - } - val depsStr = StringBuilder() - deps().forEach { (depName, depVersion) -> + deps().forEach { depsStr.append('\n') - depsStr.append(createDependency("exploded", depName, depVersion)) + + val lines = depTemplate + .replace("%GROUP_ID%", it.group) + .replace("%ARTIFACT_ID%", it.name) + .replace("%VERSION%", it.version) + .lines() + depsStr.append(lines.joinToString(separator = "\n ", prefix = " ")) } depsStr.append('\n') @@ -83,62 +103,64 @@ abstract class ExplosionExtImpl( return bom } - override fun fabric(action: Action) = project.provider { + private fun resolve( + action: Action, + loader: String + ) = project.provider { val desc = ExplosionDescImpl(project) action(desc) - val tempDir = createTempDirectory() + val inputDir = createTempDirectory() + val outputDir = createTempDirectory() try { - val hashBuilder = StringBuilder() + val hashBuilder = StringBuilder(loader).append(";") + if (transformerId != null) hashBuilder.append(transformerId).append(";") desc.resolveJars { hashBuilder.append(Hashing.murmur3_128().hashBytes(it.readBytes())) hashBuilder.append(";") - it.copyTo(tempDir.resolve(it.name).toFile()) + it.copyTo(inputDir.resolve(it.name).toFile()) } val hash = Hashing.murmur3_128().hashString(hashBuilder.toString(), Charsets.UTF_8).toString() - return@provider getOrCreateBom(hash) { - if (FabricLauncherBase.getLauncher() == null) Knot(EnvType.CLIENT) - - val loader = FabricLoaderImpl.INSTANCE.apply { - gameProvider = FakeGameProvider(tempDir) - } - - val candidates = createModDiscoverer(tempDir).discoverMods(loader, mutableMapOf()) - candidates.removeIf { it.id == "java" } - val candidateIds = hashSetOf() - - candidates.forEach { - candidateIds.add(it.id) - candidateIds.addAll(it.provides) - } - - candidates.forEach { candidate -> - candidate.metadata.dependencies = candidate.metadata.dependencies.filter { - candidateIds.contains(it.modId) - } + return@provider getOrCreateBom(hash) { + val task = project.tasks.create("__explosion_resolver_" + Any().hashCode()) { + this.loader.set(loader) + this.inputDir.set(inputDir.toFile()) + this.outputDir.set(outputDir.toFile()) } - val mods = ModResolver.resolve(candidates, EnvType.CLIENT, mutableMapOf()) - val bomDeps = arrayListOf>() + task.exec() + task.enabled = false + val metaLines = outputDir.resolve("__meta.txt").readLines() + val bomDeps = arrayListOf() - for (mod in mods) { - val version = sanitizeVersion(mod.version.friendlyString) + for (line in metaLines) { + val trimmed = line.trim() + if (trimmed.isEmpty()) continue - bomDeps.add(mod.id to version) - val out = createPom(mod.id, version) - mod.copyToDir(out.parent, false).moveTo(out, overwrite = true) + val (modFile, modId, version) = trimmed.split("\t") + bomDeps.add(createPom(loader, modId, version) { path -> + outputDir.resolve(modFile).copyTo(path) + }) } return@getOrCreateBom bomDeps } } finally { - tempDir.toFile().deleteRecursively() + inputDir.toFile().deleteRecursively() + outputDir.toFile().deleteRecursively() } } + override fun withTransformer(id: String, transformer: Transformer): ExplosionExt { + return ExplosionExtImpl(project, outputDir, id, transformer) + } + + override fun fabric(action: Action) = resolve(action, "fabric") + override fun forge(action: Action) = resolve(action, "forge") + } \ No newline at end of file diff --git a/src/main/kotlin/lol/bai/explosion/internal/resolver/Main.kt b/src/main/kotlin/lol/bai/explosion/internal/resolver/Main.kt new file mode 100644 index 0000000..f83685f --- /dev/null +++ b/src/main/kotlin/lol/bai/explosion/internal/resolver/Main.kt @@ -0,0 +1,97 @@ +package lol.bai.explosion.internal.resolver + +import lol.bai.explosion.internal.fabric.FakeGameProvider +import net.fabricmc.api.EnvType +import net.fabricmc.loader.impl.FabricLoaderImpl +import net.fabricmc.loader.impl.discovery.ModResolver +import net.fabricmc.loader.impl.discovery.createModDiscoverer +import net.fabricmc.loader.impl.launch.FabricLauncherBase +import net.fabricmc.loader.impl.launch.knot.Knot +import net.minecraftforge.fml.loading.UniqueModListBuilder +import net.minecraftforge.fml.loading.moddiscovery.JarInJarDependencyLocator +import net.minecraftforge.fml.loading.moddiscovery.ModFile +import net.minecraftforge.fml.loading.moddiscovery.createModsFolderLocator +import net.minecraftforge.forgespi.locating.IModFile +import java.nio.file.Path +import kotlin.io.path.* + +fun main(args: Array) { + val (loader, inputDirStr, outputDirStr) = args + + val inputDir = Path(inputDirStr) + val outputDir = Path(outputDirStr) + + when (loader) { + "fabric" -> fabric(inputDir, outputDir) + "forge" -> forge(inputDir, outputDir) + else -> throw IllegalArgumentException("Unsupported loader $loader") + } +} + +private fun fabric(inputDir: Path, outputDir: Path) { + if (FabricLauncherBase.getLauncher() == null) Knot(EnvType.CLIENT) + + val loader = FabricLoaderImpl.INSTANCE.apply { + gameProvider = FakeGameProvider(inputDir) + } + + val candidates = createModDiscoverer(inputDir).discoverMods(loader, mutableMapOf()) + candidates.removeIf { it.id == "java" } + + val candidateIds = hashSetOf() + candidates.forEach { + candidateIds.add(it.id) + candidateIds.addAll(it.provides) + } + + candidates.forEach { candidate -> + candidate.metadata.dependencies = candidate.metadata.dependencies.filter { + candidateIds.contains(it.modId) + } + } + + val meta = StringBuilder() + val mods = ModResolver.resolve(candidates, EnvType.CLIENT, mutableMapOf()) + + for (mod in mods) { + val path = outputDir.resolve("${mod.id}-${mod.version.friendlyString}") + mod.copyToDir(outputDir, false).moveTo(path, overwrite = true) + meta.append(path.name) + .append("\t") + .append(mod.id) + .append("\t") + .append(mod.version.friendlyString) + .append("\n") + } + + outputDir.resolve("__meta.txt").writeText(meta.toString()) +} + +@Suppress("UnstableApiUsage") +private fun forge(inputDir: Path, outputDir: Path) { + val modLocator = createModsFolderLocator(inputDir, "explosion!!!") + val jarJar = JarInJarDependencyLocator() + + val primeModFiles = modLocator.scanMods().map { it.file } + val jarJarModFiles = jarJar.scanMods(primeModFiles) + val combinedModFiles = (primeModFiles + jarJarModFiles).filterIsInstance() + + val meta = StringBuilder() + val uniqueModFiles = UniqueModListBuilder(combinedModFiles) + .buildUniqueList().modFiles + .filter { it.type == IModFile.Type.MOD } + + for (modFile in uniqueModFiles) { + val mod = modFile.modInfos[0] + val path = outputDir.resolve("${mod.modId}-${mod.version}") + modFile.filePath.copyTo(path, overwrite = true) + meta.append(path.name) + .append("\t") + .append(mod.modId) + .append("\t") + .append(mod.version) + .append("\n") + } + + outputDir.resolve("__meta.txt").writeText(meta.toString()) +} \ No newline at end of file diff --git a/src/main/kotlin/lol/bai/explosion/internal/resolver/ResolverTask.kt b/src/main/kotlin/lol/bai/explosion/internal/resolver/ResolverTask.kt new file mode 100644 index 0000000..b4e07e7 --- /dev/null +++ b/src/main/kotlin/lol/bai/explosion/internal/resolver/ResolverTask.kt @@ -0,0 +1,40 @@ +package lol.bai.explosion.internal.resolver + +import org.gradle.api.file.DirectoryProperty +import org.gradle.api.provider.Property +import org.gradle.api.tasks.* +import org.gradle.work.DisableCachingByDefault + +@DisableCachingByDefault +abstract class ResolverTask : JavaExec() { + + companion object { + const val CONFIGURATION = "__explosion_resolver" + } + + @get:Input + abstract val loader: Property + + @get:InputDirectory + abstract val inputDir: DirectoryProperty + + @get:OutputDirectory + abstract val outputDir: DirectoryProperty + + init { + classpath = project.configurations.getByName(CONFIGURATION) + mainClass.set("lol.bai.explosion.internal.resolver.MainKt") + } + + @TaskAction + override fun exec() { + args( + loader.get(), + inputDir.asFile.get().absolutePath, + outputDir.asFile.get().absolutePath, + ) + + super.exec() + } + +} \ No newline at end of file diff --git a/src/main/kotlin/net/minecraftforge/fml/loading/moddiscovery/DiscoveryAccess.kt b/src/main/kotlin/net/minecraftforge/fml/loading/moddiscovery/DiscoveryAccess.kt new file mode 100644 index 0000000..30bcbe3 --- /dev/null +++ b/src/main/kotlin/net/minecraftforge/fml/loading/moddiscovery/DiscoveryAccess.kt @@ -0,0 +1,5 @@ +package net.minecraftforge.fml.loading.moddiscovery + +import java.nio.file.Path + +fun createModsFolderLocator(modsFolder: Path, name: String) = ModsFolderLocator(modsFolder, name) \ No newline at end of file diff --git a/src/main/resources/__meta.txt b/src/main/resources/__meta.txt new file mode 100644 index 0000000..b732f7a --- /dev/null +++ b/src/main/resources/__meta.txt @@ -0,0 +1 @@ +${meta} \ No newline at end of file