diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c6507e0..4d9c624 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -6,27 +6,51 @@ jobs: runs-on: ubuntu-20.04 steps: - uses: actions/checkout@v4 + - name: Install Packages run: sudo apt-get install -y advancecomp + + # cache local gradle files, global ones will be taken care of by the setup-gradle action + - uses: actions/cache@v4 + with: + path: | + **/.gradle/ + **/build/ + key: ${{ runner.os }}-gradlelocal-${{ github.ref }} + - uses: actions/setup-java@v4 with: distribution: temurin java-version: 21 + - name: Setup Gradle uses: gradle/actions/setup-gradle@v3 with: cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }} + - name: :build - run: ./gradlew build --stacktrace + run: ./gradlew build --stacktrace --no-daemon + + - name: Upload artifacts + if: always() + uses: actions/upload-artifact@v4 + with: + name: zume + path: | + **/build/libs/zume*.jar + build/libs/*mappings.txt + - name: :smokeTest + id: smokeTest uses: coactions/setup-xvfb@v1 with: - run: ./gradlew :smokeTest - - name: Upload artifacts - uses: actions/upload-artifact@v3 + run: ./gradlew :smokeTest --no-daemon + + - name: Upload test results + if: failure() + uses: actions/upload-artifact@v4 with: - name: zume + name: smokeTest path: | - **/zume-*.jar - **/*mappings.txt - build/smoke_test/**/*.log + build/smoke_test/**/logs/** + build/smoke_test/**/setup.log diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index d664067..fc6639f 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -18,13 +18,13 @@ jobs: cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY_PR }} cache-read-only: true - name: :build - run: ./gradlew build + run: ./gradlew build --no-daemon - name: :smokeTest uses: coactions/setup-xvfb@v1 with: - run: ./gradlew :smokeTest + run: ./gradlew :smokeTest --no-daemon - name: Upload artifacts - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: zume path: | diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index dd204f2..0f4daf6 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -27,7 +27,7 @@ jobs: with: run: ./gradlew :smokeTest - name: :publishMods - run: ./gradlew publishMods -Prelease_channel=RELEASE + run: ./gradlew publishMods -Prelease_channel=RELEASE --no-daemon env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} MODRINTH_TOKEN: ${{ secrets.MODRINTH_TOKEN }} diff --git a/.github/workflows/release_dev.yml b/.github/workflows/release_dev.yml index 29231c8..223b104 100644 --- a/.github/workflows/release_dev.yml +++ b/.github/workflows/release_dev.yml @@ -21,7 +21,7 @@ jobs: cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }} cache-read-only: true - name: :publishMods - run: ./gradlew publishMods -Prelease_channel=DEV_BUILD --stacktrace + run: ./gradlew publishMods -Prelease_channel=DEV_BUILD --stacktrace --no-daemon env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} DISCORD_WEBHOOK: ${{ secrets.DISCORD_DEV_WEBHOOK }} \ No newline at end of file diff --git a/.github/workflows/release_pre.yml b/.github/workflows/release_pre.yml index c5e0045..16466e8 100644 --- a/.github/workflows/release_pre.yml +++ b/.github/workflows/release_pre.yml @@ -21,7 +21,7 @@ jobs: cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }} cache-read-only: true - name: :publishMods - run: ./gradlew publishMods -Prelease_channel=PRE_RELEASE + run: ./gradlew publishMods -Prelease_channel=PRE_RELEASE --no-daemon env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} DISCORD_WEBHOOK: ${{ secrets.DISCORD_DEV_WEBHOOK }} \ No newline at end of file diff --git a/.github/workflows/release_rc.yml b/.github/workflows/release_rc.yml index 4e3e3a9..9dce8ec 100644 --- a/.github/workflows/release_rc.yml +++ b/.github/workflows/release_rc.yml @@ -21,7 +21,7 @@ jobs: cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }} cache-read-only: true - name: :publishMods - run: ./gradlew publishMods -Prelease_channel=RELEASE_CANDIDATE + run: ./gradlew publishMods -Prelease_channel=RELEASE_CANDIDATE --no-daemon env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} DISCORD_WEBHOOK: ${{ secrets.DISCORD_DEV_WEBHOOK }} \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index 4fc46fb..a142d2a 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,6 +1,11 @@ @file:Suppress("UnstableApiUsage") import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar -import dev.nolij.zumegradle.* +import dev.nolij.zumegradle.DeflateAlgorithm +import dev.nolij.zumegradle.JsonShrinkingType +import dev.nolij.zumegradle.MixinConfigMergingTransformer +import dev.nolij.zumegradle.entryprocessing.EntryProcessors +import dev.nolij.zumegradle.smoketest.SmokeTest.Config +import dev.nolij.zumegradle.task.* import kotlinx.serialization.encodeToString import me.modmuss50.mpp.HttpUtils import me.modmuss50.mpp.PublishModTask @@ -38,13 +43,13 @@ operator fun String.invoke(): String = rootProject.properties[this] as? String ? enum class ReleaseChannel( val suffix: String? = null, val releaseType: ReleaseType? = null, - val deflation: DeflateAlgorithm = DeflateAlgorithm.ZOPFLI, + val deflation: DeflateAlgorithm = DeflateAlgorithm.INSANE, val json: JsonShrinkingType = JsonShrinkingType.MINIFY, val proguard: Boolean = true, ) { DEV_BUILD( suffix = "dev", - deflation = DeflateAlgorithm.SEVENZIP, + deflation = DeflateAlgorithm.EXTRA, json = JsonShrinkingType.PRETTY_PRINT ), PRE_RELEASE("pre"), @@ -183,6 +188,12 @@ allprojects { includeGroup("maven.modrinth") } } + exclusiveContent { + forRepository { maven("https://cursemaven.com") } + filter { + includeGroup("curse.maven") + } + } maven("https://maven.blamejared.com") maven("https://maven.taumc.org/releases") } @@ -264,7 +275,7 @@ subprojects { defaultRemapJar = true } - val outputJar = tasks.register("outputJar") { + val outputJar by tasks.registering(ShadowJar::class) { group = "build" val remapJarTasks = tasks.withType() @@ -354,7 +365,7 @@ tasks.jar { enabled = false } -val sourcesJar = tasks.register("sourcesJar") { +val sourcesJar by tasks.registering(Jar::class) { group = "build" archiveClassifier = "sources" @@ -366,8 +377,8 @@ val sourcesJar = tasks.register("sourcesJar") { } if (releaseChannel.proguard) { - dependsOn(compressJar) - from(compressJar.get().mappingsFile!!) { + dependsOn(proguardJar) + from(proguardJar.get().mappingsFile) { rename { "mappings.txt" } } } @@ -447,23 +458,78 @@ tasks.shadowJar { } } -val compressJar = tasks.register("compressJar") { +//region compressJar +val cjTempDir = layout.buildDirectory.dir("compressJar") +val proguardJar by tasks.registering(ProguardTask::class) { dependsOn(tasks.shadowJar) - group = "build" + inputJar = tasks.shadowJar.get().archiveFile + destinationDirectory = cjTempDir + run = releaseChannel.proguard - val shadowJar = tasks.shadowJar.get() - inputJar = shadowJar.archiveFile.get().asFile - outputJar = shadowJar.archiveFile.get().asFile.let { - it.parentFile.resolve("${it.nameWithoutExtension.removeSuffix("-deobfuscated")}.jar") - } + config(file("proguard.pro")) + + mappingsFile = destinationDirectory.get().asFile + .resolve("${archiveFile.get().asFile.nameWithoutExtension}-mappings.txt") + + jmod("java.base") + jmod("java.desktop") + + classpath.addAll( + uniminedImpls.flatMap { implName -> project(":$implName").unimined.minecrafts.values }.flatMap { mc -> + val prodNamespace = mc.mcPatcher.prodNamespace + + val minecrafts = listOf( + mc.sourceSet.compileClasspath.files, + mc.sourceSet.runtimeClasspath.files + ) + .flatten() + .filter { !mc.isMinecraftJar(it.toPath()) } + .toHashSet() + + mc.mods.getClasspathAs(prodNamespace, prodNamespace, minecrafts) + .filter { it.extension == "jar" && !it.name.startsWith("zume") } + .plus(mc.getMinecraft(prodNamespace, prodNamespace).toFile()) + } + ) - deflateAlgorithm = releaseChannel.deflation - jsonShrinkingType = releaseChannel.json + archiveClassifier = "proguard" +} + +val minifyJar by tasks.registering(JarEntryModificationTask::class) { + dependsOn(proguardJar) + inputJar = proguardJar.get().archiveFile + destinationDirectory = cjTempDir + + archiveClassifier = "minified" + json(releaseChannel.json) { + it.endsWith(".json") || it.endsWith(".mcmeta") || it == "mcmod.info" + } + + process(EntryProcessors.minifyClass { it.desc.startsWith("Ldev/nolij/zumegradle/proguard/") }) + if (releaseChannel.proguard) { - useProguard(uniminedImpls.flatMap { implName -> project(":$implName").unimined.minecrafts.values }) + process(EntryProcessors.obfuscationFixer(proguardJar.get().mappingsFile.get().asFile)) } } +val advzip by tasks.registering(AdvzipTask::class) { + dependsOn(minifyJar) + inputJar = minifyJar.get().archiveFile + destinationDirectory = cjTempDir + + archiveClassifier = "advzip" + level = releaseChannel.deflation +} + +val compressJar by tasks.registering(CopyJarTask::class) { + dependsOn(advzip) + group = "build" + inputJar = advzip.get().archiveFile + + archiveClassifier = null +} +//endregion + tasks.assemble { dependsOn(compressJar, sourcesJar) } @@ -475,89 +541,93 @@ python { pip("portablemc:${"portablemc_version"()}") } -val smokeTest = tasks.register("smokeTest") { - group = "verification" +val smokeTest by tasks.registering(SmokeTestTask::class) { dependsOn(tasks.checkPython, tasks.pipInstall, compressJar) - doFirst { - SmokeTest( - logger, - "${project.rootDir}/.gradle/python/bin/portablemc", - compressJar.get().outputJar.asFile.get(), - "${project.rootDir}/.gradle/portablemc", - "${project.layout.buildDirectory.get()}/smoke_test", - max(2, Runtime.getRuntime().availableProcessors() / 5), - TimeUnit.SECONDS.toNanos(60), - listOf( - SmokeTest.Config("fabric", "snapshot", dependencies = listOf( - "fabric-api" to "https://github.com/FabricMC/fabric/releases/download/0.107.0%2B1.21.4/fabric-api-0.107.0+1.21.4.jar", - )), - SmokeTest.Config("fabric", "release", dependencies = listOf( - "fabric-api" to "https://github.com/FabricMC/fabric/releases/download/0.107.0%2B1.21.3/fabric-api-0.107.0+1.21.3.jar", - "modmenu" to "https://github.com/TerraformersMC/ModMenu/releases/download/v11.0.3/modmenu-11.0.3.jar", - )), - SmokeTest.Config("fabric", "1.21.1", dependencies = listOf( - "fabric-api" to "https://github.com/FabricMC/fabric/releases/download/0.107.0%2B1.21.1/fabric-api-0.107.0+1.21.1.jar", - "modmenu" to "https://github.com/TerraformersMC/ModMenu/releases/download/v11.0.3/modmenu-11.0.3.jar", - )), - SmokeTest.Config("fabric", "1.20.6", dependencies = listOf( - "fabric-api" to "https://github.com/FabricMC/fabric/releases/download/0.100.8%2B1.20.6/fabric-api-0.100.8+1.20.6.jar", - "modmenu" to "https://github.com/TerraformersMC/ModMenu/releases/download/v10.0.0/modmenu-10.0.0.jar", - )), - SmokeTest.Config("fabric", "1.20.1", dependencies = listOf( - "fabric-api" to "https://github.com/FabricMC/fabric/releases/download/0.92.2%2B1.20.1/fabric-api-0.92.2+1.20.1.jar", - "modmenu" to "https://github.com/TerraformersMC/ModMenu/releases/download/v7.2.2/modmenu-7.2.2.jar", - )), - SmokeTest.Config("fabric", "1.18.2", dependencies = listOf( - "fabric-api" to "https://github.com/FabricMC/fabric/releases/download/0.77.0%2B1.18.2/fabric-api-0.77.0+1.18.2.jar", - "modmenu" to "https://github.com/TerraformersMC/ModMenu/releases/download/v3.2.5/modmenu-3.2.5.jar", - ), extraArgs = listOf("--lwjgl=3.2.3")), - SmokeTest.Config("fabric", "1.16.5", dependencies = listOf( - "fabric-api" to "https://github.com/FabricMC/fabric/releases/download/0.42.0%2B1.16/fabric-api-0.42.0+1.16.jar", - "modmenu" to "https://github.com/TerraformersMC/ModMenu/releases/download/v1.16.23/modmenu-1.16.23.jar", - )), - SmokeTest.Config("fabric", "1.14.4", dependencies = listOf( - "fabric-api" to "https://github.com/FabricMC/fabric/releases/download/0.28.5%2B1.14/fabric-api-0.28.5+1.14.jar", - "modmenu" to "https://github.com/TerraformersMC/ModMenu/releases/download/v1.7.11/modmenu-1.7.11+build.121.jar", - )), - SmokeTest.Config("legacyfabric", "1.12.2", dependencies = listOf( - "legacy-fabric-api" to "https://github.com/Legacy-Fabric/fabric/releases/download/1.10.2/legacy-fabric-api-1.10.2.jar", - )), - SmokeTest.Config("legacyfabric", "1.8.9", dependencies = listOf( - "legacy-fabric-api" to "https://github.com/Legacy-Fabric/fabric/releases/download/1.10.2/legacy-fabric-api-1.10.2.jar", - )), - SmokeTest.Config("legacyfabric", "1.7.10", dependencies = listOf( - "legacy-fabric-api" to "https://github.com/Legacy-Fabric/fabric/releases/download/1.10.2/legacy-fabric-api-1.10.2.jar", - )), -// SmokeTest.Config("legacyfabric", "1.6.4", dependencies = listOf( -// "legacy-fabric-api" to "https://github.com/Legacy-Fabric/fabric/releases/download/1.10.2/legacy-fabric-api-1.10.2.jar", -// )), - SmokeTest.Config("babric", "b1.7.3", jvmVersion = 17, dependencies = listOf( - "station-api" to "https://cdn.modrinth.com/data/472oW63Q/versions/W3QVtn6S/StationAPI-2.0-alpha.2.4.jar", - ), extraArgs = listOf("--exclude-lib=asm-all")), - SmokeTest.Config("neoforge", "release"), - SmokeTest.Config("neoforge", "1.21.1"), - SmokeTest.Config("neoforge", "1.20.4"), - SmokeTest.Config("forge", "1.20.4"), - SmokeTest.Config("forge", "1.20.1"), - SmokeTest.Config("forge", "1.19.2"), - SmokeTest.Config("forge", "1.18.2", extraArgs = listOf("--lwjgl=3.2.3")), - SmokeTest.Config("forge", "1.16.5", extraArgs = listOf("--lwjgl=3.2.3")), - SmokeTest.Config("forge", "1.14.4", dependencies = listOf( - "mixinbootstrap" to "https://github.com/LXGaming/MixinBootstrap/releases/download/v1.1.0/_MixinBootstrap-1.1.0.jar" - ), extraArgs = listOf("--lwjgl=3.2.3")), - SmokeTest.Config("forge", "1.12.2", dependencies = listOf( - "mixinbooter" to "https://github.com/CleanroomMC/MixinBooter/releases/download/9.3/mixinbooter-9.3.jar" - )), - SmokeTest.Config("forge", "1.8.9", dependencies = listOf( - "mixinbooter" to "https://github.com/CleanroomMC/MixinBooter/releases/download/9.3/mixinbooter-9.3.jar" - )), - SmokeTest.Config("forge", "1.7.10", dependencies = listOf( - "unimixins" to "https://github.com/LegacyModdingMC/UniMixins/releases/download/0.1.19/+unimixins-all-1.7.10-0.1.19.jar" - )), - ) - ).test() - } + inputTask = compressJar + portableMCBinary = "${python.envPath}/bin/portablemc" + mainDir = "${project.rootDir}/.gradle/portablemc" + workDir = "${project.layout.buildDirectory.get()}/smoke_test" + maxThreads = max(2, Runtime.getRuntime().availableProcessors() / 5) + threadTimeout = TimeUnit.SECONDS.toNanos(60) + + configs( + Config("fabric", "snapshot", dependencies = setOf( + "maven.modrinth:fabric-api:0.107.2+1.21.4", + )), + Config("fabric", "release", dependencies = setOf( + "maven.modrinth:fabric-api:0.107.0+1.21.3", + "maven.modrinth:modmenu:11.0.3", + )), + Config("fabric", "1.21.1", dependencies = setOf( + "maven.modrinth:fabric-api:0.107.0+1.21.1", + "maven.modrinth:modmenu:11.0.3", + )), + Config("fabric", "1.20.6", dependencies = setOf( + "maven.modrinth:fabric-api:0.100.8+1.20.6", + "maven.modrinth:modmenu:10.0.0", + )), + Config("fabric", "1.20.1", dependencies = setOf( + "maven.modrinth:fabric-api:0.92.2+1.20.1", + "maven.modrinth:modmenu:7.2.2", + )), + Config("fabric", "1.18.2", dependencies = setOf( + "maven.modrinth:fabric-api:0.77.0+1.18.2", + "maven.modrinth:modmenu:3.2.5", + "maven.modrinth:lazydfu:0.1.2", + ), extraArgs = listOf("--lwjgl=3.2.3")), + Config("fabric", "1.16.5", dependencies = setOf( + "maven.modrinth:fabric-api:0.42.0+1.16", + "maven.modrinth:modmenu:1.16.23", + "maven.modrinth:lazydfu:0.1.2", + )), + Config("fabric", "1.14.4", dependencies = setOf( + "maven.modrinth:fabric-api:0.28.5+1.14", + "maven.modrinth:modmenu:1.7.17", + "maven.modrinth:lazydfu:0.1.2", + )), + Config("legacyfabric", "1.12.2", dependencies = setOf( + "maven.modrinth:legacy-fabric-api:1.10.2", + )), + Config("legacyfabric", "1.8.9", dependencies = setOf( + "maven.modrinth:legacy-fabric-api:1.10.2", + )), + Config("legacyfabric", "1.7.10", dependencies = setOf( + "maven.modrinth:legacy-fabric-api:1.10.2", + )), +// Config("legacyfabric", "1.6.4", dependencies = setOf( +// "maven.modrinth:legacy-fabric-api:1.10.2", +// )), + Config("babric", "b1.7.3", jvmVersion = 17, dependencies = setOf( + "maven.modrinth:stationapi:2.0-alpha.2.4", + ), extraArgs = listOf("--exclude-lib=asm-all")), + Config("neoforge", "release"), + Config("neoforge", "1.21.1"), + Config("neoforge", "1.20.4"), + Config("forge", "1.20.4"), + Config("forge", "1.20.1"), + Config("forge", "1.19.2", dependencies = setOf( + "curse.maven:lazydfu-460819:4327266" + )), + Config("forge", "1.18.2", dependencies = setOf( + "curse.maven:lazuyfu-460819:3544496" + ), extraArgs = listOf("--lwjgl=3.2.3")), + Config("forge", "1.16.5", dependencies = setOf( + "curse.maven:lazydfu-460819:3249059" + ), extraArgs = listOf("--lwjgl=3.2.3")), + Config("forge", "1.14.4", dependencies = setOf( + "maven.modrinth:mixinbootstrap:1.1.0" + ), extraArgs = listOf("--lwjgl=3.2.3")), + Config("forge", "1.12.2", dependencies = setOf( + "maven.modrinth:mixinbooter:9.3" + )), + Config("forge", "1.8.9", dependencies = setOf( + "maven.modrinth:mixinbooter:9.3" + )), + Config("forge", "1.7.10", dependencies = setOf( + "maven.modrinth:unimixins:1.7.10-0.1.19" + )), + ) } //endregion @@ -570,7 +640,7 @@ afterEvaluate { publications { create("mod_id"()) { - artifact(compressJar.get().outputJar) + artifact(compressJar.get().archiveFile) artifact(sourcesJar) } } @@ -585,7 +655,7 @@ afterEvaluate { } publishMods { - file = compressJar.get().outputJar + file = compressJar.get().archiveFile additionalFiles.from(sourcesJar.get().archiveFile) type = releaseChannel.releaseType ?: ALPHA displayName = Zume.version diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts index e9be815..3207865 100644 --- a/buildSrc/build.gradle.kts +++ b/buildSrc/build.gradle.kts @@ -15,6 +15,12 @@ repositories { } } +kotlin { + jvmToolchain { + languageVersion = JavaLanguageVersion.of(21) + } +} + fun DependencyHandler.plugin(id: String, version: String) { this.implementation(group = id, name = "$id.gradle.plugin", version = version) } diff --git a/buildSrc/src/main/kotlin/dev/nolij/zumegradle/DeflateAlgorithm.kt b/buildSrc/src/main/kotlin/dev/nolij/zumegradle/DeflateAlgorithm.kt new file mode 100644 index 0000000..255f6f7 --- /dev/null +++ b/buildSrc/src/main/kotlin/dev/nolij/zumegradle/DeflateAlgorithm.kt @@ -0,0 +1,33 @@ +package dev.nolij.zumegradle + +import org.gradle.kotlin.dsl.support.uppercaseFirstChar + +enum class DeflateAlgorithm(val id: Int?) { + + /** + * Entries are stored without compression + */ + STORE(0), + + /** + * Entries are stored with Zlib + */ + FAST(1), + + /** + * Entries are stored with libdeflate + */ + NORMAL(2), + + /** + * Entries are stored with 7zip + */ + EXTRA(3), + + /** + * Entries are stored with Zopfli + */ + INSANE(4); + + override fun toString() = name.lowercase().uppercaseFirstChar() +} \ No newline at end of file diff --git a/buildSrc/src/main/kotlin/dev/nolij/zumegradle/JarCompressing.kt b/buildSrc/src/main/kotlin/dev/nolij/zumegradle/JarCompressing.kt deleted file mode 100644 index 3cf1018..0000000 --- a/buildSrc/src/main/kotlin/dev/nolij/zumegradle/JarCompressing.kt +++ /dev/null @@ -1,345 +0,0 @@ -package dev.nolij.zumegradle - -import groovy.json.JsonOutput -import groovy.json.JsonSlurper -import net.fabricmc.mappingio.MappingReader -import net.fabricmc.mappingio.format.MappingFormat -import net.fabricmc.mappingio.tree.MappingTree.ClassMapping -import net.fabricmc.mappingio.tree.MemoryMappingTree -import org.gradle.api.DefaultTask -import org.gradle.api.file.RegularFileProperty -import org.gradle.api.tasks.* -import org.gradle.api.tasks.options.Option -import org.gradle.kotlin.dsl.support.uppercaseFirstChar -import org.objectweb.asm.ClassReader -import org.objectweb.asm.ClassWriter -import org.objectweb.asm.tree.AnnotationNode -import org.objectweb.asm.tree.ClassNode -import proguard.Configuration -import proguard.ConfigurationParser -import proguard.ProGuard -import xyz.wagyourtail.unimined.api.minecraft.MinecraftConfig -import java.io.File -import java.util.Properties -import java.util.jar.JarEntry -import java.util.jar.JarFile -import java.util.jar.JarOutputStream -import java.util.zip.Deflater -import kotlin.collections.HashSet - -enum class DeflateAlgorithm(val id: Int?) { - NONE(null), - LIBDEFLATE(2), - SEVENZIP(3), - ZOPFLI(4), // too slow - ; - - override fun toString() = name.lowercase().uppercaseFirstChar() -} - -enum class JsonShrinkingType { - NONE, MINIFY, PRETTY_PRINT -} - -fun squishJar(jar: File, jsonProcessing: JsonShrinkingType, mappingsFile: File?, isObfuscating: Boolean = false) { - val contents = linkedMapOf() - JarFile(jar).use { - it.entries().asIterator().forEach { entry -> - if (!entry.isDirectory) { - contents[entry.name] = it.getInputStream(entry).readAllBytes() - } - } - } - - jar.delete() - - val json = JsonSlurper() - - val mappings = if (isObfuscating) mappings(mappingsFile!!) else null - - JarOutputStream(jar.outputStream()).use { out -> - out.setLevel(Deflater.BEST_COMPRESSION) - contents.forEach { var (name, bytes) = it - - if (name.endsWith("mixins.json") && isObfuscating) { - bytes = remapMixinConfig(bytes, mappings!!) - } - - if (jsonProcessing != JsonShrinkingType.NONE && - name.endsWith(".json") || name.endsWith(".mcmeta") || name == "mcmod.info" - ) { - bytes = when (jsonProcessing) { - JsonShrinkingType.MINIFY -> JsonOutput.toJson(json.parse(bytes)).toByteArray() - JsonShrinkingType.PRETTY_PRINT -> JsonOutput.prettyPrint(JsonOutput.toJson(json.parse(bytes))) - .toByteArray() - - else -> throw AssertionError() - } - } - - if (name.endsWith(".class")) { - bytes = processClassFile(bytes, mappings) - } - - out.putNextEntry(JarEntry(name)) - out.write(bytes) - out.closeEntry() - } - out.finish() - } -} - -@Suppress("UNCHECKED_CAST") -private fun remapMixinConfig(bytes: ByteArray, mappings: MemoryMappingTree): ByteArray { - val json = (JsonSlurper().parse(bytes) as Map).toMutableMap() - val old = json["plugin"] as String - val obf = mappings.map(old) - json["plugin"] = obf - - json["package"] = "zume.mixin" - - return JsonOutput.toJson(json).toByteArray() -} - -private fun processClassFile(bytes: ByteArray, mappings: MemoryMappingTree?): ByteArray { - val classNode = ClassNode() - ClassReader(bytes).accept(classNode, 0) - - if(mappings != null) { - for (annotation in classNode.visibleAnnotations ?: emptyList()) { - if (annotation.desc.endsWith("fml/common/Mod;")) { - for (i in 0 until annotation.values.size step 2) { - if (annotation.values[i] == "guiFactory") { - val old = annotation.values[i + 1] as String - annotation.values[i + 1] = mappings.map(old) - println("Remapped guiFactory: $old -> ${annotation.values[i + 1]}") - } - } - } - } - } - - val strippableAnnotations = setOf( - "Lorg/spongepowered/asm/mixin/Dynamic;", - "Lorg/spongepowered/asm/mixin/Final;", - "Ljava/lang/SafeVarargs;", - ) - val canStripAnnotation = { annotationNode: AnnotationNode -> - annotationNode.desc.startsWith("Ldev/nolij/zumegradle/proguard/") || - annotationNode.desc.startsWith("Lorg/jetbrains/annotations/") || - strippableAnnotations.contains(annotationNode.desc) - } - - classNode.invisibleAnnotations?.removeIf(canStripAnnotation) - classNode.visibleAnnotations?.removeIf(canStripAnnotation) - classNode.invisibleTypeAnnotations?.removeIf(canStripAnnotation) - classNode.visibleTypeAnnotations?.removeIf(canStripAnnotation) - classNode.fields.forEach { fieldNode -> - fieldNode.invisibleAnnotations?.removeIf(canStripAnnotation) - fieldNode.visibleAnnotations?.removeIf(canStripAnnotation) - fieldNode.invisibleTypeAnnotations?.removeIf(canStripAnnotation) - fieldNode.visibleTypeAnnotations?.removeIf(canStripAnnotation) - } - classNode.methods.forEach { methodNode -> - methodNode.invisibleAnnotations?.removeIf(canStripAnnotation) - methodNode.visibleAnnotations?.removeIf(canStripAnnotation) - methodNode.invisibleTypeAnnotations?.removeIf(canStripAnnotation) - methodNode.visibleTypeAnnotations?.removeIf(canStripAnnotation) - methodNode.invisibleLocalVariableAnnotations?.removeIf(canStripAnnotation) - methodNode.visibleLocalVariableAnnotations?.removeIf(canStripAnnotation) - methodNode.invisibleParameterAnnotations?.forEach { parameterAnnotations -> - parameterAnnotations?.removeIf(canStripAnnotation) - } - methodNode.visibleParameterAnnotations?.forEach { parameterAnnotations -> - parameterAnnotations?.removeIf(canStripAnnotation) - } - } - - if (classNode.invisibleAnnotations?.any { it.desc == "Lorg/spongepowered/asm/mixin/Mixin;" } == true) { - classNode.methods.removeAll { it.name == "" && it.instructions.size() <= 3 } // ALOAD, super(), RETURN - } - - val writer = ClassWriter(0) - classNode.accept(writer) - return writer.toByteArray() -} - -val advzipInstalled = try { - ProcessBuilder("advzip", "-V").start().waitFor() == 0 -} catch (e: Exception) { - false -} - -fun deflate(zip: File, type: DeflateAlgorithm) { - if (type == DeflateAlgorithm.NONE) return - if (!advzipInstalled) { - println("advzip is not installed; skipping re-deflation of $zip") - return - } - - try { - val process = ProcessBuilder("advzip", "-z", "-${type.id}", zip.absolutePath).start() - val exitCode = process.waitFor() - if (exitCode != 0) { - error("Failed to compress $zip with $type") - } - } catch (e: Exception) { - error("Failed to compress $zip with $type: ${e.message}") - } -} - -val JAVA_HOME = System.getProperty("java.home") - -@Suppress("UnstableApiUsage") -fun applyProguard(jar: File, minecraftConfigs: List, configDir: File, mappingsFile: File? = null) { - val inputJar = jar.copyTo( - jar.parentFile.resolve(".${jar.nameWithoutExtension}_proguardRunning.jar"), true - ).also { - it.deleteOnExit() - } - - val config = configDir.resolve("proguard.pro") - if (!config.exists()) { - error("proguard.pro not found") - } - val proguardCommand = mutableListOf( - "@${config.absolutePath}", - "-injars", inputJar.absolutePath, - "-outjars", jar.absolutePath, - ) - - if (mappingsFile != null) { - proguardCommand.add("-printmapping") - proguardCommand.add(mappingsFile.absolutePath) - } - - val libraries = HashSet() - libraries.add("${JAVA_HOME}/jmods/java.base.jmod") - libraries.add("${JAVA_HOME}/jmods/java.desktop.jmod") - - for (minecraftConfig in minecraftConfigs) { - val prodNamespace = minecraftConfig.mcPatcher.prodNamespace - - libraries.add(minecraftConfig.getMinecraft(prodNamespace, prodNamespace).toFile().absolutePath) - - val minecrafts = listOf( - minecraftConfig.sourceSet.compileClasspath.files, - minecraftConfig.sourceSet.runtimeClasspath.files - ) - .flatten() - .filter { it: File -> !minecraftConfig.isMinecraftJar(it.toPath()) } - .toHashSet() - - libraries += minecraftConfig.mods.getClasspathAs(prodNamespace, prodNamespace, minecrafts) - .filter { it.extension == "jar" && !it.name.startsWith("zume") } - .map { it.absolutePath } - } - - val debug = Properties().apply { - val gradleproperties = configDir.resolve("gradle.properties") - if (gradleproperties.exists()) { - load(gradleproperties.inputStream()) - } - }.getProperty("zumegradle.proguard.keepAttrs").toBoolean() - - if (debug) { - proguardCommand.add("-keepattributes") - proguardCommand.add("*Annotation*,SourceFile,MethodParameters,L*Table") - proguardCommand.add("-dontobfuscate") - } - - proguardCommand.add("-libraryjars") - proguardCommand.add(libraries.joinToString(File.pathSeparator) { "\"$it\"" }) - - val configuration = Configuration() - ConfigurationParser(proguardCommand.toTypedArray(), System.getProperties()) - .parse(configuration) - - try { - ProGuard(configuration).execute() - } catch (ex: Exception) { - throw IllegalStateException("ProGuard failed for $jar", ex) - } finally { - inputJar.delete() - } -} - -abstract class CompressJarTask : DefaultTask() { - @get:InputFile - @get:PathSensitive(PathSensitivity.RELATIVE) - abstract val inputJar: RegularFileProperty - - @Input - var deflateAlgorithm = DeflateAlgorithm.LIBDEFLATE - - @Input - var jsonShrinkingType = JsonShrinkingType.NONE - - @get:Input - val useProguard get() = this.minecraftConfigs.isNotEmpty() - - private var minecraftConfigs: List = emptyList() - - @get:OutputFile - abstract val outputJar: RegularFileProperty - - @get:OutputFile - @get:Optional - val mappingsFile - get() = if (useProguard) - inputJar.get().asFile.let { - it.parentFile.resolve("${it.nameWithoutExtension.removeSuffix("-deobfuscated")}-mappings.txt") - } - else null - - @Option(option = "compression-type", description = "How to recompress the jar") - fun setDeflateAlgorithm(value: String) { - deflateAlgorithm = value.uppercase().let { - if (it.matches(Regex("7Z(?:IP)?"))) DeflateAlgorithm.SEVENZIP - else DeflateAlgorithm.valueOf(it) - } - } - - @Option(option = "json-processing", description = "How to process json files") - fun setJsonShrinkingType(value: String) { - jsonShrinkingType = JsonShrinkingType.valueOf(value.uppercase()) - } - - fun useProguard(minecraftConfigs: List) { - this.minecraftConfigs = minecraftConfigs - } - - @TaskAction - fun compressJar() { - val jar = inputJar.get().asFile - val temp = jar.copyTo(temporaryDir.resolve("temp.jar"), true) - if(useProguard) { - applyProguard(temp, minecraftConfigs, project.rootDir, mappingsFile) - } - squishJar(temp, jsonShrinkingType, mappingsFile, useProguard) - deflate(temp, deflateAlgorithm) - temp.copyTo(outputJar.get().asFile, true) - } -} - -fun mappings(file: File, format: MappingFormat = MappingFormat.PROGUARD): MemoryMappingTree { - if (!file.exists()) { - error("Mappings file $file does not exist") - } - - val mappingTree = MemoryMappingTree() - MappingReader.read(file.toPath(), format, mappingTree) - return mappingTree -} - -@Suppress("INACCESSIBLE_TYPE", "NAME_SHADOWING") -fun MemoryMappingTree.map(src: String): String { - val src = src.replace('.', '/') - val dstNamespaceIndex = getNamespaceId(dstNamespaces[0]) - val classMapping: ClassMapping? = getClass(src) - if (classMapping == null) { - println("Class $src not found in mappings") - return src - } - return classMapping.getDstName(dstNamespaceIndex).replace('/', '.') -} \ No newline at end of file diff --git a/buildSrc/src/main/kotlin/dev/nolij/zumegradle/JsonShrinkingType.kt b/buildSrc/src/main/kotlin/dev/nolij/zumegradle/JsonShrinkingType.kt new file mode 100644 index 0000000..d05cea7 --- /dev/null +++ b/buildSrc/src/main/kotlin/dev/nolij/zumegradle/JsonShrinkingType.kt @@ -0,0 +1,5 @@ +package dev.nolij.zumegradle + +enum class JsonShrinkingType { + MINIFY, PRETTY_PRINT +} \ No newline at end of file diff --git a/buildSrc/src/main/kotlin/dev/nolij/zumegradle/entryprocessing/EntryProcessors.kt b/buildSrc/src/main/kotlin/dev/nolij/zumegradle/entryprocessing/EntryProcessors.kt new file mode 100644 index 0000000..8f85d5c --- /dev/null +++ b/buildSrc/src/main/kotlin/dev/nolij/zumegradle/entryprocessing/EntryProcessors.kt @@ -0,0 +1,118 @@ +package dev.nolij.zumegradle.entryprocessing + +import dev.nolij.zumegradle.util.map +import dev.nolij.zumegradle.util.mappings +import groovy.json.JsonOutput +import groovy.json.JsonSlurper +import net.fabricmc.mappingio.format.MappingFormat +import org.objectweb.asm.tree.AnnotationNode +import java.io.File + +import dev.nolij.zumegradle.util.toBytes +import dev.nolij.zumegradle.util.ClassNode + +typealias EntryProcessor = (String, ByteArray) -> ByteArray + +object EntryProcessors { + val PASS: EntryProcessor = { _, bytes -> bytes } + + fun jsonMinifier(shouldRun: (String) -> Boolean = { it.endsWith(".json") }): EntryProcessor = { name, bytes -> + if (shouldRun(name)) { + val json = String(bytes) + val minified = JsonSlurper().parseText(json).toString() + minified.toByteArray() + } else { + bytes + } + } + + fun jsonPrettyPrinter(shouldRun: (String) -> Boolean = { it.endsWith(".json") }): EntryProcessor = { name, bytes -> + if (shouldRun(name)) { + JsonOutput.prettyPrint(String(bytes)).toByteArray() + } else { + bytes + } + } + + @Suppress("UNCHECKED_CAST") + fun obfuscationFixer(mappingsFile: File, format: MappingFormat = MappingFormat.PROGUARD): EntryProcessor = { name, bytes -> + val mappings = mappings(mappingsFile, format) + if (name.endsWith("mixins.json")) { + val json = (JsonSlurper().parse(bytes) as Map).toMutableMap() + json["plugin"] = mappings.map(json["plugin"] as String) + + json["package"] = "zume.mixin" // TODO: make this configurable + + JsonOutput.toJson(json).toByteArray() + } else if (name.endsWith(".class")) { + val classNode = ClassNode(bytes) + + for (annotation in classNode.visibleAnnotations ?: emptyList()) { + if (annotation.desc.endsWith("fml/common/Mod;")) { + for (i in 0 until annotation.values.size step 2) { + if (annotation.values[i] == "guiFactory") { + val old = annotation.values[i + 1] as String + annotation.values[i + 1] = mappings.map(old) + println("Remapped guiFactory: $old -> ${annotation.values[i + 1]}") + } + } + } + } + + classNode.toBytes() + } else { + bytes + } + } + + fun minifyClass(runOnEverything: Boolean = false, extraAnnotationsToStrip: (AnnotationNode) -> Boolean): EntryProcessor = a@{ name, bytes -> + if(!name.endsWith(".class") && !runOnEverything) { + return@a bytes + } + + val shouldStripAnnotation: (AnnotationNode) -> Boolean = { + setOf( + "Lorg/spongepowered/asm/mixin/Dynamic;", + "Lorg/spongepowered/asm/mixin/Final;", + "Ljava/lang/SafeVarargs;", + ).contains(it.desc) + || it.desc.startsWith("Lorg/jetbrains/annotations/") + || extraAnnotationsToStrip(it) + } + val classNode = ClassNode(bytes) + + classNode.invisibleAnnotations?.removeIf(shouldStripAnnotation) + classNode.visibleAnnotations?.removeIf(shouldStripAnnotation) + classNode.invisibleTypeAnnotations?.removeIf(shouldStripAnnotation) + classNode.visibleTypeAnnotations?.removeIf(shouldStripAnnotation) + classNode.fields.forEach { + it.invisibleAnnotations?.removeIf(shouldStripAnnotation) + it.visibleAnnotations?.removeIf(shouldStripAnnotation) + it.invisibleTypeAnnotations?.removeIf(shouldStripAnnotation) + it.visibleTypeAnnotations?.removeIf(shouldStripAnnotation) + } + classNode.methods.forEach { + it.invisibleAnnotations?.removeIf(shouldStripAnnotation) + it.visibleAnnotations?.removeIf(shouldStripAnnotation) + it.invisibleTypeAnnotations?.removeIf(shouldStripAnnotation) + it.visibleTypeAnnotations?.removeIf(shouldStripAnnotation) + it.invisibleLocalVariableAnnotations?.removeIf(shouldStripAnnotation) + it.visibleLocalVariableAnnotations?.removeIf(shouldStripAnnotation) + it.invisibleParameterAnnotations?.forEach { parameterAnnotations -> + parameterAnnotations?.removeIf(shouldStripAnnotation) + } + it.visibleParameterAnnotations?.forEach { parameterAnnotations -> + parameterAnnotations?.removeIf(shouldStripAnnotation) + } + } + + if (classNode.invisibleAnnotations?.any { it.desc == "Lorg/spongepowered/asm/mixin/Mixin;" } == true) { + // remove any empty constructors that just call super() + // since these classes are never loaded, they are not needed + // 3 instructions are ALOAD + call to super() + RETURN + classNode.methods.removeAll { it.name == "" && it.instructions.size() <= 3 } + } + + classNode.toBytes() + } +} \ No newline at end of file diff --git a/buildSrc/src/main/kotlin/dev/nolij/zumegradle/SmokeTest.kt b/buildSrc/src/main/kotlin/dev/nolij/zumegradle/smoketest/SmokeTest.kt similarity index 71% rename from buildSrc/src/main/kotlin/dev/nolij/zumegradle/SmokeTest.kt rename to buildSrc/src/main/kotlin/dev/nolij/zumegradle/smoketest/SmokeTest.kt index d787e43..8990ff3 100644 --- a/buildSrc/src/main/kotlin/dev/nolij/zumegradle/SmokeTest.kt +++ b/buildSrc/src/main/kotlin/dev/nolij/zumegradle/smoketest/SmokeTest.kt @@ -1,14 +1,8 @@ -@file:OptIn(ExperimentalPathApi::class) +package dev.nolij.zumegradle.smoketest -package dev.nolij.zumegradle - -import org.gradle.api.logging.Logger +import org.gradle.api.Project +import xyz.wagyourtail.unimined.util.cachingDownload import java.io.File -import java.io.FileOutputStream -import java.net.URL -import java.nio.file.Files -import java.util.* -import kotlin.io.path.* import kotlin.math.max fun sleep(millis: Long) { @@ -16,7 +10,7 @@ fun sleep(millis: Long) { } class SmokeTest( - private val logger: Logger, + private val project: Project, private val portableMCBinary: String, private val modFile: File, private val mainDir: String, @@ -31,8 +25,8 @@ class SmokeTest( val mcVersion: String, val loaderVersion: String? = null, val jvmVersion: Int? = null, - val extraArgs: List? = null, - val dependencies: List>? = null, + val extraArgs: List = emptyList(), + val dependencies: Set = emptySet(), ) { val name: String = hashCode().toUInt().toString(16) @@ -50,8 +44,8 @@ class SmokeTest( result.appendLine("mcVersion=${mcVersion}") result.appendLine("loaderVersion=${loaderVersion}") result.appendLine("jvmVersion=${jvmVersion}") - result.appendLine("extraArgs=[${extraArgs?.joinToString(", ") ?: ""}]") - result.appendLine("mods=[${dependencies?.joinToString(", ") { (name, _) -> name } ?: ""}]") + result.appendLine("extraArgs=[${extraArgs.joinToString(", ")}]") + result.append("mods=[${dependencies.joinToString(", ") { it.split(":")[1] }}]") return result.toString() } @@ -71,12 +65,12 @@ class SmokeTest( } private inner class Thread(val config: Config) { - val instancePath = "${workDir}/${config.name}" - val modsPath = "${instancePath}/mods" + val instancePath = File(workDir, config.name) + val modsPath = instancePath.resolve("mods") val command: Array - val setupLogFile = File("${instancePath}/setup.log") - val testLogFile = File("${instancePath}/test.log") - val gameLogFile = File("${instancePath}/logs/latest.log") + val setupLogFile = instancePath.resolve("setup.log") + val testLogFile = instancePath.resolve("test.log") + val gameLogFile = instancePath.resolve("logs/latest.log") private var process: Process? = null private var startTimestamp: Long? = null @@ -98,45 +92,49 @@ class SmokeTest( val done: Boolean get() = failed || stage == ThreadStage.COMPLETED init { - Path(instancePath).also { path -> - if (!path.exists()) - path.createDirectories() + if (!instancePath.exists()) { + instancePath.mkdirs() } - Path(modsPath).also { modsPath -> - if (modsPath.exists()) - modsPath.deleteRecursively() - modsPath.createDirectories() - } + if (modsPath.exists()) + modsPath.deleteRecursively() + modsPath.mkdirs() if (gameLogFile.exists()) gameLogFile.delete() - config.dependencies?.forEach { (name, urlString) -> - URL(urlString).openStream().use { inputStream -> - FileOutputStream("${modsPath}/${name}.jar").use(inputStream::transferTo) - } + val urlDeps = config.dependencies.filter { it.matches(Regex("https?://.*")) }.toSet() + val mavenDeps = (config.dependencies - urlDeps).toSet() + + val files = project.configurations.detachedConfiguration( + *mavenDeps.map { project.dependencies.create(it) } + .toTypedArray() + ).resolve() + urlDeps.map { project.cachingDownload(it).toFile() } + + files.forEach { file -> + file.copyTo(modsPath.resolve(file.name), overwrite = true) } - Files.copy(modFile.toPath(), Path("${modsPath}/${modFile.name}")) + modFile.copyTo(modsPath.resolve(modFile.name), overwrite = true) - val extraArgs = arrayListOf() + val extraArgs = mutableListOf() val jvmVersionMap = mapOf( 17 to "java-runtime-gamma", 21 to "java-runtime-delta", 8 to "jre-legacy" ) - if (config.jvmVersion != null) - extraArgs.add("--jvm=${mainDir}/jvm/${jvmVersionMap[config.jvmVersion]!!}/bin/java") + if (config.jvmVersion != null) { + val vmName = jvmVersionMap[config.jvmVersion] ?: error("Invalid JVM version: ${config.jvmVersion}") + extraArgs.add("--jvm=${mainDir}/jvm/${vmName}/bin/java") + } - if (config.extraArgs != null) - extraArgs.addAll(config.extraArgs) + extraArgs.addAll(config.extraArgs) command = arrayOf( portableMCBinary, "--main-dir", mainDir, - "--work-dir", instancePath, + "--work-dir", instancePath.absolutePath, "start", config.versionString, *extraArgs.toTypedArray(), "--jvm-args=-DzumeGradle.auditAndExit=true -Xmx1G", @@ -197,7 +195,7 @@ class SmokeTest( if (passed) { println("Smoke test passed for config:\n${config}") } else { - logger.error("Smoke test failed for config:\n${config}") + project.logger.error("Smoke test failed for config:\n${config}") } printThreads() @@ -237,14 +235,16 @@ class SmokeTest( if (failedThreads.isNotEmpty()) { failedThreads.forEach { thread -> - logger.error( - "Config ${thread.config.name} failed!\n" + - "> STAGE: ${thread.stage}\n" + - "> CONFIG: {\n${thread.config}}\n" + - "> COMMAND: [${thread.command.joinToString(", ")}]\n" + - "> FAILURE REASON: ${thread.failureReason}\n" + - "> INSTANCE PATH: ${thread.instancePath}\n" - ) + project.logger.error(listOf( + "Config ${thread.config.name} failed!", + "> STAGE: ${thread.stage}", + "> CONFIG: {", + thread.config.toString().prependIndent("\t"), + "}", + "> COMMAND: [${thread.command.joinToString(", ")}]", + "> FAILURE REASON: ${thread.failureReason}", + "> INSTANCE PATH: ${thread.instancePath}" + ).joinToString("\n")) } error("${failedThreads.size} smoke test config(s) failed. See logs for more details.") } diff --git a/buildSrc/src/main/kotlin/dev/nolij/zumegradle/task/AdvzipTask.kt b/buildSrc/src/main/kotlin/dev/nolij/zumegradle/task/AdvzipTask.kt new file mode 100644 index 0000000..fcf8a96 --- /dev/null +++ b/buildSrc/src/main/kotlin/dev/nolij/zumegradle/task/AdvzipTask.kt @@ -0,0 +1,44 @@ +package dev.nolij.zumegradle.task + +import dev.nolij.zumegradle.DeflateAlgorithm +import org.gradle.api.provider.Property +import org.gradle.api.tasks.Input + +@Suppress("LeakingThis") +abstract class AdvzipTask : ProcessJarTask() { + @get:Input + abstract val level: Property + + @get:Input + abstract val throwIfNotInstalled: Property + + init { + throwIfNotInstalled.convention(false) + } + + override fun process() { + if(try { + ProcessBuilder("advzip", "-V").start().waitFor() != 0 + } catch (e: Exception) { true }) { + if(throwIfNotInstalled.get()) { + throw IllegalStateException("advzip is not installed") + } + + println("advzip is not installed, skipping ${this.name}") + return + } + + val jar = inputJar.get().asFile.copyTo(archiveFile.get().asFile, true) + val type = level.get() + + try { + val process = ProcessBuilder("advzip", "-z", "-${type.id}", jar.absolutePath).start() + val exitCode = process.waitFor() + if (exitCode != 0) { + error("Failed to compress $jar with $type") + } + } catch (e: Exception) { + error("Failed to compress $jar with $type: ${e.message}") + } + } +} \ No newline at end of file diff --git a/buildSrc/src/main/kotlin/dev/nolij/zumegradle/task/CopyJarTask.kt b/buildSrc/src/main/kotlin/dev/nolij/zumegradle/task/CopyJarTask.kt new file mode 100644 index 0000000..04fe607 --- /dev/null +++ b/buildSrc/src/main/kotlin/dev/nolij/zumegradle/task/CopyJarTask.kt @@ -0,0 +1,7 @@ +package dev.nolij.zumegradle.task + +abstract class CopyJarTask : ProcessJarTask() { + override fun process() { + inputJar.get().asFile.copyTo(archiveFile.get().asFile, true) + } +} \ No newline at end of file diff --git a/buildSrc/src/main/kotlin/dev/nolij/zumegradle/task/JarEntryModificationTask.kt b/buildSrc/src/main/kotlin/dev/nolij/zumegradle/task/JarEntryModificationTask.kt new file mode 100644 index 0000000..8e2089a --- /dev/null +++ b/buildSrc/src/main/kotlin/dev/nolij/zumegradle/task/JarEntryModificationTask.kt @@ -0,0 +1,54 @@ +package dev.nolij.zumegradle.task + +import dev.nolij.zumegradle.JsonShrinkingType +import org.gradle.api.tasks.Input +import dev.nolij.zumegradle.entryprocessing.EntryProcessor +import dev.nolij.zumegradle.entryprocessing.EntryProcessors +import org.gradle.api.provider.ListProperty +import java.util.jar.JarEntry +import java.util.jar.JarFile +import java.util.jar.JarOutputStream +import java.util.zip.Deflater + +abstract class JarEntryModificationTask : ProcessJarTask() { + @get:Input + protected abstract val processors: ListProperty + + fun process(processor: EntryProcessor) { + processors.add(processor) + } + + fun json(type: JsonShrinkingType?, shouldRun: (String) -> Boolean = { it.endsWith(".json") }) { + processors.add(when(type) { + null -> EntryProcessors.PASS + JsonShrinkingType.MINIFY -> EntryProcessors.jsonMinifier(shouldRun) + JsonShrinkingType.PRETTY_PRINT -> EntryProcessors.jsonPrettyPrinter(shouldRun) + }) + } + + override fun process() { + val contents = linkedMapOf() + JarFile(inputJar.get().asFile).use { + it.entries().asIterator().forEach { entry -> + if (!entry.isDirectory) { + contents[entry.name] = it.getInputStream(entry).readAllBytes() + } + } + } + + JarOutputStream(archiveFile.get().asFile.outputStream()).use { out -> + out.setLevel(Deflater.BEST_COMPRESSION) + contents.forEach { var (name, bytes) = it + + processors.get().forEach { processor -> + bytes = processor(name, bytes) + } + + out.putNextEntry(JarEntry(name)) + out.write(bytes) + out.closeEntry() + } + out.finish() + } + } +} \ No newline at end of file diff --git a/buildSrc/src/main/kotlin/dev/nolij/zumegradle/task/ProcessJarTask.kt b/buildSrc/src/main/kotlin/dev/nolij/zumegradle/task/ProcessJarTask.kt new file mode 100644 index 0000000..3cc11cb --- /dev/null +++ b/buildSrc/src/main/kotlin/dev/nolij/zumegradle/task/ProcessJarTask.kt @@ -0,0 +1,35 @@ +package dev.nolij.zumegradle.task + +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.provider.Property +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.InputFile +import org.gradle.api.tasks.PathSensitive +import org.gradle.api.tasks.PathSensitivity +import org.gradle.jvm.tasks.Jar + +@Suppress("LeakingThis") +abstract class ProcessJarTask : Jar() { + @get:InputFile + @get:PathSensitive(PathSensitivity.RELATIVE) + abstract val inputJar: RegularFileProperty + + @get:Input + abstract val ignoreSameInputOutput: Property + + init { + ignoreSameInputOutput.convention(false) + + group = "processing" + } + + override fun copy() { + if(!ignoreSameInputOutput.get() && inputJar.get().asFile.equals(archiveFile.get().asFile)) { + throw IllegalStateException("Input jar and output jar are the same file; this breaks caching" + + "\nTo ignore this, set ignoreSameInputOutput to true") + } + process() + } + + protected abstract fun process() +} \ No newline at end of file diff --git a/buildSrc/src/main/kotlin/dev/nolij/zumegradle/task/ProguardTask.kt b/buildSrc/src/main/kotlin/dev/nolij/zumegradle/task/ProguardTask.kt new file mode 100644 index 0000000..51e35c1 --- /dev/null +++ b/buildSrc/src/main/kotlin/dev/nolij/zumegradle/task/ProguardTask.kt @@ -0,0 +1,87 @@ +package dev.nolij.zumegradle.task + +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.provider.ListProperty +import org.gradle.api.provider.Property +import org.gradle.api.tasks.* +import org.gradle.api.tasks.Optional +import proguard.Configuration +import proguard.ConfigurationParser +import proguard.ProGuard +import java.io.File +import java.util.* + +private val javaHome = System.getProperty("java.home")!! + +abstract class ProguardTask : ProcessJarTask() { + + @get:InputFiles + @get:Classpath + abstract val classpath: ListProperty + + @get:Input + abstract val options: ListProperty + + @get:Input + abstract val run: Property + + @get:OutputFile + @get:Optional + abstract val mappingsFile: RegularFileProperty + + fun config(config: File) { + options.add("@${config.relativeTo(project.rootDir)}") + } + + fun jmod(jmod: String) { + classpath.add(File("$javaHome/jmods/$jmod.jmod")) + } + + override fun process() { + if(!run.get()) { + inputJar.get().asFile.copyTo(archiveFile.get().asFile, true) + return + } + + val cmd = this.options.get().toMutableSet() + + cmd.addAll(arrayOf( + "-injars", inputJar.get().asFile.absolutePath, + "-outjars", archiveFile.get().asFile.absolutePath + )) + + if (mappingsFile.isPresent) { + cmd.add("-printmapping") + cmd.add(mappingsFile.get().asFile.absolutePath) + } + + cmd.addAll(arrayOf( + "-libraryjars", classpath.get().toSet().joinToString(File.pathSeparator) { "\"$it\"" } + )) + + val debug = Properties().apply { + val gradleproperties = project.rootDir.resolve("gradle.properties") + if (gradleproperties.exists()) { + load(gradleproperties.inputStream()) + } + }.getProperty("zumegradle.proguard.keepAttrs")?.toBoolean() ?: false + + if (debug) { + cmd.add("-keepattributes") + cmd.add("*Annotation*,SourceFile,MethodParameters,L*Table") + cmd.add("-dontobfuscate") + } + + project.logger.debug("Proguard command: {}", cmd) + + val configuration = Configuration() + ConfigurationParser(cmd.toTypedArray(), System.getProperties()) + .parse(configuration) + + try { + ProGuard(configuration).execute() + } catch (ex: Exception) { + throw IllegalStateException("ProGuard failed for task ${this.name}", ex) + } + } +} \ No newline at end of file diff --git a/buildSrc/src/main/kotlin/dev/nolij/zumegradle/task/SmokeTestTask.kt b/buildSrc/src/main/kotlin/dev/nolij/zumegradle/task/SmokeTestTask.kt new file mode 100644 index 0000000..5f659b7 --- /dev/null +++ b/buildSrc/src/main/kotlin/dev/nolij/zumegradle/task/SmokeTestTask.kt @@ -0,0 +1,62 @@ +package dev.nolij.zumegradle.task + +import dev.nolij.zumegradle.smoketest.SmokeTest +import dev.nolij.zumegradle.smoketest.SmokeTest.Config +import org.gradle.api.DefaultTask +import org.gradle.api.provider.ListProperty +import org.gradle.api.provider.Property +import org.gradle.api.tasks.Input +import org.gradle.jvm.tasks.Jar +import org.gradle.api.tasks.TaskAction + +abstract class SmokeTestTask : DefaultTask() { + init { + group = "verification" + } + + /** + * The task that provides the input jar file. + */ + @get:Input + abstract val inputTask: Property + + @get:Input + abstract val mainDir: Property + + @get:Input + abstract val workDir: Property + + @get:Input + abstract val maxThreads: Property + + @get:Input + abstract val threadTimeout: Property + + @get:Input + abstract val configs: ListProperty + + @get:Input + abstract val portableMCBinary: Property + + fun config(config: Config) { + configs.add(config) + } + + fun configs(vararg configs: Config) { + this.configs.addAll(configs.asList()) + } + + @TaskAction + fun runSmokeTest() { + SmokeTest( + project, + portableMCBinary.get(), + inputTask.get().archiveFile.get().asFile, + mainDir.get(), + workDir.get(), + maxThreads.get(), + threadTimeout.get(), + configs.get() + ).test() + } +} \ No newline at end of file diff --git a/buildSrc/src/main/kotlin/dev/nolij/zumegradle/util/AsmHelper.kt b/buildSrc/src/main/kotlin/dev/nolij/zumegradle/util/AsmHelper.kt new file mode 100644 index 0000000..b425276 --- /dev/null +++ b/buildSrc/src/main/kotlin/dev/nolij/zumegradle/util/AsmHelper.kt @@ -0,0 +1,17 @@ +package dev.nolij.zumegradle.util + +import org.objectweb.asm.ClassReader +import org.objectweb.asm.ClassWriter +import org.objectweb.asm.tree.ClassNode + +fun ClassNode.toBytes(flags: Int = 0): ByteArray { + val writer = ClassWriter(flags) + this.accept(writer) + return writer.toByteArray() +} + +fun ClassNode(bytes: ByteArray): ClassNode { + val node = ClassNode() + ClassReader(bytes).accept(node, 0) + return node +} \ No newline at end of file diff --git a/buildSrc/src/main/kotlin/dev/nolij/zumegradle/util/MappingsHelper.kt b/buildSrc/src/main/kotlin/dev/nolij/zumegradle/util/MappingsHelper.kt new file mode 100644 index 0000000..5c6540a --- /dev/null +++ b/buildSrc/src/main/kotlin/dev/nolij/zumegradle/util/MappingsHelper.kt @@ -0,0 +1,29 @@ +package dev.nolij.zumegradle.util + +import net.fabricmc.mappingio.MappingReader +import net.fabricmc.mappingio.format.MappingFormat +import net.fabricmc.mappingio.tree.MappingTree.ClassMapping +import net.fabricmc.mappingio.tree.MemoryMappingTree +import java.io.File + +fun mappings(file: File, format: MappingFormat = MappingFormat.PROGUARD): MemoryMappingTree { + if (!file.exists()) { + error("Mappings file $file does not exist") + } + + val mappingTree = MemoryMappingTree() + MappingReader.read(file.toPath(), format, mappingTree) + return mappingTree +} + +@Suppress("INACCESSIBLE_TYPE", "NAME_SHADOWING") +fun MemoryMappingTree.map(src: String): String { + val src = src.replace('.', '/') + val dstNamespaceIndex = getNamespaceId(dstNamespaces[0]) + val classMapping: ClassMapping? = getClass(src) + if (classMapping == null) { + println("Class $src not found in mappings") + return src + } + return classMapping.getDstName(dstNamespaceIndex).replace('/', '.') +} \ No newline at end of file