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
import org.jetbrains.kotlin.gradle.internals.MULTIPLATFORM_PROJECT_METADATA_FILE_NAME
import org.jetbrains.kotlin.gradle.internals.parseKotlinSourceSetMetadataFromXml
import org.jetbrains.kotlin.gradle.internals.MULTIPLATFORM_PROJECT_METADATA_JSON_FILE_NAME
import org.jetbrains.kotlin.gradle.internals.parseKotlinSourceSetMetadataFromJson
import org.jetbrains.kotlin.gradle.plugin.KotlinJsCompilerType
import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinProjectStructureMetadata
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 java.io.File
import java.util.zip.ZipFile
import javax.xml.parsers.DocumentBuilderFactory
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertTrue
@@ -216,7 +215,7 @@ class HierarchicalMppIT : BaseGradleIT() {
)
).use { publishedMetadataJar ->
publishedMetadataJar.checkAllEntryNamesArePresent(
"META-INF/$MULTIPLATFORM_PROJECT_METADATA_FILE_NAME",
"META-INF/$MULTIPLATFORM_PROJECT_METADATA_JSON_FILE_NAME",
"commonMain/default/manifest",
"commonMain/default/linkdata/package_com.example/",
@@ -254,7 +253,7 @@ class HierarchicalMppIT : BaseGradleIT() {
)
).use { publishedMetadataJar ->
publishedMetadataJar.checkAllEntryNamesArePresent(
"META-INF/$MULTIPLATFORM_PROJECT_METADATA_FILE_NAME",
"META-INF/$MULTIPLATFORM_PROJECT_METADATA_JSON_FILE_NAME",
"commonMain/default/manifest",
"commonMain/default/linkdata/package_com.example.bar/",
@@ -441,10 +440,8 @@ class HierarchicalMppIT : BaseGradleIT() {
}
private fun ZipFile.getProjectStructureMetadata(): KotlinProjectStructureMetadata {
val documentBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder()
val document = getInputStream(getEntry("META-INF/$MULTIPLATFORM_PROJECT_METADATA_FILE_NAME"))
.use { inputStream -> documentBuilder.parse(inputStream) }
return checkNotNull(parseKotlinSourceSetMetadataFromXml(document))
val json = getInputStream(getEntry("META-INF/$MULTIPLATFORM_PROJECT_METADATA_JSON_FILE_NAME")).reader().readText()
return checkNotNull(parseKotlinSourceSetMetadataFromJson(json))
}
@Test
@@ -26,19 +26,15 @@ open class GenerateProjectStructureMetadata : DefaultTask() {
@get:OutputFile
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
fun generateMetadataXml() {
resultXmlFile.parentFile.mkdirs()
val document = kotlinProjectStructureMetadata.toXmlDocument()
TransformerFactory.newInstance().newTransformer().apply {
setOutputProperty(OutputKeys.INDENT, "yes")
setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "4")
}.transform(DOMSource(document), StreamResult(resultXmlFile))
val resultString = kotlinProjectStructureMetadata.toJson()
resultXmlFile.writeText(resultString)
}
}
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.sourceSetDependencyConfigurationByScope
import java.io.File
import java.io.InputStream
import java.util.*
import java.util.zip.ZipEntry
import java.util.zip.ZipFile
@@ -344,16 +345,20 @@ private class JarArtifactMppDependencyMetadataExtractor(
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? {
return ZipFile(primaryArtifact).use { zip ->
val metadata = zip.getEntry("META-INF/$MULTIPLATFORM_PROJECT_METADATA_FILE_NAME")
?: return null
val (metadata, parseFunction) =
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 ->
DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(inputStream)
}
parseKotlinSourceSetMetadataFromXml(metadataXmlDocument)
zip.getInputStream(metadata).use(parseFunction)
}
}
@@ -5,6 +5,10 @@
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.tasks.Input
import org.gradle.api.tasks.Internal
@@ -18,6 +22,7 @@ import org.w3c.dom.Document
import org.w3c.dom.Element
import org.w3c.dom.Node
import org.w3c.dom.NodeList
import java.io.StringWriter
import javax.xml.parsers.DocumentBuilderFactory
data class ModuleDependencyIdentifier(
@@ -34,6 +39,8 @@ sealed class SourceSetMetadataLayout(
object METADATA : SourceSetMetadataLayout("metadata", "jar")
object KLIB : SourceSetMetadataLayout("klib", "klib")
override fun toString(): String = name
companion object {
private val values = listOf(METADATA, KLIB)
@@ -119,41 +126,40 @@ internal fun buildKotlinProjectStructureMetadata(project: Project): KotlinProjec
)
}
internal fun KotlinProjectStructureMetadata.toXmlDocument(): Document {
return DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument().apply {
fun Node.node(name: String, action: Element.() -> Unit) = appendChild(createElement(name).apply(action))
fun Node.textNode(name: String, value: String) =
appendChild(createElement(name).apply { appendChild(createTextNode(value)) })
internal fun <Serializer> KotlinProjectStructureMetadata.serialize(
serializer: Serializer,
node: Serializer.(name: String, Serializer.() -> Unit) -> Unit,
multiNodes: Serializer.(name: String, Serializer.() -> Unit) -> Unit,
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) {
textNode(FORMAT_VERSION_NODE_NAME, formatVersion)
node(VARIANTS_NODE_NAME) {
sourceSetNamesByVariantName.forEach { (variantName, sourceSets) ->
node(VARIANT_NODE_NAME) {
textNode(NAME_NODE_NAME, variantName)
sourceSets.forEach { sourceSetName -> textNode(SOURCE_SET_NODE_NAME, sourceSetName) }
}
multiNodes(VARIANTS_NODE_NAME) {
sourceSetNamesByVariantName.forEach { (variantName, sourceSets) ->
multiNodesItem(VARIANT_NODE_NAME) {
value(NAME_NODE_NAME, variantName)
multiValue(SOURCE_SET_NODE_NAME, sourceSets.toList())
}
}
}
node(SOURCE_SETS_NODE_NAME) {
val keys = sourceSetsDependsOnRelation.keys + sourceSetModuleDependencies.keys
for (sourceSet in keys) {
node(SOURCE_SET_NODE_NAME) {
textNode(NAME_NODE_NAME, sourceSet)
sourceSetsDependsOnRelation[sourceSet].orEmpty().forEach { dependsOn ->
textNode(DEPENDS_ON_NODE_NAME, dependsOn)
}
sourceSetModuleDependencies[sourceSet].orEmpty().forEach { moduleDependency ->
textNode(MODULE_DEPENDENCY_NODE_NAME, moduleDependency.groupId + ":" + moduleDependency.moduleId)
}
sourceSetBinaryLayout[sourceSet]?.let { binaryLayout ->
textNode(BINARY_LAYOUT_NODE_NAME, binaryLayout.name)
}
if (sourceSet in hostSpecificSourceSets) {
textNode(HOST_SPECIFIC_NODE_NAME, "true")
}
multiNodes(SOURCE_SETS_NODE_NAME) {
val keys = sourceSetsDependsOnRelation.keys + sourceSetModuleDependencies.keys
for (sourceSet in keys) {
multiNodesItem(SOURCE_SET_NODE_NAME) {
value(NAME_NODE_NAME, sourceSet)
multiValue(DEPENDS_ON_NODE_NAME, sourceSetsDependsOnRelation[sourceSet].orEmpty().toList())
multiValue(MODULE_DEPENDENCY_NODE_NAME, sourceSetModuleDependencies[sourceSet].orEmpty().map { moduleDependency ->
moduleDependency.groupId + ":" + moduleDependency.moduleId
})
sourceSetBinaryLayout[sourceSet]?.let { binaryLayout ->
value(BINARY_LAYOUT_NODE_NAME, binaryLayout.name)
}
if (sourceSet in hostSpecificSourceSets) {
value(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>()
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? {
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>>()
variantsNode.childNodes.elements.filter { it.tagName == VARIANT_NODE_NAME }.forEach { variantNode ->
val variantName = variantNode.getElementsByTagName(NAME_NODE_NAME).elements.single().textContent
val sourceSets =
variantNode.childNodes.elements.filter { it.tagName == SOURCE_SET_NODE_NAME }.mapTo(mutableSetOf()) { it.textContent }
variantsNode.forEach { variantNode ->
val variantName = requireNotNull(variantNode.valueNamed(NAME_NODE_NAME))
val sourceSets = variantNode.multiValues(SOURCE_SET_NODE_NAME).toSet()
sourceSetsByVariant[variantName] = sourceSets
}
@@ -185,34 +246,24 @@ internal fun parseKotlinSourceSetMetadataFromXml(document: Document): KotlinProj
val sourceSetBinaryLayout = mutableMapOf<String, SourceSetMetadataLayout>()
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 ->
val sourceSetName = sourceSetNode.getElementsByTagName(NAME_NODE_NAME).elements.single().textContent
sourceSetsNode.forEach { sourceSetNode ->
val sourceSetName = checkNotNull(sourceSetNode.valueNamed(NAME_NODE_NAME))
val dependsOn = mutableSetOf<String>()
val moduleDependencies = mutableSetOf<ModuleDependencyIdentifier>()
sourceSetNode.childNodes.elements.forEach { node ->
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)
}
}
}
val dependsOn = sourceSetNode.multiValues(DEPENDS_ON_NODE_NAME).toSet()
val moduleDependencies = sourceSetNode.multiValues(MODULE_DEPENDENCY_NODE_NAME).mapTo(mutableSetOf()) {
val (groupId, moduleId) = it.split(":")
ModuleDependencyIdentifier(groupId, moduleId)
}
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
sourceSetModuleDependencies[sourceSetName] = moduleDependencies
}
@@ -197,7 +197,7 @@ class KotlinMetadataTargetConfigurator(kotlinPluginVersion: String) :
allMetadataJar.configure {
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_WARNING
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.parseKotlinSourceSetMetadataFromXml
import org.jetbrains.kotlin.gradle.plugin.mpp.MULTIPLATFORM_PROJECT_METADATA_JSON_FILE_NAME
import org.jetbrains.kotlin.gradle.plugin.mpp.parseKotlinSourceSetMetadataFromJson
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