diff options
author | Linnea Gräf <nea@nea.moe> | 2023-12-09 14:00:12 +0100 |
---|---|---|
committer | Linnea Gräf <nea@nea.moe> | 2023-12-09 14:00:12 +0100 |
commit | 67bf79815ec27c8b813480c11a45f35ef502fe5b (patch) | |
tree | 3656e257e8cb3c9652ee06c101b4da718384d609 /plugin | |
download | archenemy-67bf79815ec27c8b813480c11a45f35ef502fe5b.tar.gz archenemy-67bf79815ec27c8b813480c11a45f35ef502fe5b.tar.bz2 archenemy-67bf79815ec27c8b813480c11a45f35ef502fe5b.zip |
Initial commit
Diffstat (limited to 'plugin')
16 files changed, 576 insertions, 0 deletions
diff --git a/plugin/build.gradle.kts b/plugin/build.gradle.kts new file mode 100644 index 0000000..d0a7eff --- /dev/null +++ b/plugin/build.gradle.kts @@ -0,0 +1,50 @@ +plugins { + `java-gradle-plugin` + kotlin("jvm") version "1.8.0" + kotlin("plugin.serialization") version "1.8.0" +} + +repositories { + mavenCentral() + maven("https://maven.neoforged.net/releases") +} + +dependencies { + implementation("net.neoforged:artifactural:3.0.17") + implementation(platform("org.jetbrains.kotlin:kotlin-bom")) + implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8") + implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.2") + + testImplementation("org.jetbrains.kotlin:kotlin-test") + testImplementation("org.jetbrains.kotlin:kotlin-test-junit") +} + +gradlePlugin { + val greeting by plugins.creating { + id = "moe.nea.archenemy.greeting" + implementationClass = "moe.nea.archenemy.ArchenemyGreetingPlugin" + } + val mojang by plugins.creating { + id = "moe.nea.archenemy.mojang" + implementationClass = "moe.nea.archenemy.mojang.ArchenemyMojangPlugin" + } +} + +// Add a source set for the functional test suite +val functionalTestSourceSet = sourceSets.create("functionalTest") { +} + +configurations["functionalTestImplementation"].extendsFrom(configurations["testImplementation"]) + +// Add a task to run the functional tests +val functionalTest by tasks.registering(Test::class) { + testClassesDirs = functionalTestSourceSet.output.classesDirs + classpath = functionalTestSourceSet.runtimeClasspath +} + +gradlePlugin.testSourceSets(functionalTestSourceSet) + +tasks.named<Task>("check") { + // Run the functional tests as part of `check` + dependsOn(functionalTest) +} diff --git a/plugin/src/functionalTest/kotlin/moe/nea/archenemy/ArchenemyPluginFunctionalTest.kt b/plugin/src/functionalTest/kotlin/moe/nea/archenemy/ArchenemyPluginFunctionalTest.kt new file mode 100644 index 0000000..c4ba495 --- /dev/null +++ b/plugin/src/functionalTest/kotlin/moe/nea/archenemy/ArchenemyPluginFunctionalTest.kt @@ -0,0 +1,44 @@ +/* + * This Kotlin source file was generated by the Gradle 'init' task. + */ +package moe.nea.archenemy + +import java.io.File +import java.nio.file.Files +import kotlin.test.assertTrue +import kotlin.test.Test +import org.gradle.testkit.runner.GradleRunner +import org.junit.Rule +import org.junit.rules.TemporaryFolder + +/** + * A simple functional test for the 'moe.nea.archenemy.greeting' plugin. + */ +class ArchenemyPluginFunctionalTest { + @get:Rule val tempFolder = TemporaryFolder() + + private fun getProjectDir() = tempFolder.root + private fun getBuildFile() = getProjectDir().resolve("build.gradle") + private fun getSettingsFile() = getProjectDir().resolve("settings.gradle") + + @Test fun `can run task`() { + // Setup the test build + getSettingsFile().writeText("") + getBuildFile().writeText(""" +plugins { + id('moe.nea.archenemy.greeting') +} +""") + + // Run the build + val runner = GradleRunner.create() + runner.forwardOutput() + runner.withPluginClasspath() + runner.withArguments("greeting") + runner.withProjectDir(getProjectDir()) + val result = runner.build(); + + // Verify the result + assertTrue(result.output.contains("Hello from plugin 'moe.nea.archenemy.greeting'")) + } +} diff --git a/plugin/src/main/kotlin/moe/nea/archenemy/ArchenemyGreetingPlugin.kt b/plugin/src/main/kotlin/moe/nea/archenemy/ArchenemyGreetingPlugin.kt new file mode 100644 index 0000000..85144f1 --- /dev/null +++ b/plugin/src/main/kotlin/moe/nea/archenemy/ArchenemyGreetingPlugin.kt @@ -0,0 +1,21 @@ +/* + * This Kotlin source file was generated by the Gradle 'init' task. + */ +package moe.nea.archenemy + +import org.gradle.api.Project +import org.gradle.api.Plugin + +/** + * A simple 'hello world' plugin. + */ +class ArchenemyGreetingPlugin: Plugin<Project> { + override fun apply(project: Project) { + // Register a task + project.tasks.register("greeting") { task -> + task.doLast { + println("Hello from plugin 'moe.nea.archenemy.greeting'") + } + } + } +} diff --git a/plugin/src/main/kotlin/moe/nea/archenemy/DownloadUtils.kt b/plugin/src/main/kotlin/moe/nea/archenemy/DownloadUtils.kt new file mode 100644 index 0000000..c849349 --- /dev/null +++ b/plugin/src/main/kotlin/moe/nea/archenemy/DownloadUtils.kt @@ -0,0 +1,52 @@ +package moe.nea.archenemy + +import java.io.File +import java.io.IOException +import java.net.URL +import java.security.MessageDigest + +object DownloadUtils { + fun bytesToHex(hash: ByteArray): String { + val hexString = StringBuilder(2 * hash.size) + for (i in hash.indices) { + val hex = Integer.toHexString(0xff and hash[i].toInt()) + if (hex.length == 1) { + hexString.append('0') + } + hexString.append(hex) + } + return hexString.toString() + } + + + fun sha1Hash(file: File): String { + if (!file.exists()) return "" + val messageDigest = MessageDigest.getInstance("SHA-1") + + file.inputStream().use { + val r = ByteArray(4096) + while (true) { + val d = it.read(r) + if (d < 0) break + messageDigest.update(r, 0, d) + } + } + return bytesToHex(messageDigest.digest()) + } + + fun areHashesEqual(a: String, b: String): Boolean { + return b.equals(a, ignoreCase = true) + } + + fun downloadFile(source: URL, sha1: String, targetFile: File) { + targetFile.parentFile.mkdirs() + if (areHashesEqual(sha1Hash(targetFile), sha1)) return + source.openStream().use { inp -> + targetFile.outputStream().use { out -> + inp.copyTo(out) + } + } + if (!areHashesEqual(sha1Hash(targetFile), sha1)) + throw IOException("$targetFile should hash to $sha1, but does not") + } +}
\ No newline at end of file diff --git a/plugin/src/main/kotlin/moe/nea/archenemy/MCSide.kt b/plugin/src/main/kotlin/moe/nea/archenemy/MCSide.kt new file mode 100644 index 0000000..65604aa --- /dev/null +++ b/plugin/src/main/kotlin/moe/nea/archenemy/MCSide.kt @@ -0,0 +1,6 @@ +package moe.nea.archenemy + + +enum class MCSide { + CLIENT, SERVER +}
\ 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 new file mode 100644 index 0000000..b3b3cfd --- /dev/null +++ b/plugin/src/main/kotlin/moe/nea/archenemy/mojang/ArchenemyMojangExtension.kt @@ -0,0 +1,38 @@ +package moe.nea.archenemy.mojang + +import moe.nea.archenemy.MCSide +import net.minecraftforge.artifactural.gradle.GradleRepositoryAdapter +import org.gradle.api.Project +import org.gradle.api.artifacts.Dependency +import java.net.URI + +abstract class ArchenemyMojangExtension(val project: Project) { + val sharedExtension = project.rootProject.extensions.getByType(ArchenemySharedExtension::class.java) + + private val _registerMinecraftProvider by lazy { + GradleRepositoryAdapter.add( + project.repositories, + "Minecraft Provider", + sharedExtension.getLocalCacheDirectory().resolve("minecraft-provider"), + sharedExtension.minecraftProvider + ) + project.repositories.maven { + it.name = "Minecraft Libraries" + it.url = URI("https://libraries.minecraft.net/") + } + } + + + fun minecraft(version: String, side: MCSide): Dependency { + _registerMinecraftProvider + return project.dependencies.create( + sharedExtension.minecraftProvider.getDependencyCoordinate( + MinecraftProvider.MinecraftCoordinate( + version, + side, + ) + ) + ) + } + +}
\ No newline at end of file diff --git a/plugin/src/main/kotlin/moe/nea/archenemy/mojang/ArchenemyMojangPlugin.kt b/plugin/src/main/kotlin/moe/nea/archenemy/mojang/ArchenemyMojangPlugin.kt new file mode 100644 index 0000000..4f01322 --- /dev/null +++ b/plugin/src/main/kotlin/moe/nea/archenemy/mojang/ArchenemyMojangPlugin.kt @@ -0,0 +1,22 @@ +package moe.nea.archenemy.mojang + +import org.gradle.api.Plugin +import org.gradle.api.Project + +class ArchenemyMojangPlugin : Plugin<Project> { + + override fun apply(project: Project) { + project.rootProject.tasks.maybeCreate( + "downloadMinecraftVersionManifest", + DownloadMinecraftVersionManifest::class.java + ) + + val rootExt = project.rootProject.extensions + if (rootExt.findByName("archenemyShared") == null) { + rootExt.create("archenemyShared", ArchenemySharedExtension::class.java, project.rootProject) + } + val mojang = project.extensions.create( + "mojang", ArchenemyMojangExtension::class.java, project, + ) + } +}
\ No newline at end of file diff --git a/plugin/src/main/kotlin/moe/nea/archenemy/mojang/ArchenemySharedExtension.kt b/plugin/src/main/kotlin/moe/nea/archenemy/mojang/ArchenemySharedExtension.kt new file mode 100644 index 0000000..950446d --- /dev/null +++ b/plugin/src/main/kotlin/moe/nea/archenemy/mojang/ArchenemySharedExtension.kt @@ -0,0 +1,20 @@ +package moe.nea.archenemy.mojang + +import org.gradle.api.Project +import java.io.File + +abstract class ArchenemySharedExtension(val rootProject: Project) { + init { + require(rootProject == rootProject.rootProject) + } + + fun getLocalCacheDirectory(): File { + return rootProject.rootDir.resolve(".gradle/archenemy") + } + + fun getDownloadMinecraftVersionManifestTask(): DownloadMinecraftVersionManifest { + return rootProject.tasks.getByName("downloadMinecraftVersionManifest") as DownloadMinecraftVersionManifest + } + + val minecraftProvider = MinecraftProvider(this) +}
\ No newline at end of file diff --git a/plugin/src/main/kotlin/moe/nea/archenemy/mojang/DownloadMinecraftVersionManifest.kt b/plugin/src/main/kotlin/moe/nea/archenemy/mojang/DownloadMinecraftVersionManifest.kt new file mode 100644 index 0000000..2a21c2e --- /dev/null +++ b/plugin/src/main/kotlin/moe/nea/archenemy/mojang/DownloadMinecraftVersionManifest.kt @@ -0,0 +1,55 @@ +package moe.nea.archenemy.mojang + +import kotlinx.serialization.decodeFromString +import kotlinx.serialization.json.Json +import org.gradle.api.DefaultTask +import org.gradle.api.Project +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.provider.Property +import org.gradle.api.provider.Provider +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.Internal +import org.gradle.api.tasks.OutputFile +import org.gradle.api.tasks.TaskAction +import java.net.URL + +abstract class DownloadMinecraftVersionManifest : DefaultTask() { + + @get:OutputFile + abstract val manifestFile: RegularFileProperty + + @get:Input + abstract val manifestUrl: Property<String> + + init { + manifestUrl.convention("https://launchermeta.mojang.com/mc/game/version_manifest.json") + manifestFile.convention(project.layout.buildDirectory.file("mojang-version-manifest.json")) + } + + @TaskAction + fun downloadManifest() { + val url = URL(manifestUrl.get()) + val file = manifestFile.asFile.get() + file.parentFile.mkdirs() + url.openStream().use { input -> + file.outputStream().use { output -> + input.copyTo(output) + } + } + } + + @Internal + fun getManifestNow(): MojangVersionManifest { + // Force resolution + project.objects.fileCollection().from(manifestFile).files + return getManifest().get() + } + + @Internal + fun getManifest(): Provider<MojangVersionManifest> { + return manifestFile.asFile.map { + val manifestText = it.readText() + Json.decodeFromString<MojangVersionManifest>(manifestText) + } + } +}
\ No newline at end of file diff --git a/plugin/src/main/kotlin/moe/nea/archenemy/mojang/IdentityMinecraftTransformer.kt b/plugin/src/main/kotlin/moe/nea/archenemy/mojang/IdentityMinecraftTransformer.kt new file mode 100644 index 0000000..9d23c60 --- /dev/null +++ b/plugin/src/main/kotlin/moe/nea/archenemy/mojang/IdentityMinecraftTransformer.kt @@ -0,0 +1,15 @@ +package moe.nea.archenemy.mojang + +import java.io.File +import java.security.MessageDigest + +class IdentityMinecraftTransformer : MinecraftTransformer { + override fun updateHash(hash: MessageDigest) { + hash.update("Identity") + } + + override fun transformJar(oldJar: File, newJar: File) { + oldJar.copyTo(newJar) + } + +}
\ No newline at end of file diff --git a/plugin/src/main/kotlin/moe/nea/archenemy/mojang/MinecraftProvider.kt b/plugin/src/main/kotlin/moe/nea/archenemy/mojang/MinecraftProvider.kt new file mode 100644 index 0000000..845d3e7 --- /dev/null +++ b/plugin/src/main/kotlin/moe/nea/archenemy/mojang/MinecraftProvider.kt @@ -0,0 +1,138 @@ +package moe.nea.archenemy.mojang + +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.decodeFromStream +import moe.nea.archenemy.DownloadUtils +import moe.nea.archenemy.MCSide +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.api.artifact.Streamable +import net.minecraftforge.artifactural.api.repository.Repository +import net.minecraftforge.artifactural.base.artifact.StreamableArtifact +import java.io.File +import java.io.IOException +import java.net.URL +import java.security.MessageDigest +import java.util.concurrent.ConcurrentHashMap + +fun MessageDigest.updateField(text: String, value: String) { + this.update(text) + this.update(":") + this.update(value) + this.update(";") +} + +fun MessageDigest.update(text: String) { + this.update(text.encodeToByteArray()) +} + +class MinecraftProvider(val sharedExtension: ArchenemySharedExtension) : Repository { + + + data class MinecraftCoordinate( + val version: String, + val side: MCSide, + ) + + private val manifest by lazy { + URL("https://launchermeta.mojang.com/mc/game/version_manifest.json").openStream().use { + Json.decodeFromStream<MojangVersionManifest>(it) + } + } + private val providers = mutableSetOf<MinecraftCoordinate>() + private val versionManifest: MutableMap<String, MojangVersionMetadata> = ConcurrentHashMap() + private val json = Json { + ignoreUnknownKeys = true + } + + fun getDependencyCoordinate(minecraftCoordinate: MinecraftCoordinate): String { + providers.add(minecraftCoordinate) + return "archenemy.mojang:minecraft:${minecraftCoordinate.version}:${minecraftCoordinate.side}" + } + + fun getMappingsDependencyCoordinate(minecraftCoordinate: MinecraftCoordinate): String { + providers.add(minecraftCoordinate) + return "archenemy.mojang:minecraft:${minecraftCoordinate.version}:${minecraftCoordinate.side}-mappings@txt" + } + + + private fun getVersionManifest(version: String): MojangVersionMetadata { + return versionManifest.computeIfAbsent(version) { + val versionMetadata = manifest.versions.find { it.id == version } + if (versionMetadata == null) + throw IOException("Invalid minecraft version $version") + val metadata = URL(versionMetadata.url).openStream().use { + json.decodeFromStream<MojangVersionMetadata>(it) + } + metadata + } + } + + private fun downloadMinecraft(coordinate: MinecraftCoordinate, mappings: Boolean): File { + val metadata = getVersionManifest(coordinate.version) + val downloadType = when (coordinate.side) { + MCSide.CLIENT -> "client" + MCSide.SERVER -> "server" + } + if (mappings) "-mappings" else "" + val download = metadata.downloads[downloadType] + ?: throw IOException("Invalid minecraft side $downloadType for ${coordinate.version}") + val targetFile = + sharedExtension.getLocalCacheDirectory().resolve("minecraft-raw") + .resolve("minecraft-${coordinate.version}-${coordinate.side}.${if (mappings) "txt" else "jar"}") + DownloadUtils.downloadFile(URL(download.url), download.sha1, targetFile) + return targetFile + } + + private fun provideStreamableMinecraftJar(coordinate: MinecraftCoordinate): Streamable { + return Streamable { + downloadMinecraft(coordinate, false).inputStream() + } + } + + private fun provideStreamableMappings(coordinate: MinecraftCoordinate): Streamable { + return Streamable { + downloadMinecraft(coordinate, true).inputStream() + } + } + + fun getNullsafeIdentifier(identifier: ArtifactIdentifier): ArtifactIdentifier { + return object : ArtifactIdentifier by identifier { + override fun getClassifier(): String { + return if (identifier.classifier == null) + "" + else + identifier.classifier + } + } + } + + override fun getArtifact(identifier: ArtifactIdentifier?): Artifact { + if (identifier == null) return Artifact.none() + if (identifier.name != "minecraft") return Artifact.none() + if (!identifier.group.startsWith("archenemy.")) return Artifact.none() + if (identifier.extension == "pom") return Artifact.none() + val coordinate = + MinecraftCoordinate(identifier.version, MCSide.valueOf(identifier.classifier.removeSuffix("-mappings"))) + val isMappings = identifier.classifier.endsWith("-mappings") + if (!providers.contains(coordinate)) + error("Unregistered minecraft dependency") + if (identifier.extension == "jar" && !isMappings) { + return StreamableArtifact.ofStreamable( + getNullsafeIdentifier(identifier), + ArtifactType.BINARY, + provideStreamableMinecraftJar(coordinate) + ) + } + if (identifier.extension == "txt" && isMappings) { + return StreamableArtifact.ofStreamable( + getNullsafeIdentifier(identifier), + ArtifactType.OTHER, + provideStreamableMappings(coordinate) + ) + } + return Artifact.none() + } + + +}
\ No newline at end of file diff --git a/plugin/src/main/kotlin/moe/nea/archenemy/mojang/MinecraftTransformer.kt b/plugin/src/main/kotlin/moe/nea/archenemy/mojang/MinecraftTransformer.kt new file mode 100644 index 0000000..4748e36 --- /dev/null +++ b/plugin/src/main/kotlin/moe/nea/archenemy/mojang/MinecraftTransformer.kt @@ -0,0 +1,9 @@ +package moe.nea.archenemy.mojang + +import java.io.File +import java.security.MessageDigest + +interface MinecraftTransformer { + fun updateHash(hash: MessageDigest) + fun transformJar(oldJar: File, newJar: File) +} diff --git a/plugin/src/main/kotlin/moe/nea/archenemy/mojang/MojangVersionManifest.kt b/plugin/src/main/kotlin/moe/nea/archenemy/mojang/MojangVersionManifest.kt new file mode 100644 index 0000000..b3daa48 --- /dev/null +++ b/plugin/src/main/kotlin/moe/nea/archenemy/mojang/MojangVersionManifest.kt @@ -0,0 +1,29 @@ +@file:UseSerializers(InstantSerializer::class) + +package moe.nea.archenemy.mojang + +import kotlinx.serialization.Serializable +import kotlinx.serialization.UseSerializers +import moe.nea.archenemy.util.InstantSerializer +import java.time.Instant + +@Serializable +data class MojangVersionManifest( + val latest: Promotions, + val versions: List<VersionReference> +) { + @Serializable + data class VersionReference( + val id: String, + val type: String, + val url: String, + val time: Instant, + val releaseTime: Instant, + ) + + @Serializable + data class Promotions( + val release: String, + val snapshot: String, + ) +}
\ No newline at end of file diff --git a/plugin/src/main/kotlin/moe/nea/archenemy/mojang/MojangVersionMetadata.kt b/plugin/src/main/kotlin/moe/nea/archenemy/mojang/MojangVersionMetadata.kt new file mode 100644 index 0000000..4ef3313 --- /dev/null +++ b/plugin/src/main/kotlin/moe/nea/archenemy/mojang/MojangVersionMetadata.kt @@ -0,0 +1,31 @@ +package moe.nea.archenemy.mojang + +import kotlinx.serialization.Serializable + +@Serializable +data class MojangVersionMetadata( + val assetIndex: AssetIndex, + val downloads: Map<String, Download>, + val libraries: List<Library> +) { + @Serializable + data class Library( + val name: String, + ) + @Serializable + data class Download( + val sha1: String, + val size: Long, + val url: String, + ) + + @Serializable + data class AssetIndex( + val id: String, + val sha1: String, + val size: Long, + val totalSize: Long, + val url: String, + ) + +}
\ No newline at end of file diff --git a/plugin/src/main/kotlin/moe/nea/archenemy/util/InstantSerializer.kt b/plugin/src/main/kotlin/moe/nea/archenemy/util/InstantSerializer.kt new file mode 100644 index 0000000..10baf04 --- /dev/null +++ b/plugin/src/main/kotlin/moe/nea/archenemy/util/InstantSerializer.kt @@ -0,0 +1,24 @@ +package moe.nea.archenemy.util + +import kotlinx.serialization.KSerializer +import kotlinx.serialization.descriptors.PrimitiveKind +import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder +import java.time.Instant +import java.time.format.DateTimeFormatter + +object InstantSerializer : KSerializer<Instant> { + override val descriptor: SerialDescriptor + get() = PrimitiveSerialDescriptor("Instant", PrimitiveKind.STRING) + val format: DateTimeFormatter = DateTimeFormatter.ISO_OFFSET_DATE_TIME + + override fun deserialize(decoder: Decoder): Instant { + return format.parse(decoder.decodeString(), Instant::from) as Instant + } + + override fun serialize(encoder: Encoder, value: Instant) { + TODO("Not yet implemented") + } +}
\ No newline at end of file diff --git a/plugin/src/test/kotlin/moe/nea/archenemy/ArchenemyGreetingPluginTest.kt b/plugin/src/test/kotlin/moe/nea/archenemy/ArchenemyGreetingPluginTest.kt new file mode 100644 index 0000000..7d36988 --- /dev/null +++ b/plugin/src/test/kotlin/moe/nea/archenemy/ArchenemyGreetingPluginTest.kt @@ -0,0 +1,22 @@ +/* + * This Kotlin source file was generated by the Gradle 'init' task. + */ +package moe.nea.archenemy + +import org.gradle.testfixtures.ProjectBuilder +import kotlin.test.Test +import kotlin.test.assertNotNull + +/** + * A simple unit test for the 'moe.nea.archenemy.greeting' plugin. + */ +class ArchenemyGreetingPluginTest { + @Test fun `plugin registers task`() { + // Create a test project and apply the plugin + val project = ProjectBuilder.builder().build() + project.plugins.apply("moe.nea.archenemy.greeting") + + // Verify the result + assertNotNull(project.tasks.findByName("greeting")) + } +} |