Use JSON as the format for the Kotlin project structure metadata

This commit is contained in:
Sergey Igushkin
2020-09-15 20:23:20 +03:00
parent f19ef0184c
commit 5963b07987
6 changed files with 141 additions and 93 deletions
@@ -5,8 +5,8 @@
package org.jetbrains.kotlin.gradle package org.jetbrains.kotlin.gradle
import org.jetbrains.kotlin.gradle.internals.MULTIPLATFORM_PROJECT_METADATA_FILE_NAME import org.jetbrains.kotlin.gradle.internals.MULTIPLATFORM_PROJECT_METADATA_JSON_FILE_NAME
import org.jetbrains.kotlin.gradle.internals.parseKotlinSourceSetMetadataFromXml import org.jetbrains.kotlin.gradle.internals.parseKotlinSourceSetMetadataFromJson
import org.jetbrains.kotlin.gradle.plugin.KotlinJsCompilerType import org.jetbrains.kotlin.gradle.plugin.KotlinJsCompilerType
import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinProjectStructureMetadata import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinProjectStructureMetadata
import org.jetbrains.kotlin.gradle.plugin.mpp.ModuleDependencyIdentifier import org.jetbrains.kotlin.gradle.plugin.mpp.ModuleDependencyIdentifier
@@ -16,7 +16,6 @@ import org.jetbrains.kotlin.gradle.util.checkedReplace
import org.jetbrains.kotlin.gradle.util.modify import org.jetbrains.kotlin.gradle.util.modify
import java.io.File import java.io.File
import java.util.zip.ZipFile import java.util.zip.ZipFile
import javax.xml.parsers.DocumentBuilderFactory
import kotlin.test.Test import kotlin.test.Test
import kotlin.test.assertEquals import kotlin.test.assertEquals
import kotlin.test.assertTrue import kotlin.test.assertTrue
@@ -216,7 +215,7 @@ class HierarchicalMppIT : BaseGradleIT() {
) )
).use { publishedMetadataJar -> ).use { publishedMetadataJar ->
publishedMetadataJar.checkAllEntryNamesArePresent( publishedMetadataJar.checkAllEntryNamesArePresent(
"META-INF/$MULTIPLATFORM_PROJECT_METADATA_FILE_NAME", "META-INF/$MULTIPLATFORM_PROJECT_METADATA_JSON_FILE_NAME",
"commonMain/default/manifest", "commonMain/default/manifest",
"commonMain/default/linkdata/package_com.example/", "commonMain/default/linkdata/package_com.example/",
@@ -254,7 +253,7 @@ class HierarchicalMppIT : BaseGradleIT() {
) )
).use { publishedMetadataJar -> ).use { publishedMetadataJar ->
publishedMetadataJar.checkAllEntryNamesArePresent( publishedMetadataJar.checkAllEntryNamesArePresent(
"META-INF/$MULTIPLATFORM_PROJECT_METADATA_FILE_NAME", "META-INF/$MULTIPLATFORM_PROJECT_METADATA_JSON_FILE_NAME",
"commonMain/default/manifest", "commonMain/default/manifest",
"commonMain/default/linkdata/package_com.example.bar/", "commonMain/default/linkdata/package_com.example.bar/",
@@ -441,10 +440,8 @@ class HierarchicalMppIT : BaseGradleIT() {
} }
private fun ZipFile.getProjectStructureMetadata(): KotlinProjectStructureMetadata { private fun ZipFile.getProjectStructureMetadata(): KotlinProjectStructureMetadata {
val documentBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder() val json = getInputStream(getEntry("META-INF/$MULTIPLATFORM_PROJECT_METADATA_JSON_FILE_NAME")).reader().readText()
val document = getInputStream(getEntry("META-INF/$MULTIPLATFORM_PROJECT_METADATA_FILE_NAME")) return checkNotNull(parseKotlinSourceSetMetadataFromJson(json))
.use { inputStream -> documentBuilder.parse(inputStream) }
return checkNotNull(parseKotlinSourceSetMetadataFromXml(document))
} }
@Test @Test
@@ -26,19 +26,15 @@ open class GenerateProjectStructureMetadata : DefaultTask() {
@get:OutputFile @get:OutputFile
val resultXmlFile: File val resultXmlFile: File
get() = project.buildDir.resolve("kotlinProjectStructureMetadata/$MULTIPLATFORM_PROJECT_METADATA_FILE_NAME") get() = project.buildDir.resolve("kotlinProjectStructureMetadata/$MULTIPLATFORM_PROJECT_METADATA_JSON_FILE_NAME")
@TaskAction @TaskAction
fun generateMetadataXml() { fun generateMetadataXml() {
resultXmlFile.parentFile.mkdirs() resultXmlFile.parentFile.mkdirs()
val resultString = kotlinProjectStructureMetadata.toJson()
val document = kotlinProjectStructureMetadata.toXmlDocument() resultXmlFile.writeText(resultString)
TransformerFactory.newInstance().newTransformer().apply {
setOutputProperty(OutputKeys.INDENT, "yes")
setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "4")
}.transform(DOMSource(document), StreamResult(resultXmlFile))
} }
} }
internal const val MULTIPLATFORM_PROJECT_METADATA_FILE_NAME = "kotlin-project-structure-metadata.xml" internal const val MULTIPLATFORM_PROJECT_METADATA_FILE_NAME = "kotlin-project-structure-metadata.xml"
internal const val MULTIPLATFORM_PROJECT_METADATA_JSON_FILE_NAME = "kotlin-project-structure-metadata.json"
@@ -17,6 +17,7 @@ import org.jetbrains.kotlin.gradle.plugin.KotlinSourceSet
import org.jetbrains.kotlin.gradle.plugin.sources.KotlinDependencyScope import org.jetbrains.kotlin.gradle.plugin.sources.KotlinDependencyScope
import org.jetbrains.kotlin.gradle.plugin.sources.sourceSetDependencyConfigurationByScope import org.jetbrains.kotlin.gradle.plugin.sources.sourceSetDependencyConfigurationByScope
import java.io.File import java.io.File
import java.io.InputStream
import java.util.* import java.util.*
import java.util.zip.ZipEntry import java.util.zip.ZipEntry
import java.util.zip.ZipFile import java.util.zip.ZipFile
@@ -344,16 +345,20 @@ private class JarArtifactMppDependencyMetadataExtractor(
val metadataArtifactBySourceSet: MutableMap<String, File> = mutableMapOf() val metadataArtifactBySourceSet: MutableMap<String, File> = mutableMapOf()
private fun parseJsonProjectStructureMetadata(input: InputStream) =
parseKotlinSourceSetMetadataFromJson(input.reader().readText())
private fun parseXmlProjectStructureMetadata(input: InputStream) =
parseKotlinSourceSetMetadataFromXml(DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(input))
override fun getProjectStructureMetadata(): KotlinProjectStructureMetadata? { override fun getProjectStructureMetadata(): KotlinProjectStructureMetadata? {
return ZipFile(primaryArtifact).use { zip -> return ZipFile(primaryArtifact).use { zip ->
val metadata = zip.getEntry("META-INF/$MULTIPLATFORM_PROJECT_METADATA_FILE_NAME") val (metadata, parseFunction) =
?: return null zip.getEntry("META-INF/$MULTIPLATFORM_PROJECT_METADATA_JSON_FILE_NAME")?.to(::parseJsonProjectStructureMetadata)
?: zip.getEntry("META-INF/$MULTIPLATFORM_PROJECT_METADATA_FILE_NAME")?.to(::parseXmlProjectStructureMetadata)
?: return null
val metadataXmlDocument = zip.getInputStream(metadata).use { inputStream -> zip.getInputStream(metadata).use(parseFunction)
DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(inputStream)
}
parseKotlinSourceSetMetadataFromXml(metadataXmlDocument)
} }
} }
@@ -5,6 +5,10 @@
package org.jetbrains.kotlin.gradle.plugin.mpp package org.jetbrains.kotlin.gradle.plugin.mpp
import com.google.gson.GsonBuilder
import com.google.gson.JsonObject
import com.google.gson.JsonParser
import com.google.gson.stream.JsonWriter
import org.gradle.api.Project import org.gradle.api.Project
import org.gradle.api.tasks.Input import org.gradle.api.tasks.Input
import org.gradle.api.tasks.Internal import org.gradle.api.tasks.Internal
@@ -18,6 +22,7 @@ import org.w3c.dom.Document
import org.w3c.dom.Element import org.w3c.dom.Element
import org.w3c.dom.Node import org.w3c.dom.Node
import org.w3c.dom.NodeList import org.w3c.dom.NodeList
import java.io.StringWriter
import javax.xml.parsers.DocumentBuilderFactory import javax.xml.parsers.DocumentBuilderFactory
data class ModuleDependencyIdentifier( data class ModuleDependencyIdentifier(
@@ -34,6 +39,8 @@ sealed class SourceSetMetadataLayout(
object METADATA : SourceSetMetadataLayout("metadata", "jar") object METADATA : SourceSetMetadataLayout("metadata", "jar")
object KLIB : SourceSetMetadataLayout("klib", "klib") object KLIB : SourceSetMetadataLayout("klib", "klib")
override fun toString(): String = name
companion object { companion object {
private val values = listOf(METADATA, KLIB) private val values = listOf(METADATA, KLIB)
@@ -119,41 +126,40 @@ internal fun buildKotlinProjectStructureMetadata(project: Project): KotlinProjec
) )
} }
internal fun KotlinProjectStructureMetadata.toXmlDocument(): Document { internal fun <Serializer> KotlinProjectStructureMetadata.serialize(
return DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument().apply { serializer: Serializer,
fun Node.node(name: String, action: Element.() -> Unit) = appendChild(createElement(name).apply(action)) node: Serializer.(name: String, Serializer.() -> Unit) -> Unit,
fun Node.textNode(name: String, value: String) = multiNodes: Serializer.(name: String, Serializer.() -> Unit) -> Unit,
appendChild(createElement(name).apply { appendChild(createTextNode(value)) }) multiNodesItem: Serializer.(name: String, Serializer.() -> Unit) -> Unit,
value: Serializer.(key: String, value: String) -> Unit,
multiValue: Serializer.(name: String, values: List<String>) -> Unit
) = with(serializer) {
node(ROOT_NODE_NAME) {
value(FORMAT_VERSION_NODE_NAME, formatVersion)
node(ROOT_NODE_NAME) { multiNodes(VARIANTS_NODE_NAME) {
textNode(FORMAT_VERSION_NODE_NAME, formatVersion) sourceSetNamesByVariantName.forEach { (variantName, sourceSets) ->
multiNodesItem(VARIANT_NODE_NAME) {
node(VARIANTS_NODE_NAME) { value(NAME_NODE_NAME, variantName)
sourceSetNamesByVariantName.forEach { (variantName, sourceSets) -> multiValue(SOURCE_SET_NODE_NAME, sourceSets.toList())
node(VARIANT_NODE_NAME) {
textNode(NAME_NODE_NAME, variantName)
sourceSets.forEach { sourceSetName -> textNode(SOURCE_SET_NODE_NAME, sourceSetName) }
}
} }
} }
}
node(SOURCE_SETS_NODE_NAME) { multiNodes(SOURCE_SETS_NODE_NAME) {
val keys = sourceSetsDependsOnRelation.keys + sourceSetModuleDependencies.keys val keys = sourceSetsDependsOnRelation.keys + sourceSetModuleDependencies.keys
for (sourceSet in keys) { for (sourceSet in keys) {
node(SOURCE_SET_NODE_NAME) { multiNodesItem(SOURCE_SET_NODE_NAME) {
textNode(NAME_NODE_NAME, sourceSet) value(NAME_NODE_NAME, sourceSet)
sourceSetsDependsOnRelation[sourceSet].orEmpty().forEach { dependsOn -> multiValue(DEPENDS_ON_NODE_NAME, sourceSetsDependsOnRelation[sourceSet].orEmpty().toList())
textNode(DEPENDS_ON_NODE_NAME, dependsOn) multiValue(MODULE_DEPENDENCY_NODE_NAME, sourceSetModuleDependencies[sourceSet].orEmpty().map { moduleDependency ->
} moduleDependency.groupId + ":" + moduleDependency.moduleId
sourceSetModuleDependencies[sourceSet].orEmpty().forEach { moduleDependency -> })
textNode(MODULE_DEPENDENCY_NODE_NAME, moduleDependency.groupId + ":" + moduleDependency.moduleId) sourceSetBinaryLayout[sourceSet]?.let { binaryLayout ->
} value(BINARY_LAYOUT_NODE_NAME, binaryLayout.name)
sourceSetBinaryLayout[sourceSet]?.let { binaryLayout -> }
textNode(BINARY_LAYOUT_NODE_NAME, binaryLayout.name) if (sourceSet in hostSpecificSourceSets) {
} value(HOST_SPECIFIC_NODE_NAME, "true")
if (sourceSet in hostSpecificSourceSets) {
textNode(HOST_SPECIFIC_NODE_NAME, "true")
}
} }
} }
} }
@@ -161,21 +167,76 @@ internal fun KotlinProjectStructureMetadata.toXmlDocument(): Document {
} }
} }
internal fun KotlinProjectStructureMetadata.toXmlDocument(): Document {
return DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument().apply {
val node: Node.(String, Node.() -> Unit) -> Unit = { name, content -> appendChild(createElement(name).apply(content)) }
val textNode: Node.(String, String) -> Unit =
{ name, value -> appendChild(createElement(name).apply { appendChild(createTextNode(value)) }) }
serialize(this as Node, node, node, node, textNode, { name, values -> for (v in values) textNode(name, v) })
}
}
internal fun KotlinProjectStructureMetadata.toJson(): String {
val gson = GsonBuilder().setPrettyPrinting().create()
val stringWriter = StringWriter()
with(gson.newJsonWriter(stringWriter)) {
val obj: JsonWriter.(String, JsonWriter.() -> Unit) -> Unit =
{ name, content -> if (name.isNotEmpty()) name(name); beginObject(); content(); endObject() }
val property: JsonWriter.(String, String) -> Unit = { name, value -> name(name); value(value) }
val array: JsonWriter.(String, JsonWriter.() -> Unit) -> Unit =
{ name, contents -> name(name); beginArray(); contents(); endArray() }
beginObject()
serialize(this, obj, array, { _, fn -> obj("", fn) }, property, { key, values -> array(key) { values.forEach { value(it) } } })
endObject()
}
return stringWriter.toString()
}
private val NodeList.elements: Iterable<Element> get() = (0 until length).map { this@elements.item(it) }.filterIsInstance<Element>() private val NodeList.elements: Iterable<Element> get() = (0 until length).map { this@elements.item(it) }.filterIsInstance<Element>()
internal fun parseKotlinSourceSetMetadataFromJson(string: String): KotlinProjectStructureMetadata? {
@Suppress("DEPRECATION") // The replacement doesn't compile against old dependencies such as AS 4.0
val json = JsonParser().parse(string).asJsonObject
val nodeNamed: JsonObject.(String) -> JsonObject? = { name -> get(name)?.asJsonObject }
val valueNamed: JsonObject.(String) -> String? = { name -> get(name)?.asString }
val multiObjects: JsonObject.(String?) -> Iterable<JsonObject> = { name -> get(name).asJsonArray.map { it.asJsonObject } }
val multiValues: JsonObject.(String?) -> Iterable<String> = { name -> get(name).asJsonArray.map { it.asString } }
return parseKotlinSourceSetMetadata({ json.get(ROOT_NODE_NAME).asJsonObject }, valueNamed, multiObjects, multiValues)
}
internal fun parseKotlinSourceSetMetadataFromXml(document: Document): KotlinProjectStructureMetadata? { internal fun parseKotlinSourceSetMetadataFromXml(document: Document): KotlinProjectStructureMetadata? {
val projectStructureNode = document.getElementsByTagName(ROOT_NODE_NAME).elements.single() val nodeNamed: Element.(String) -> Element? = { name -> getElementsByTagName(name).elements.singleOrNull() }
val valueNamed: Element.(String) -> String? =
{ name -> getElementsByTagName(name).run { if (length > 0) item(0).textContent else null } }
val multiObjects: Element.(String) -> Iterable<Element> = { name -> nodeNamed(name)?.childNodes?.elements ?: emptyList()}
val multiValues: Element.(String) -> Iterable<String> = { name -> getElementsByTagName(name).elements.map { it.textContent } }
val formatVersion = projectStructureNode.getElementsByTagName(FORMAT_VERSION_NODE_NAME).item(0).textContent return parseKotlinSourceSetMetadata(
{ document.getElementsByTagName(ROOT_NODE_NAME).elements.single() },
valueNamed,
multiObjects,
multiValues
)
}
val variantsNode = projectStructureNode.getElementsByTagName(VARIANTS_NODE_NAME).item(0) ?: return null internal fun <ParsingContext> parseKotlinSourceSetMetadata(
getRoot: () -> ParsingContext,
valueNamed: ParsingContext.(key: String) -> String?,
multiObjects: ParsingContext.(named: String) -> Iterable<ParsingContext>,
multiValues: ParsingContext.(named: String) -> Iterable<String>
): KotlinProjectStructureMetadata? {
val projectStructureNode = getRoot()
val formatVersion = checkNotNull(projectStructureNode.valueNamed(FORMAT_VERSION_NODE_NAME))
val variantsNode = projectStructureNode.multiObjects(VARIANTS_NODE_NAME)
val sourceSetsByVariant = mutableMapOf<String, Set<String>>() val sourceSetsByVariant = mutableMapOf<String, Set<String>>()
variantsNode.childNodes.elements.filter { it.tagName == VARIANT_NODE_NAME }.forEach { variantNode -> variantsNode.forEach { variantNode ->
val variantName = variantNode.getElementsByTagName(NAME_NODE_NAME).elements.single().textContent val variantName = requireNotNull(variantNode.valueNamed(NAME_NODE_NAME))
val sourceSets = val sourceSets = variantNode.multiValues(SOURCE_SET_NODE_NAME).toSet()
variantNode.childNodes.elements.filter { it.tagName == SOURCE_SET_NODE_NAME }.mapTo(mutableSetOf()) { it.textContent }
sourceSetsByVariant[variantName] = sourceSets sourceSetsByVariant[variantName] = sourceSets
} }
@@ -185,34 +246,24 @@ internal fun parseKotlinSourceSetMetadataFromXml(document: Document): KotlinProj
val sourceSetBinaryLayout = mutableMapOf<String, SourceSetMetadataLayout>() val sourceSetBinaryLayout = mutableMapOf<String, SourceSetMetadataLayout>()
val hostSpecificSourceSets = mutableSetOf<String>() val hostSpecificSourceSets = mutableSetOf<String>()
val sourceSetsNode = projectStructureNode.getElementsByTagName(SOURCE_SETS_NODE_NAME).item(0) ?: return null val sourceSetsNode = projectStructureNode.multiObjects(SOURCE_SETS_NODE_NAME)
sourceSetsNode.childNodes.elements.filter { it.tagName == SOURCE_SET_NODE_NAME }.forEach { sourceSetNode -> sourceSetsNode.forEach { sourceSetNode ->
val sourceSetName = sourceSetNode.getElementsByTagName(NAME_NODE_NAME).elements.single().textContent val sourceSetName = checkNotNull(sourceSetNode.valueNamed(NAME_NODE_NAME))
val dependsOn = mutableSetOf<String>() val dependsOn = sourceSetNode.multiValues(DEPENDS_ON_NODE_NAME).toSet()
val moduleDependencies = mutableSetOf<ModuleDependencyIdentifier>() val moduleDependencies = sourceSetNode.multiValues(MODULE_DEPENDENCY_NODE_NAME).mapTo(mutableSetOf()) {
val (groupId, moduleId) = it.split(":")
sourceSetNode.childNodes.elements.forEach { node -> ModuleDependencyIdentifier(groupId, moduleId)
when (node.tagName) {
DEPENDS_ON_NODE_NAME -> dependsOn.add(node.textContent)
MODULE_DEPENDENCY_NODE_NAME -> {
val (groupId, moduleId) = node.textContent.split(":")
moduleDependencies.add(ModuleDependencyIdentifier(groupId, moduleId))
}
BINARY_LAYOUT_NODE_NAME -> {
SourceSetMetadataLayout.byName(node.textContent)?.let { binaryLayout ->
sourceSetBinaryLayout[sourceSetName] = binaryLayout
}
}
HOST_SPECIFIC_NODE_NAME -> {
if (node.textContent == "true") {
hostSpecificSourceSets.add(sourceSetName)
}
}
}
} }
sourceSetNode.valueNamed(HOST_SPECIFIC_NODE_NAME)
?.let { if (it.toBoolean()) hostSpecificSourceSets.add(sourceSetName) }
sourceSetNode.valueNamed(BINARY_LAYOUT_NODE_NAME)
?.let { SourceSetMetadataLayout.byName(it) }
?.let { sourceSetBinaryLayout[sourceSetName] = it }
sourceSetDependsOnRelation[sourceSetName] = dependsOn sourceSetDependsOnRelation[sourceSetName] = dependsOn
sourceSetModuleDependencies[sourceSetName] = moduleDependencies sourceSetModuleDependencies[sourceSetName] = moduleDependencies
} }
@@ -197,7 +197,7 @@ class KotlinMetadataTargetConfigurator(kotlinPluginVersion: String) :
allMetadataJar.configure { allMetadataJar.configure {
it.from(generateMetadata.map { it.resultXmlFile }) { spec -> it.from(generateMetadata.map { it.resultXmlFile }) { spec ->
spec.into("META-INF").rename { MULTIPLATFORM_PROJECT_METADATA_FILE_NAME } spec.into("META-INF").rename { MULTIPLATFORM_PROJECT_METADATA_JSON_FILE_NAME }
} }
} }
} }
@@ -9,14 +9,13 @@ import org.jetbrains.kotlin.gradle.plugin.KOTLIN_12X_MPP_DEPRECATION_WARNING
import org.jetbrains.kotlin.gradle.targets.native.internal.NO_NATIVE_STDLIB_PROPERTY_WARNING import org.jetbrains.kotlin.gradle.targets.native.internal.NO_NATIVE_STDLIB_PROPERTY_WARNING
import org.jetbrains.kotlin.gradle.targets.native.internal.NO_NATIVE_STDLIB_WARNING import org.jetbrains.kotlin.gradle.targets.native.internal.NO_NATIVE_STDLIB_WARNING
import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinProjectStructureMetadata import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinProjectStructureMetadata
import org.jetbrains.kotlin.gradle.plugin.mpp.MULTIPLATFORM_PROJECT_METADATA_FILE_NAME import org.jetbrains.kotlin.gradle.plugin.mpp.MULTIPLATFORM_PROJECT_METADATA_JSON_FILE_NAME
import org.jetbrains.kotlin.gradle.plugin.mpp.parseKotlinSourceSetMetadataFromXml import org.jetbrains.kotlin.gradle.plugin.mpp.parseKotlinSourceSetMetadataFromJson
import org.jetbrains.kotlin.gradle.targets.native.DisabledNativeTargetsReporter import org.jetbrains.kotlin.gradle.targets.native.DisabledNativeTargetsReporter
import org.w3c.dom.Document
fun parseKotlinSourceSetMetadataFromXml(document: Document): KotlinProjectStructureMetadata? = parseKotlinSourceSetMetadataFromXml(document) fun parseKotlinSourceSetMetadataFromJson(json: String): KotlinProjectStructureMetadata? = parseKotlinSourceSetMetadataFromJson(json)
const val MULTIPLATFORM_PROJECT_METADATA_FILE_NAME = MULTIPLATFORM_PROJECT_METADATA_FILE_NAME const val MULTIPLATFORM_PROJECT_METADATA_JSON_FILE_NAME = MULTIPLATFORM_PROJECT_METADATA_JSON_FILE_NAME
const val DISABLED_NATIVE_TARGETS_REPORTER_DISABLE_WARNING_PROPERTY_NAME = DisabledNativeTargetsReporter.DISABLE_WARNING_PROPERTY_NAME const val DISABLED_NATIVE_TARGETS_REPORTER_DISABLE_WARNING_PROPERTY_NAME = DisabledNativeTargetsReporter.DISABLE_WARNING_PROPERTY_NAME