Implement kotlin-tooling-metadata.json artifact

- Introduce new :kotlin-tooling-metadata project
- Create new buildKotlinToolingMetadata task by default
- Add `kotlin-tooling-metadata.json` to root mpp publications
- Add kotlin.mpp.enableKotlinToolingMetadataArtifact flag to disable kotlin-tooling-metadata.json artifact

^KT-44584 Verification Pending
This commit is contained in:
sebastian.sellmair
2021-02-02 14:04:05 +01:00
committed by TeamCityServer
parent 88058ca2c8
commit ac8b4c1b79
29 changed files with 975 additions and 9 deletions
@@ -0,0 +1,15 @@
plugins {
java
kotlin("jvm")
id("jps-compatible")
}
publish()
sourcesJar()
javadocJar()
dependencies {
implementation(kotlinStdlib())
implementation("com.google.code.gson:gson:${rootProject.extra["versions.jar.gson"]}")
testImplementation(project(":kotlin-test:kotlin-test-junit"))
}
@@ -0,0 +1,124 @@
/*
* Copyright 2010-2021 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlin.tooling
import com.google.gson.*
import org.jetbrains.kotlin.tooling.KotlinToolingMetadataParsingResult.Failure
import org.jetbrains.kotlin.tooling.KotlinToolingMetadataParsingResult.Success
fun KotlinToolingMetadata.toJsonString(): String {
val gson = GsonBuilder().setPrettyPrinting().create()
return gson.toJson(toJsonObject())
}
internal fun KotlinToolingMetadata.toJsonObject(): JsonObject {
return JsonObject().apply {
addProperty("buildSystem", buildSystem)
addProperty("buildSystemVersion", buildSystemVersion)
addProperty("buildPlugin", buildPlugin)
addProperty("buildPluginVersion", buildPluginVersion)
add("projectSettings", projectSettings.toJsonObject())
add("projectTargets", projectTargets.toJsonArray())
}
}
internal fun KotlinToolingMetadata.ProjectSettings.toJsonObject(): JsonObject {
return JsonObject().apply {
addProperty("isHmppEnabled", isHmppEnabled)
addProperty("isCompatibilityMetadataVariantEnabled", isCompatibilityMetadataVariantEnabled)
}
}
internal fun List<KotlinToolingMetadata.ProjectTargetMetadata>.toJsonArray(): JsonArray {
return JsonArray().apply {
this@toJsonArray.forEach { targetMetadata ->
add(targetMetadata.toJsonObject())
}
}
}
internal fun KotlinToolingMetadata.ProjectTargetMetadata.toJsonObject(): JsonObject {
return JsonObject().apply {
addProperty("target", target)
addProperty("platformType", platformType)
if (extras.isNotEmpty()) {
add("extras", JsonObject().apply {
for (extra in extras) {
addProperty(extra.key, extra.value)
}
})
}
}
}
sealed class KotlinToolingMetadataParsingResult {
data class Success(val value: KotlinToolingMetadata) : KotlinToolingMetadataParsingResult()
data class Failure(val reason: String) : KotlinToolingMetadataParsingResult()
}
fun KotlinToolingMetadata.Companion.parseJson(json: String): KotlinToolingMetadataParsingResult {
val jsonElement = try {
JsonParser.parseString(json)
} catch (e: JsonParseException) {
return Failure("Invalid json: ${e.message}")
}
if (jsonElement !is JsonObject) {
return Failure("Expected JsonObject. Found ${json::class.java.canonicalName}")
}
return runCatching { jsonElement.toKotlinToolingMetadataOrThrow() }
.fold(
onSuccess = { Success(it) },
onFailure = { Failure("Failed parsing JsonObject: ${it.message}") }
)
}
fun KotlinToolingMetadata.Companion.parseJsonOrThrow(value: String): KotlinToolingMetadata {
return when (val result = parseJson(value)) {
is Success -> result.value
is Failure -> throw IllegalArgumentException(result.reason)
}
}
private fun JsonObject.toKotlinToolingMetadataOrThrow(): KotlinToolingMetadata {
return KotlinToolingMetadata(
buildSystem = getOrThrow("buildSystem").asString,
buildSystemVersion = getOrThrow("buildSystemVersion").asString,
buildPlugin = getOrThrow("buildPlugin").asString,
buildPluginVersion = getOrThrow("buildPluginVersion").asString,
projectSettings = getOrThrow("projectSettings").asJsonObject.toProjectSettingsOrThrow(),
projectTargets = getOrThrow("projectTargets").asJsonArray.map { it.asJsonObject.toTargetMetadataOrThrow() }
)
}
private fun JsonObject.toProjectSettingsOrThrow(): KotlinToolingMetadata.ProjectSettings {
return KotlinToolingMetadata.ProjectSettings(
isHmppEnabled = getOrThrow("isHmppEnabled").asBoolean,
isCompatibilityMetadataVariantEnabled = getOrThrow("isCompatibilityMetadataVariantEnabled").asBoolean
)
}
private fun JsonObject.toTargetMetadataOrThrow(): KotlinToolingMetadata.ProjectTargetMetadata {
return KotlinToolingMetadata.ProjectTargetMetadata(
target = getOrThrow("target").asString,
platformType = getOrThrow("platformType").asString,
extras = (get("extras") as? JsonObject)?.toTargetMetadataExtrasOrThrow() ?: emptyMap()
)
}
private fun JsonObject.toTargetMetadataExtrasOrThrow(): Map<String, String> {
val map = mutableMapOf<String, String>()
this.keySet().forEach { key ->
val primitive = this[key] as? JsonPrimitive
if (primitive != null) {
map[key] = primitive.asString
}
}
return map.toMap()
}
private fun JsonObject.getOrThrow(key: String): JsonElement {
return get(key) ?: throw IllegalArgumentException("Missing key: $key")
}
@@ -0,0 +1,43 @@
/*
* Copyright 2010-2021 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlin.tooling
data class KotlinToolingMetadata(
/**
* Build System used (e.g. Gradle, Maven, ...)
*/
val buildSystem: String,
val buildSystemVersion: String,
/**
* Plugin used to build (e.g.
* - org.jetbrains.kotlin.gradle.plugin.mpp.KotlinMultiplatformPlugin
* - org.jetbrains.kotlin.gradle.targets.js.KotlinJsPlugin
* - ...
*/
val buildPlugin: String,
val buildPluginVersion: String,
val projectSettings: ProjectSettings,
val projectTargets: List<ProjectTargetMetadata>,
) {
data class ProjectSettings(
val isHmppEnabled: Boolean,
val isCompatibilityMetadataVariantEnabled: Boolean,
)
data class ProjectTargetMetadata(
val target: String,
val platformType: String,
val extras: Map<String, String>
)
companion object
}
@@ -0,0 +1,50 @@
/*
* Copyright 2010-2021 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlin.tooling
import org.intellij.lang.annotations.Language
import org.junit.Test
import kotlin.test.assertFailsWith
import kotlin.test.assertTrue
class DeserializationFailureTest {
@Test
fun sample1() {
val result = KotlinToolingMetadata.parseJson("")
assertTrue(
result is KotlinToolingMetadataParsingResult.Failure,
"Expected empty String to produce Failure. Actual: $result"
)
assertFailsWith<IllegalArgumentException> { KotlinToolingMetadata.parseJsonOrThrow("") }
}
@Test
fun `sample2 missing build pluginVersion`() {
@Language("JSON") val json =
"""
{
"buildSystem": "Gradle",
"buildSystemVersion": "6.7",
"buildPlugin": "org.jetbrains.kotlin.gradle.plugin.KotlinMultiplatformPluginWrapper",
"projectSettings": {
"isHmppEnabled": false,
"isCompatibilityMetadataVariantEnabled": true
},
"projectTargets": []
}
""".trimIndent()
val result = KotlinToolingMetadata.parseJson(json)
assertTrue(
result is KotlinToolingMetadataParsingResult.Failure,
"Expected parsing failure, because of missing pluginVersion. Actual: $result"
)
assertFailsWith<IllegalArgumentException> { KotlinToolingMetadata.parseJsonOrThrow(json) }
}
}
@@ -0,0 +1,99 @@
/*
* Copyright 2010-2021 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlin.tooling
import junit.framework.Assert.assertFalse
import junit.framework.Assert.assertTrue
import org.intellij.lang.annotations.Language
import org.junit.Test
import kotlin.test.assertEquals
/*
* Copyright 2010-2021 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
class DeserializeStringTest {
@Test
fun sample1() {
@Language("JSON") val json =
"""
{
"buildSystem": "Gradle",
"buildSystemVersion": "6.7",
"buildPlugin": "org.jetbrains.kotlin.gradle.plugin.KotlinMultiplatformPluginWrapper",
"buildPluginVersion": "1.5.255-SNAPSHOT",
"projectSettings": {
"isHmppEnabled": false,
"isCompatibilityMetadataVariantEnabled": true
},
"projectTargets": [
{
"target": "org.jetbrains.kotlin.gradle.plugin.mpp.KotlinAndroidTarget",
"platformType": "androidJvm",
"extras": {
"sourceCompatibility": "1.7",
"targetCompatibility": "1.7"
}
},
{
"target": "org.jetbrains.kotlin.gradle.targets.js.KotlinJsTarget_Decorated",
"platformType": "js",
"extras": {
"isBrowserConfigured": "true",
"isNodejsConfigured": "true"
}
},
{
"target": "org.jetbrains.kotlin.gradle.targets.jvm.KotlinJvmTarget_Decorated",
"platformType": "jvm",
"extras": {
"withJavaEnabled": "false",
"jvmTarget": "1.8"
}
},
{
"target": "org.jetbrains.kotlin.gradle.plugin.mpp.KotlinMetadataTarget_Decorated",
"platformType": "common"
}
]
}
""".trimIndent()
val metadata = KotlinToolingMetadata.parseJsonOrThrow(json)
assertEquals("Gradle", metadata.buildSystem)
assertEquals("6.7", metadata.buildSystemVersion)
assertEquals("org.jetbrains.kotlin.gradle.plugin.KotlinMultiplatformPluginWrapper", metadata.buildPlugin)
assertEquals("1.5.255-SNAPSHOT", metadata.buildPluginVersion)
assertFalse(metadata.projectSettings.isHmppEnabled)
assertTrue(metadata.projectSettings.isCompatibilityMetadataVariantEnabled)
assertEquals(4, metadata.projectTargets.size, "Expected exactly 4 targets")
val androidJvmTarget = metadata.projectTargets.single { it.platformType == "androidJvm" }
assertEquals("org.jetbrains.kotlin.gradle.plugin.mpp.KotlinAndroidTarget", androidJvmTarget.target)
assertEquals(2, androidJvmTarget.extras.size, "Expected exactly two extras")
assertEquals("1.7", androidJvmTarget.extras["sourceCompatibility"])
assertEquals("1.7", androidJvmTarget.extras["targetCompatibility"])
val jsTarget = metadata.projectTargets.single { it.platformType == "js" }
assertEquals("org.jetbrains.kotlin.gradle.targets.js.KotlinJsTarget_Decorated", jsTarget.target)
assertEquals(2, jsTarget.extras.size, "Expected exactly two extras")
assertEquals("true", jsTarget.extras["isBrowserConfigured"])
assertEquals("true", jsTarget.extras["isNodejsConfigured"])
val jvmTarget = metadata.projectTargets.single { it.platformType == "jvm" }
assertEquals("org.jetbrains.kotlin.gradle.targets.jvm.KotlinJvmTarget_Decorated", jvmTarget.target)
assertEquals(2, jvmTarget.extras.size, "Expected exactly two extras")
assertEquals("false", jvmTarget.extras["withJavaEnabled"])
assertEquals("1.8", jvmTarget.extras["jvmTarget"])
val commonTarget = metadata.projectTargets.single { it.platformType == "common" }
assertEquals("org.jetbrains.kotlin.gradle.plugin.mpp.KotlinMetadataTarget_Decorated", commonTarget.target)
assertEquals(0, commonTarget.extras.size, "Expected zero extras")
}
}
@@ -0,0 +1,80 @@
/*
* Copyright 2010-2021 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlin.tooling
import org.jetbrains.kotlin.tooling.KotlinToolingMetadata.ProjectTargetMetadata
import kotlin.test.Test
import kotlin.test.assertEquals
/*
* Copyright 2010-2021 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
class SerializeAndDeserializeTest {
@Test
fun sample1() = assertDeserializedMatchesOrigin(
defaultKotlinToolingMetadata()
)
@Test
fun sample2() = assertDeserializedMatchesOrigin(
defaultKotlinToolingMetadata().copy(
projectTargets = listOf(
ProjectTargetMetadata(
target = "generic target",
platformType = "generic platform type",
extras = mapOf(
"extra0" to "extra value0",
"extra1" to "extra value1"
)
)
)
)
)
@Test
fun sample3() = assertDeserializedMatchesOrigin(
defaultKotlinToolingMetadata().copy(
projectTargets = listOf(
ProjectTargetMetadata(
target = "generic target",
platformType = "generic platform type",
extras = mapOf(
"extra0" to "{ some extra value %\" with, chars to escape",
"extra1" to "extra value1"
)
),
ProjectTargetMetadata(
target = "generic target 2 (with no extras)",
platformType = "generic platform type 2",
extras = emptyMap()
)
)
)
)
}
private fun assertDeserializedMatchesOrigin(origin: KotlinToolingMetadata) {
val json = origin.toJsonString()
val deserialized = KotlinToolingMetadata.parseJsonOrThrow(json)
assertEquals(origin, deserialized)
}
private fun defaultKotlinToolingMetadata(): KotlinToolingMetadata {
return KotlinToolingMetadata(
buildSystem = "generic build system",
buildSystemVersion = "v1.0 (build system)",
buildPlugin = "generic build plugin",
buildPluginVersion = "v1.0 (build plugin)",
projectSettings = KotlinToolingMetadata.ProjectSettings(
isHmppEnabled = false,
isCompatibilityMetadataVariantEnabled = true
),
projectTargets = emptyList()
)
}