diff --git a/build.gradle.kts b/build.gradle.kts index ebd8f4a07..c47fe6986 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -113,6 +113,8 @@ dependencies { implementation(libs.jdependency) implementation(libs.jdom2) implementation(libs.kotlin.metadata) + implementation(libs.moshi) + implementation(libs.moshi.kotlin) implementation(libs.plexus.utils) implementation(libs.plexus.xml) diff --git a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/PublishingTest.kt b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/PublishingTest.kt index f3a6c138f..b5ae7b5d0 100644 --- a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/PublishingTest.kt +++ b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/PublishingTest.kt @@ -10,6 +10,7 @@ import assertk.assertions.isEqualTo import assertk.assertions.single import com.github.jengelman.gradle.plugins.shadow.ShadowJavaPlugin.Companion.SHADOW_RUNTIME_ELEMENTS_CONFIGURATION_NAME import com.github.jengelman.gradle.plugins.shadow.internal.classPathAttributeKey +import com.github.jengelman.gradle.plugins.shadow.internal.moshi import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar import com.github.jengelman.gradle.plugins.shadow.util.GradleModuleMetadata import com.github.jengelman.gradle.plugins.shadow.util.Issue @@ -20,8 +21,6 @@ import com.github.jengelman.gradle.plugins.shadow.util.containsOnly import com.github.jengelman.gradle.plugins.shadow.util.coordinate import com.github.jengelman.gradle.plugins.shadow.util.getMainAttr import com.squareup.moshi.JsonAdapter -import com.squareup.moshi.Moshi -import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory import java.nio.file.Path import kotlin.io.path.appendText import kotlin.io.path.exists @@ -663,7 +662,7 @@ class PublishingTest : BasePluginTest() { } private companion object { - val gmmAdapter: JsonAdapter = Moshi.Builder().add(KotlinJsonAdapterFactory()).build() + val gmmAdapter: JsonAdapter = moshi .adapter(GradleModuleMetadata::class.java) val pomReader = MavenXpp3Reader() diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/Moshi.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/Moshi.kt new file mode 100644 index 000000000..429c039ce --- /dev/null +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/Moshi.kt @@ -0,0 +1,247 @@ +@file:OptIn(UnstableMetadataApi::class) + +package com.github.jengelman.gradle.plugins.shadow.internal + +import com.squareup.moshi.JsonAdapter +import com.squareup.moshi.JsonDataException +import com.squareup.moshi.JsonReader +import com.squareup.moshi.JsonWriter +import com.squareup.moshi.Moshi +import com.squareup.moshi.Types +import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory +import java.lang.reflect.Type +import kotlin.metadata.ClassName +import kotlin.metadata.KmClass +import kotlin.metadata.jvm.JvmMetadataVersion +import kotlin.metadata.jvm.KmModule +import kotlin.metadata.jvm.KmPackageParts +import kotlin.metadata.jvm.KotlinModuleMetadata +import kotlin.metadata.jvm.UnstableMetadataApi + +internal val moshi: Moshi = Moshi.Builder() + .add(KotlinJsonAdapterFactory()) + .add(KotlinModuleMetadataAdapterFactory()) + .build() + +private class KotlinModuleMetadataAdapterFactory : JsonAdapter.Factory { + override fun create(type: Type, annotations: Set, moshi: Moshi): JsonAdapter<*>? { + return when (type) { + KotlinModuleMetadata::class.java -> { + val kmModuleAdapter = moshi.adapter(KmModule::class.java) + val versionAdapter = moshi.adapter(JvmMetadataVersion::class.java) + KotlinModuleMetadataAdapter(kmModuleAdapter, versionAdapter) + } + JvmMetadataVersion::class.java -> JvmMetadataVersionAdapter() + KmModule::class.java -> { + val mapAdapter = moshi.adapter>( + Types.newParameterizedType( + Map::class.java, + String::class.java, + KmPackageParts::class.java, + ), + ) + val listKmClassAdapter = moshi.adapter>( + Types.newParameterizedType(List::class.java, KmClass::class.java), + ) + KmModuleAdapter(mapAdapter, listKmClassAdapter) + } + KmPackageParts::class.java -> { + val listStringAdapter = moshi.adapter>( + Types.newParameterizedType(List::class.java, String::class.java), + ) + val mapStringStringAdapter = moshi.adapter>( + Types.newParameterizedType( + Map::class.java, + String::class.java, + String::class.java, + ), + ) + KmPackagePartsAdapter(listStringAdapter, mapStringStringAdapter) + } + KmClass::class.java -> { + // This adapter is simplified. A full adapter would be very large. + val classNameAdapter = moshi.adapter(ClassName::class.java) + KmClassJsonAdapter(classNameAdapter) + } + else -> null + } + } +} + +private class KotlinModuleMetadataAdapter( + private val kmModuleAdapter: JsonAdapter, + private val versionAdapter: JsonAdapter, +) : JsonAdapter() { + override fun fromJson(reader: JsonReader): KotlinModuleMetadata { + reader.beginObject() + var kmModule: KmModule? = null + var version: JvmMetadataVersion? = null + while (reader.hasNext()) { + when (reader.nextName()) { + "kmModule" -> kmModule = kmModuleAdapter.fromJson(reader) + "version" -> version = versionAdapter.fromJson(reader) + else -> reader.skipValue() + } + } + reader.endObject() + return KotlinModuleMetadata( + kmModule ?: throw JsonDataException("Required property 'kmModule' is missing"), + version ?: throw JsonDataException("Required property 'version' is missing"), + ) + } + + override fun toJson(writer: JsonWriter, value: KotlinModuleMetadata?) { + if (value == null) { + writer.nullValue() + return + } + writer.beginObject() + writer.name("kmModule") + kmModuleAdapter.toJson(writer, value.kmModule) + writer.name("version") + versionAdapter.toJson(writer, value.version) + writer.endObject() + } +} + +private class JvmMetadataVersionAdapter : JsonAdapter() { + override fun fromJson(reader: JsonReader): JvmMetadataVersion { + reader.beginObject() + var major = -1 + var minor = -1 + var patch = -1 + while (reader.hasNext()) { + when (reader.nextName()) { + "major" -> major = reader.nextInt() + "minor" -> minor = reader.nextInt() + "patch" -> patch = reader.nextInt() + else -> reader.skipValue() + } + } + reader.endObject() + return JvmMetadataVersion(major, minor, patch) + } + + override fun toJson(writer: JsonWriter, value: JvmMetadataVersion?) { + if (value == null) { + writer.nullValue() + return + } + writer.beginObject() + writer.name("major").value(value.major) + writer.name("minor").value(value.minor) + writer.name("patch").value(value.patch) + writer.endObject() + } +} + +private class KmModuleAdapter( + private val packagePartsAdapter: JsonAdapter>, + private val optionalClassesAdapter: JsonAdapter>, +) : JsonAdapter() { + override fun fromJson(reader: JsonReader): KmModule { + val kmModule = KmModule() + reader.beginObject() + while (reader.hasNext()) { + when (reader.nextName()) { + "packageParts" -> { + packagePartsAdapter.fromJson(reader)?.let { + kmModule.packageParts.putAll(it) + } + } + "optionalAnnotationClasses" -> { + optionalClassesAdapter.fromJson(reader)?.let { + kmModule.optionalAnnotationClasses.addAll(it) + } + } + else -> reader.skipValue() + } + } + reader.endObject() + return kmModule + } + + override fun toJson(writer: JsonWriter, value: KmModule?) { + if (value == null) { + writer.nullValue() + return + } + writer.beginObject() + writer.name("packageParts") + packagePartsAdapter.toJson(writer, value.packageParts) + writer.name("optionalAnnotationClasses") + optionalClassesAdapter.toJson(writer, value.optionalAnnotationClasses) + writer.endObject() + } +} + +private class KmPackagePartsAdapter( + private val listStringAdapter: JsonAdapter>, + private val mapStringStringAdapter: JsonAdapter>, +) : JsonAdapter() { + override fun fromJson(reader: JsonReader): KmPackageParts { + var fileFacades: MutableList? = null + var multiFileClassParts: MutableMap? = null + reader.beginObject() + while (reader.hasNext()) { + when (reader.nextName()) { + "fileFacades" -> fileFacades = listStringAdapter.fromJson(reader)?.toMutableList() + "multiFileClassParts" -> + multiFileClassParts = + mapStringStringAdapter.fromJson(reader)?.toMutableMap() + else -> reader.skipValue() + } + } + reader.endObject() + return KmPackageParts( + fileFacades ?: mutableListOf(), + multiFileClassParts ?: mutableMapOf(), + ) + } + + override fun toJson(writer: JsonWriter, value: KmPackageParts?) { + if (value == null) { + writer.nullValue() + return + } + writer.beginObject() + writer.name("fileFacades") + listStringAdapter.toJson(writer, value.fileFacades) + writer.name("multiFileClassParts") + mapStringStringAdapter.toJson(writer, value.multiFileClassParts) + writer.endObject() + } +} + +// Simplified adapter for KmClass +private class KmClassJsonAdapter( + private val classNameAdapter: JsonAdapter, +) : JsonAdapter() { + override fun fromJson(reader: JsonReader): KmClass { + val kmClass = KmClass() + reader.beginObject() + while (reader.hasNext()) { + when (reader.nextName()) { + "name" -> kmClass.name = classNameAdapter.fromJson(reader) + ?: throw JsonDataException("Required property 'name' is missing for KmClass") + // Other properties would be deserialized here + else -> reader.skipValue() + } + } + reader.endObject() + return kmClass + } + + override fun toJson(writer: JsonWriter, value: KmClass?) { + if (value == null) { + writer.nullValue() + return + } + writer.beginObject() + writer.name("name") + classNameAdapter.toJson(writer, value.name) + // Other properties would be serialized here. + // For brevity, we are only serializing the name. + writer.endObject() + } +} diff --git a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/KotlinModuleMetadataAdapterTest.kt b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/KotlinModuleMetadataAdapterTest.kt new file mode 100644 index 000000000..9c866f044 --- /dev/null +++ b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/KotlinModuleMetadataAdapterTest.kt @@ -0,0 +1,21 @@ +package com.github.jengelman.gradle.plugins.shadow.internal + +import assertk.assertThat +import assertk.assertions.isEqualTo +import kotlin.io.path.readBytes +import kotlin.metadata.jvm.KotlinModuleMetadata +import kotlin.metadata.jvm.UnstableMetadataApi +import org.junit.jupiter.api.Test + +@OptIn(UnstableMetadataApi::class) +class KotlinModuleMetadataAdapterTest { + + @Test + fun moduleMetadataFileNotChanged() { + val expected = requireResourceAsPath("META-INF/kotlin-stdlib.kotlin_module").readBytes() + val adapter = moshi.adapter(KotlinModuleMetadata::class.java) + val json = adapter.toJson(KotlinModuleMetadata.read(expected)) + val actual = requireNotNull(adapter.fromJson(json)).write() + assertThat(actual).isEqualTo(expected) + } +} diff --git a/src/test/resources/META-INF/kotlin-stdlib.kotlin_module b/src/test/resources/META-INF/kotlin-stdlib.kotlin_module new file mode 100644 index 000000000..aa2c0b61c Binary files /dev/null and b/src/test/resources/META-INF/kotlin-stdlib.kotlin_module differ