diff options
Diffstat (limited to 'plugin/src')
15 files changed, 526 insertions, 0 deletions
| 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")) +    } +} | 
