From 3c5741977ce7271a5e1c9fbe3a0ae93272cf3a9c Mon Sep 17 00:00:00 2001 From: Filipp Zhinkin Date: Wed, 31 Jan 2024 12:18:30 +0100 Subject: [PATCH 01/53] Introduce target hierarchy mirroring default template from KMP --- src/main/kotlin/klib/TargetHierarchy.kt | 123 +++++++++++++++++++ src/test/kotlin/tests/TargetHierarchyTest.kt | 52 ++++++++ 2 files changed, 175 insertions(+) create mode 100644 src/main/kotlin/klib/TargetHierarchy.kt create mode 100644 src/test/kotlin/tests/TargetHierarchyTest.kt diff --git a/src/main/kotlin/klib/TargetHierarchy.kt b/src/main/kotlin/klib/TargetHierarchy.kt new file mode 100644 index 00000000..e78f73b6 --- /dev/null +++ b/src/main/kotlin/klib/TargetHierarchy.kt @@ -0,0 +1,123 @@ +/* + * Copyright 2016-2024 JetBrains s.r.o. + * Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file. + */ + +package kotlinx.validation.klib + +internal object TargetHierarchy { + class Node(val name: String, vararg childrenNodes: Node) { + var parent: Node? = null + val children = childrenNodes.toList().toTypedArray() + + init { + childrenNodes.forEach { + it.parent = this + } + } + + fun visit(visitor: (Node) -> T): T { + return visitor(this) + } + } + + data class NodeClosure(val node: Node, val allLeafs: Set) + + internal val hierarchyIndex: Map + + private val hierarchy = Node( + "all", + Node("js"), + Node("wasmJs"), + Node("wasmWasi"), + Node( + "native", + Node( + "mingw", + Node("mingwX64"), + Node("mingwX86") + ), + Node( + "linux", + Node("linuxArm64"), + Node("linuxArm32Hfp"), + Node("linuxX64") + ), + Node( + "androidNative", + Node("androidNativeArm64"), + Node("androidNativeArm32"), + Node("androidNativeX86"), + Node("androidNativeX64") + ), + Node( + "apple", + Node( + "macos", + Node("macosArm64"), + Node("macosX64") + ), + Node( + "ios", + Node("iosArm64"), + Node("iosArm32"), + Node("iosX64"), + Node("iosSimulatorArm64") + ), + Node( + "tvos", + Node("tvosArm64"), + Node("tvosX64"), + Node("tvosSimulatorArm64") + ), + Node( + "watchos", + Node("watchosArm32"), + Node("watchosArm64"), + Node("watchosX64"), + Node("watchosSimulatorArm64"), + Node("watchosDeviceArm64"), + Node("watchosX86") + ) + ) + ) + ) + + init { + val closure = mutableMapOf() + + fun collectLeafs(node: Node): Set { + if (node.children.isEmpty()) { + closure[node.name] = NodeClosure(node, setOf(node.name)) + return setOf(node.name) + } + val leafs = mutableSetOf() + node.children.forEach { + leafs.addAll(it.visit(::collectLeafs)) + } + closure[node.name] = NodeClosure(node, leafs) + return leafs + } + val leafs = hierarchy.visit(::collectLeafs) + closure[hierarchy.name] = NodeClosure(hierarchy, leafs) + hierarchyIndex = closure + } + + fun parent(targetOrGroup: String): String? { + return hierarchyIndex[targetOrGroup]?.node?.parent?.name + } + + fun targets(targetOrGroup: String): Set { + return hierarchyIndex[targetOrGroup]?.allLeafs ?: emptySet() + } + + fun nonLeafTargets(): Set { + return hierarchyIndex.values.asSequence() + .filter { + it.allLeafs.size > 1 || (it.allLeafs.size == 1 && it.allLeafs.first() != it.node.name) + } + .map { it.node.name } + .toSet() + } +} + diff --git a/src/test/kotlin/tests/TargetHierarchyTest.kt b/src/test/kotlin/tests/TargetHierarchyTest.kt new file mode 100644 index 00000000..9b3c308e --- /dev/null +++ b/src/test/kotlin/tests/TargetHierarchyTest.kt @@ -0,0 +1,52 @@ +/* + * Copyright 2016-2024 JetBrains s.r.o. + * Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file. + */ + +package kotlinx.validation.klib + +import org.junit.Test +import kotlin.test.assertContentEquals +import kotlin.test.assertEquals + +class TargetHierarchyTest { + @Test + fun testHierarchy() { + assertContentEquals(listOf("linuxArm64", "linux", "native", "all"), + hierarchyFrom("linuxArm64")) + + assertContentEquals(listOf("js", "all"), + hierarchyFrom("js")) + + assertContentEquals(listOf("iosArm64", "ios", "apple", "native", "all"), + hierarchyFrom("iosArm64")) + + assertContentEquals(listOf("androidNative", "native", "all"), + hierarchyFrom("androidNative")) + + assertContentEquals(listOf("unknown"), hierarchyFrom("unknown")) + } + + @Test + fun testTargetsList() { + assertEquals(setOf("linuxX64"), TargetHierarchy.targets("linuxX64")) + assertEquals(setOf("macosX64", "macosArm64"), TargetHierarchy.targets("macos")) + assertEquals(emptySet(), TargetHierarchy.targets("unknown")) + } + + @OptIn(ExperimentalStdlibApi::class) + private fun hierarchyFrom(groupOrTarget: String): List { + return buildList { + var i = 0 + var group: String? = groupOrTarget + while (group != null) { + if (i > TargetHierarchy.hierarchyIndex.size) { + throw AssertionError("Cycle detected: $this") + } + add(group) + group = TargetHierarchy.parent(group) + i++ + } + } + } +} From 4e50f6dcbf93592c6c8b0a16d231d8a1f2ec7398 Mon Sep 17 00:00:00 2001 From: Filipp Zhinkin Date: Wed, 31 Jan 2024 12:21:13 +0100 Subject: [PATCH 02/53] Implement KLib dump merger --- src/main/kotlin/klib/KlibAbiDumpFileMerger.kt | 608 ++++++++++++++++++ src/test/kotlin/tests/KlibAbiMergingTest.kt | 267 ++++++++ .../merge/diverging/androidNativeArm64.api | 18 + .../resources/merge/diverging/linuxArm64.api | 15 + .../resources/merge/diverging/linuxX64.api | 15 + src/test/resources/merge/diverging/merged.abi | 31 + .../merge/diverging/merged_with_aliases.abi | 33 + .../resources/merge/diverging/tvOsX64.api | 15 + src/test/resources/merge/guess/common.api | 14 + src/test/resources/merge/guess/guessed.api | 15 + .../merge/guess/linuxArm64Specific.api | 13 + src/test/resources/merge/guess/merged.api | 24 + .../resources/merge/header-mismatch/v1.abi | 13 + .../resources/merge/header-mismatch/v2.abi | 13 + .../merge/idempotent/bcv-klib-test.abi | 31 + src/test/resources/merge/identical/dump.abi | 13 + src/test/resources/merge/identical/merged.abi | 15 + .../merge/illegalFiles/emptyFile.txt | 0 .../merge/illegalFiles/nonDumpFile.txt | 1 + .../merge/parseNarrowChildrenDecls/merged.abi | 16 + .../withoutLinuxAll.abi | 14 + .../withoutLinuxArm64.abi | 16 + 22 files changed, 1200 insertions(+) create mode 100644 src/main/kotlin/klib/KlibAbiDumpFileMerger.kt create mode 100644 src/test/kotlin/tests/KlibAbiMergingTest.kt create mode 100644 src/test/resources/merge/diverging/androidNativeArm64.api create mode 100644 src/test/resources/merge/diverging/linuxArm64.api create mode 100644 src/test/resources/merge/diverging/linuxX64.api create mode 100644 src/test/resources/merge/diverging/merged.abi create mode 100644 src/test/resources/merge/diverging/merged_with_aliases.abi create mode 100644 src/test/resources/merge/diverging/tvOsX64.api create mode 100644 src/test/resources/merge/guess/common.api create mode 100644 src/test/resources/merge/guess/guessed.api create mode 100644 src/test/resources/merge/guess/linuxArm64Specific.api create mode 100644 src/test/resources/merge/guess/merged.api create mode 100644 src/test/resources/merge/header-mismatch/v1.abi create mode 100644 src/test/resources/merge/header-mismatch/v2.abi create mode 100644 src/test/resources/merge/idempotent/bcv-klib-test.abi create mode 100644 src/test/resources/merge/identical/dump.abi create mode 100644 src/test/resources/merge/identical/merged.abi create mode 100644 src/test/resources/merge/illegalFiles/emptyFile.txt create mode 100644 src/test/resources/merge/illegalFiles/nonDumpFile.txt create mode 100644 src/test/resources/merge/parseNarrowChildrenDecls/merged.abi create mode 100644 src/test/resources/merge/parseNarrowChildrenDecls/withoutLinuxAll.abi create mode 100644 src/test/resources/merge/parseNarrowChildrenDecls/withoutLinuxArm64.abi diff --git a/src/main/kotlin/klib/KlibAbiDumpFileMerger.kt b/src/main/kotlin/klib/KlibAbiDumpFileMerger.kt new file mode 100644 index 00000000..67321aac --- /dev/null +++ b/src/main/kotlin/klib/KlibAbiDumpFileMerger.kt @@ -0,0 +1,608 @@ +/* + * Copyright 2016-2024 JetBrains s.r.o. + * Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file. + */ + +package kotlinx.validation.klib + +import java.io.File +import java.nio.file.Files +import javax.sound.sampled.Line + +internal data class Target(val name: String) + +internal class LinesProvider(private val lines: Iterator) : Iterator { + private var nextLine: String? = null + + public fun peek(): String? { + if (nextLine != null) { + return nextLine + } + if (!lines.hasNext()) { + return null + } + nextLine = lines.next() + return nextLine + } + + override fun hasNext(): Boolean { + return nextLine != null || lines.hasNext() + } + + override fun next(): String { + if (nextLine != null) { + val res = nextLine!! + nextLine = null + return res + } + return lines.next() + } +} + +private const val MERGED_DUMP_FILE_HEADER = "// Merged KLib ABI Dump" +private const val REGULAR_DUMP_FILE_HEADER = "// Rendering settings:" +private const val COMMENT_PREFIX = "//" +private const val TARGETS_LIST_PREFIX = "// Targets: [" +private const val TARGETS_LIST_SUFFIX = "]" +private const val TARGETS_DELIMITER = ", " +private const val CLASS_DECLARATION_TERMINATOR = "}" +private const val INDENT_WIDTH = 4 + +private fun String.depth(): Int { + val indentation = this.takeWhile { it == ' ' }.count() + require(indentation % INDENT_WIDTH == 0) { + "Unexpected indentation, should be a multiple of $INDENT_WIDTH: $this" + } + return indentation / INDENT_WIDTH +} + +private fun parseBcvTargetsLine(line: String): Set { + val trimmedLine = line.trimStart(' ') + check(trimmedLine.startsWith(TARGETS_LIST_PREFIX) && trimmedLine.endsWith(TARGETS_LIST_SUFFIX)) { + "Not a targets list line: \"$line\"" + } + return trimmedLine.substring(TARGETS_LIST_PREFIX.length, trimmedLine.length - 1) + .split(TARGETS_DELIMITER) + .map { Target(it) } + .toSet() +} + +internal data class KlibAbiDumpFormat( + val includeTargets: Boolean = true, + val useGroupAliases: Boolean = false +) + +internal class KlibAbiDumpMerger { + private val targetsMut: MutableSet = mutableSetOf() + private val headerContent: MutableList = mutableListOf() + private val topLevelDeclaration: DeclarationContainer = DeclarationContainer("") + + /** + * All targets for which this dump contains declarations. + */ + public val targets: Set = targetsMut + + public fun loadMergedDump(file: File) { + require(file.exists()) { "File does not exist: $file" } + Files.lines(file.toPath()).use { + mergeFile(emptySet(), LinesProvider(it.iterator())) + } + } + + public fun addIndividualDump(target: Target, file: File) { + require(file.exists()) { "File does not exist: $file" } + Files.lines(file.toPath()).use { + mergeFile(setOf(target), LinesProvider(it.iterator())) + } + } + + private fun mergeFile(targets: Set, lines: LinesProvider) { + val isMergedFile = targets.isEmpty() + if (isMergedFile) check(this.targetsMut.isEmpty()) { "Merged dump could only be loaded once." } + + lines.checkFileFormat(isMergedFile) + + val bcvTargets = if (isMergedFile) { + lines.parseTargets() + } else { + targets + } + val aliases = if (isMergedFile) { + lines.parseAliases() + } else { + emptyMap() + } + + val header = lines.parseFileHeader() + if (isMergedFile || this.targetsMut.isEmpty()) { + headerContent.addAll(header) + } else if (headerContent != header) { + throw IllegalStateException("File header doesn't match the header of other files") + } + this.targetsMut.addAll(bcvTargets) + topLevelDeclaration.targets.addAll(bcvTargets) + + // All declarations belonging to the same scope has equal indentation. + // Nested declarations have higher indentation. + // By tracking the indentation we can decide if the line should be added into the current container, + // to its parent container (i.e. the line represents sibling declaration) or the current declaration ended, + // and we must pop one or several declaration out of the parsing stack. + var currentContainer = topLevelDeclaration + var depth = -1 + + while (lines.hasNext()) { + val line = lines.peek()!! + // TODO: wrap the line and cache the depth inside that wrapper? + val lineDepth = line.depth() + when { + // The depth is the same as before, we encountered a sibling + depth == lineDepth -> { + currentContainer = + lines.parseDeclaration(lineDepth, currentContainer.parent!!, bcvTargets, isMergedFile, aliases) + } + // The depth is increasing, that means we encountered child declaration + depth < lineDepth -> { + check(lineDepth - depth == 1) { + "The line has too big indentation relative to a previous line\nline: $line\n" + + "previous: ${currentContainer.text}" + } + currentContainer = + lines.parseDeclaration(lineDepth, currentContainer, bcvTargets, isMergedFile, aliases) + depth = lineDepth + } + // Otherwise, we're finishing all the declaration with greater depth compared to the depth of + // the next line. + // We won't process a line if it contains a new declaration here, just update the depth and current + // declaration reference to process the new declaration on the next iteration. + else -> { + while (currentContainer.text.depth() > lineDepth) { + currentContainer = currentContainer.parent!! + } + // If the line is '}' - add it as a terminator to corresponding declaration, it'll simplify + // dumping the merged file back to text format. + if (line.trim() == CLASS_DECLARATION_TERMINATOR) { + currentContainer.delimiter = line + // We processed the terminator char, so let's skip this line. + lines.next() + } + // For the top level declaration depth is -1 + depth = if (currentContainer.parent == null) -1 else currentContainer.text.depth() + } + } + } + } + + private fun LinesProvider.parseTargets(): Set { + val line = peek() + require(line != null) { + "List of targets expected, but there are no more lines left." + } + require(line.startsWith(TARGETS_LIST_PREFIX)) { + "The line should starts with $TARGETS_LIST_PREFIX, but was: $line" + } + next() + return parseBcvTargetsLine(line) + } + + private fun LinesProvider.parseAliases(): Map> { + val aliases = mutableMapOf>() + while (peek()?.startsWith("// Alias: ") == true) { + val line = next() + val trimmedLine = line.substring("// Alias: ".length) + val separatorIdx = trimmedLine.indexOf(" => [") + if (separatorIdx == -1 || !trimmedLine.endsWith(']')) { + throw IllegalStateException("Invalid alias line: $line") + } + val name = trimmedLine.substring(0, separatorIdx) + val targets = trimmedLine.substring( + separatorIdx + " => [".length, + trimmedLine.length - 1 + ) + .split(",") + .map { Target(it.trim()) } + .toSet() + aliases[name] = targets + } + return aliases + } + + private fun LinesProvider.parseFileHeader(): List { + val header = mutableListOf() + while (hasNext()) { + val next = peek()!! + if ((next.startsWith(COMMENT_PREFIX) && !next.startsWith(TARGETS_LIST_PREFIX)) || next.isBlank()) { + header.add(next) + next() + } else { + break + } + } + return header + } + + private fun LinesProvider.checkFileFormat(isMergedFile: Boolean) { + val headerLine = if (isMergedFile) { + next() + } else { + peek()!! + } + val expectedHeader = if (isMergedFile) { + MERGED_DUMP_FILE_HEADER + } else { + REGULAR_DUMP_FILE_HEADER + } + + check(headerLine == expectedHeader) { + val headerStart = if (headerLine.length > 32) { + headerLine.substring(0, 32) + "..." + } else { + headerLine + } + "Expected a file staring with \"$expectedHeader\", but the file stats with \"$headerStart\"" + } + } + + private fun LinesProvider.parseDeclaration( + depth: Int, + parent: DeclarationContainer, + allTargets: Set, + isMergedFile: Boolean, + aliases: Map> + ): DeclarationContainer { + val line = peek()!! + return if (line.startsWith(" ".repeat(depth * INDENT_WIDTH) + TARGETS_LIST_PREFIX)) { + check(isMergedFile) { + "Targets declaration should only be a part of merged file, " + + "and the current file claimed to be a regular dump file:\n$line" + } + next() // skip prefix + // Target list means that the declaration following it has narrower set of targets then its parent, + // so we must use it. + val targets = parseBcvTargetsLine(line) + val expandedTargets = targets.flatMap { + aliases[it.name] ?: listOf(it) + }.toSet() + parent.createOrUpdateChildren(next(), expandedTargets) + } else { + // That's an ugly part: + // - for a merged file (isMergedFile==true) we need to use parent declaration targets: if we're in this + // branch, no explicit targets were specified and new declaration targets should be the same as targets + // of its parent. We can't use allTargets here, as parent may have more specific set of targets. + // - for a single klib dump file we need to specify exact target associated with this file and allTargets + // must contain exactly one value here. + parent.createOrUpdateChildren(next(), if (isMergedFile) parent.targets else allTargets) + } + } + + fun dump(appendable: Appendable, dumpFormat: KlibAbiDumpFormat = KlibAbiDumpFormat()) { + val formatter = createFormatter(dumpFormat) + if (dumpFormat.includeTargets) { + appendable.append(MERGED_DUMP_FILE_HEADER).append('\n') + appendable.append(formatter.formatHeader(targets)).append('\n') + } else { + require(targets.size == 1) { + "Can skip target inclusion only if the dump contains a single target, but it contains: $targets" + } + } + headerContent.forEach { + appendable.append(it).append('\n') + } + topLevelDeclaration.children.sortedWith(DeclarationsComparator).forEach { + it.dump(appendable, targetsMut, dumpFormat, formatter) + } + } + + private fun createFormatter(dumpFormat: KlibAbiDumpFormat): KLibsTargetsFormatter { + return if (dumpFormat.useGroupAliases) { + for (target in targets) { + val node = TargetHierarchy.hierarchyIndex[target.name] + if (node != null && node.allLeafs.size == 1 && node.allLeafs.first() != node.node.name) { + throw IllegalStateException( + "Can't use target aliases as one of the this dump's targets" + + " has the same name as a group in the default targets hierarchy: ${target.name}" + ) + } + } + return GroupingFormatter(targets) + } else { + DefaultFormatter + } + } + + /** + * Remove the [target] from this dump. + * If some declaration was declared only for [target], it will be removed from the dump. + */ + fun remove(target: Target) { + if (!targetsMut.contains(target)) { + return + } + + targetsMut.remove(target) + topLevelDeclaration.remove(target) + } + + /** + * Leave only declarations specific to a [target]. + * A declaration is considered target-specific if: + * 1) it defined for some [targets] subset including [target], but not for all [targets]; + * 2) it defined for all [targets], but contains target-specific child declaration. + */ + fun retainTargetSpecificAbi(target: Target) { + if (!targetsMut.contains(target)) { + targetsMut.clear() + topLevelDeclaration.children.clear() + topLevelDeclaration.targets.clear() + return + } + + topLevelDeclaration.retainSpecific(target, targetsMut) + targetsMut.retainAll(setOf(target)) + } + + /** + * Remove all declarations that are not defined for all [KlibAbiDumpMerger.targets]. + */ + fun retainCommonAbi() { + topLevelDeclaration.retainCommon(targetsMut) + if (topLevelDeclaration.children.isEmpty()) { + targetsMut.clear() + } + } + + /** + * Merge the [other] dump containing declarations for a single target into this dump. + * The dump [other] should contain exactly one target and this dump should not contain that target. + */ + fun mergeTargetSpecific(other: KlibAbiDumpMerger) { + require(other.targetsMut.size == 1) { + "The dump to merge in should have a single target, but its targets are: ${other.targets}" + } + require(other.targetsMut.first() !in targetsMut) { + "Targets of this dump and the dump to merge into it should not intersect. " + + "Common target: ${other.targets.first()}}" + } + + targetsMut.addAll(other.targetsMut) + topLevelDeclaration.mergeTargetSpecific(other.topLevelDeclaration) + } + + /** + * For each declaration change targets to a specified [targets] set. + */ + fun overrideTargets(targets: Set) { + targetsMut.clear() + targetsMut.addAll(targets) + + topLevelDeclaration.overrideTargets(targets) + } +} + +private class DeclarationContainer(val text: String, val parent: DeclarationContainer? = null) { + val targets: MutableSet = mutableSetOf() + val children: MutableList = mutableListOf() + var delimiter: String? = null + private val childrenCache: MutableMap = mutableMapOf() + + fun createOrUpdateChildren(text: String, targets: Set): DeclarationContainer { + val child = childrenCache.computeIfAbsent(text) { + val newChild = DeclarationContainer(it, this) + children.add(newChild) + newChild + } + child.targets.addAll(targets) + return child + } + + fun dump( + appendable: Appendable, allTargets: Set, + dumpFormat: KlibAbiDumpFormat, formatter: KLibsTargetsFormatter + ) { + if (targets != allTargets && dumpFormat.includeTargets) { + // Use the same indentation for target list as for the declaration itself + appendable.append(" ".repeat(text.depth() * INDENT_WIDTH)) + .append(formatter.formatDeclarationTargets(targets)) + .append('\n') + } + appendable.append(text).append('\n') + children.sortedWith(DeclarationsComparator).forEach { + it.dump(appendable, this.targets, dumpFormat, formatter) + } + if (delimiter != null) { + appendable.append(delimiter).append('\n') + } + } + + fun remove(target: Target) { + if (parent != null && !targets.contains(target)) { + return + } + + targets.remove(target) + children.removeIf { + val shouldRemove = it.targets.contains(target) && it.targets.size == 1 + if (shouldRemove) { + childrenCache.remove(it.text) + } + shouldRemove + } + children.forEach { it.remove(target) } + } + + fun retainSpecific(target: Target, allTargets: Set) { + if (parent != null && !targets.contains(target)) { + children.clear() + targets.clear() + return + } + + children.forEach { it.retainSpecific(target, allTargets) } + children.removeIf { it.targets.isEmpty() } + if (targets == allTargets) { + if (children.isEmpty()) { + targets.clear() + } else { + targets.retainAll(setOf(target)) + } + } else { + targets.retainAll(setOf(target)) + } + } + + fun retainCommon(commonTargets: Set) { + if (parent != null && targets != commonTargets) { + children.clear() + targets.clear() + return + } + children.forEach { it.retainCommon(commonTargets) } + children.removeIf { it.targets.isEmpty() } + } + + fun mergeTargetSpecific(other: DeclarationContainer) { + targets.addAll(other.targets) + val newChildren = mutableListOf() + other.children.forEach { otherChild -> + val child = children.find { it.text == otherChild.text } + if (child != null) { + child.mergeTargetSpecific(otherChild) + } else { + newChildren.add(otherChild) + } + } + children.forEach { + if (other.targets.first() !in it.targets) { + it.addTargetRecursively(other.targets.first()) + } + } + children.addAll(newChildren) + } + + private fun addTargetRecursively(first: Target) { + targets.add(first) + children.forEach { it.addTargetRecursively(first) } + } + + fun overrideTargets(targets: Set) { + this.targets.clear() + this.targets.addAll(targets) + + children.forEach { it.overrideTargets(targets) } + } +} + +// TODO: optimize +private object DeclarationsComparator : Comparator { + override fun compare(c0: DeclarationContainer, c1: DeclarationContainer): Int { + return if (c0.targets == c1.targets) { + c0.text.compareTo(c1.text) + } else { + if (c0.targets.size == c1.targets.size) { + val c0targets = c0.targets.asSequence().map { it.name }.sorted().iterator() + val c1targets = c1.targets.asSequence().map { it.name }.sorted().iterator() + var result = 0 + while (c1targets.hasNext() && c0targets.hasNext() && result == 0) { + result = c0targets.next().compareTo(c1targets.next()) + } + result + } else { + // longer the target list, earlier the declaration would appear + c1.targets.size.compareTo(c0.targets.size) + } + } + } +} + +private interface KLibsTargetsFormatter { + fun formatHeader(targets: Set): String + + fun formatDeclarationTargets(targets: Set): String +} + +private object DefaultFormatter : KLibsTargetsFormatter { + override fun formatHeader(targets: Set): String { + return formatDeclarationTargets(targets) + } + + override fun formatDeclarationTargets(targets: Set): String { + return targets.sortedBy { it.name } + .joinToString(TARGETS_DELIMITER, TARGETS_LIST_PREFIX, TARGETS_LIST_SUFFIX) { it.name } + } +} + +private class GroupingFormatter(allTargets: Set) : KLibsTargetsFormatter { + private data class Alias(val name: String, val targets: Set) + + private val aliases: List + + init { + val aliasesBuilder = mutableListOf() + TargetHierarchy.hierarchyIndex.asSequence() + // place smaller groups (more specific groups) closer to beginning of the list + .sortedWith(compareBy({ it.value.allLeafs.size }, { it.key })) + .forEach { + // intersect with all targets to use only enabled targets in aliases + val availableTargets = it.value.allLeafs.map { + Target(it) + }.intersect(allTargets) + if (availableTargets.isNotEmpty()) { + aliasesBuilder.add(Alias(it.key, availableTargets)) + } + } + + // filter out all groups consisting of less than one member + aliasesBuilder.removeIf { it.targets.size < 2 } + aliasesBuilder.removeIf { it.targets == allTargets } + // Remove all duplicating groups. At this point, aliases are sorted so + // that more specific groups are before more common groups, so we'll remove + // more common groups here. + val toRemove = mutableListOf() + for (i in aliasesBuilder.indices) { + for (j in i + 1 until aliasesBuilder.size) { + if (aliasesBuilder[j].targets == aliasesBuilder[i].targets) { + toRemove.add(j) + } + } + } + toRemove.sortDescending() + toRemove.forEach { + aliasesBuilder.removeAt(it) + } + // reverse the order to place common group first + aliases = aliasesBuilder.reversed() + } + + override fun formatHeader(targets: Set): String { + return buildString { + append( + targets.asSequence().map { it.name }.sorted().joinToString( + prefix = TARGETS_LIST_PREFIX, + postfix = TARGETS_LIST_SUFFIX, + separator = TARGETS_DELIMITER + ) + ) + aliases.forEach { + append("\n// Alias: ${it.name} => [") + append(it.targets.map { it.name }.sorted().joinToString(TARGETS_DELIMITER)) + append(TARGETS_LIST_SUFFIX) + } + } + } + + override fun formatDeclarationTargets(targets: Set): String { + val targetsMut = targets.toMutableSet() + val resultingTargets = mutableListOf() + for (alias in aliases) { + if (targetsMut.containsAll(alias.targets)) { + targetsMut.removeAll(alias.targets) + resultingTargets.add(alias.name) + } + } + resultingTargets.addAll(targetsMut.map { it.name }) + return resultingTargets.sorted().joinToString( + prefix = TARGETS_LIST_PREFIX, + postfix = TARGETS_LIST_SUFFIX, + separator = TARGETS_DELIMITER + ) + } +} diff --git a/src/test/kotlin/tests/KlibAbiMergingTest.kt b/src/test/kotlin/tests/KlibAbiMergingTest.kt new file mode 100644 index 00000000..26d32929 --- /dev/null +++ b/src/test/kotlin/tests/KlibAbiMergingTest.kt @@ -0,0 +1,267 @@ +/* + * Copyright 2016-2023 JetBrains s.r.o. + * Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file. + */ + +package tests + +import kotlinx.validation.klib.KlibAbiDumpFormat +import kotlinx.validation.klib.KlibAbiDumpMerger +import kotlinx.validation.klib.Target +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TemporaryFolder +import java.io.File +import java.io.FileWriter +import java.nio.file.Files +import java.util.UUID +import kotlin.random.Random +import kotlin.test.assertContentEquals +import kotlin.test.assertFails +import kotlin.test.assertFailsWith + +class KlibAbiMergingTest { + @JvmField + @Rule + val tempDir = TemporaryFolder() + + private fun file(name: String): File { + val res = KlibAbiMergingTest::class.java.getResourceAsStream(name) + ?: throw IllegalStateException("Resource not found: $name") + val tempFile = File(tempDir.root, UUID.randomUUID().toString()) + Files.copy(res, tempFile.toPath()) + return tempFile + } + + private fun lines(name: String): Sequence { + val res = KlibAbiMergingTest::class.java.getResourceAsStream(name) + ?: throw IllegalStateException("Resource not found: $name") + return res.bufferedReader().lineSequence() + } + + private fun dumpToFile(klib: KlibAbiDumpMerger, + singleTargetDump: Boolean = false, + useAliases: Boolean = false): File { + val file = tempDir.newFile() + FileWriter(file).use { + klib.dump(it, KlibAbiDumpFormat( + includeTargets = !singleTargetDump, + useGroupAliases = useAliases + )) + } + return file + } + + @Test + fun identicalDumpFiles() { + val klib = KlibAbiDumpMerger() + listOf(Target("macosArm64"), Target("linuxX64")).forEach { + klib.addIndividualDump(it, file("/merge/identical/dump.abi")) + } + val merged = dumpToFile(klib) + + assertContentEquals( + lines("/merge/identical/merged.abi"), + Files.readAllLines(merged.toPath()).asSequence() + ) + } + + @Test + fun identicalDumpFilesWithAliases() { + val klib = KlibAbiDumpMerger() + listOf(Target("macosArm64"), Target("linuxX64")).forEach { + klib.addIndividualDump(it, file("/merge/identical/dump.abi")) + } + val merged = dumpToFile(klib, useAliases = true) + + // there are no groups other than "all", so no aliases will be added + assertContentEquals( + lines("/merge/identical/merged.abi"), + Files.readAllLines(merged.toPath()).asSequence() + ) + } + + @Test + fun divergingDumpFiles() { + val klib = KlibAbiDumpMerger() + val random = Random(42) + for (i in 0 until 10) { + val targets = mutableListOf("androidNativeArm64", "linuxArm64", "linuxX64", "tvOsX64") + targets.shuffle(random) + targets.forEach { + klib.addIndividualDump(Target(it), file("/merge/diverging/$it.api")) + } + val merged = dumpToFile(klib) + assertContentEquals( + lines("/merge/diverging/merged.abi"), + Files.readAllLines(merged.toPath()).asSequence(), + merged.readText() + ) + } + } + + @Test + fun divergingDumpFilesWithAliases() { + val klib = KlibAbiDumpMerger() + val random = Random(42) + for (i in 0 until 10) { + val targets = mutableListOf("androidNativeArm64", "linuxArm64", "linuxX64", "tvOsX64") + targets.shuffle(random) + targets.forEach { + klib.addIndividualDump(Target(it), file("/merge/diverging/$it.api")) + } + val merged = dumpToFile(klib, useAliases = true) + assertContentEquals( + lines("/merge/diverging/merged_with_aliases.abi"), + Files.readAllLines(merged.toPath()).asSequence() + ) + } + } + + @Test + fun aliasedDumpParsing() { + val klib = KlibAbiDumpMerger() + klib.loadMergedDump(file("/merge/diverging/merged_with_aliases.abi")) + + val withoutAliases = dumpToFile(klib, useAliases = false) + assertContentEquals( + lines("/merge/diverging/merged.abi"), + Files.readAllLines(withoutAliases.toPath()).asSequence() + ) + } + + @Test + fun mergeDumpsWithDivergedHeaders() { + val klib = KlibAbiDumpMerger() + klib.addIndividualDump( + Target("linuxArm64"), + file("/merge/header-mismatch/v1.abi") + ) + + assertFailsWith { + klib.addIndividualDump( + Target("linuxX64"), + file("/merge/header-mismatch/v2.abi") + ) + } + } + + @Test + fun overwriteAll() { + val klib = KlibAbiDumpMerger() + klib.loadMergedDump(file("/merge/diverging/merged.abi")) + + val targets = listOf("androidNativeArm64", "linuxArm64", "linuxX64", "tvOsX64") + targets.forEach { target -> + klib.remove(Target(target)) + klib.addIndividualDump(Target(target), file("/merge/diverging/$target.api")) + } + + val merged = dumpToFile(klib) + + assertContentEquals( + lines("/merge/diverging/merged.abi"), + Files.readAllLines(merged.toPath()).asSequence() + ) + } + + @Test + fun read() { + val klib = KlibAbiDumpMerger() + klib.loadMergedDump(file("/merge/idempotent/bcv-klib-test.abi")) + + val written = dumpToFile(klib) + assertContentEquals( + lines("/merge/idempotent/bcv-klib-test.abi"), + Files.readAllLines(written.toPath()).asSequence() + ) + } + + @Test + fun readDeclarationWithNarrowerChildrenDeclarations() { + val klib = KlibAbiDumpMerger() + klib.loadMergedDump(file("/merge/parseNarrowChildrenDecls/merged.abi")) + + klib.remove(Target("linuxArm64")) + val written1 = dumpToFile(klib) + assertContentEquals( + lines("/merge/parseNarrowChildrenDecls/withoutLinuxArm64.abi"), + Files.readAllLines(written1.toPath()).asSequence() + ) + + klib.remove(Target("linuxX64")) + val written2 = dumpToFile(klib) + assertContentEquals( + lines("/merge/parseNarrowChildrenDecls/withoutLinuxAll.abi"), + Files.readAllLines(written2.toPath()).asSequence() + ) + } + + @Test + fun guessAbi() { + val klib = KlibAbiDumpMerger() + klib.loadMergedDump(file("/merge/guess/merged.api")) + klib.retainTargetSpecificAbi(Target("linuxArm64")) + + val retainedLinuxAbiDump = dumpToFile(klib) + assertContentEquals( + lines("/merge/guess/linuxArm64Specific.api"), + Files.readAllLines(retainedLinuxAbiDump.toPath()).asSequence() + ) + + val commonAbi = KlibAbiDumpMerger() + commonAbi.loadMergedDump(file("/merge/guess/merged.api")) + commonAbi.remove(Target("linuxArm64")) + commonAbi.retainCommonAbi() + + val commonAbiDump = dumpToFile(commonAbi) + assertContentEquals( + lines("/merge/guess/common.api"), + Files.readAllLines(commonAbiDump.toPath()).asSequence() + ) + + commonAbi.mergeTargetSpecific(klib) + commonAbi.overrideTargets(setOf(Target("linuxArm64"))) + + val guessedAbiDump = dumpToFile(commonAbi, true) + assertContentEquals( + lines("/merge/guess/guessed.api"), + Files.readAllLines(guessedAbiDump.toPath()).asSequence() + ) + } + + @Test + fun loadInvalidFile() { + assertFails { + KlibAbiDumpMerger().loadMergedDump(file("/merge/illegalFiles/emptyFile.txt")) + } + + assertFails { + KlibAbiDumpMerger().loadMergedDump(file("/merge/illegalFiles/nonDumpFile.txt")) + } + + assertFails { + // Not a merged dump + KlibAbiDumpMerger().loadMergedDump(file("/merge/diverging/linuxArm64.api")) + } + + assertFails { + KlibAbiDumpMerger().addIndividualDump( + Target("linuxX64"), file("/merge/illegalFiles/emptyFile.txt") + ) + } + + assertFails { + KlibAbiDumpMerger().addIndividualDump( + Target("linuxX64"), file("/merge/illegalFiles/nonDumpFile.txt") + ) + } + + assertFails { + // Not a single-target dump + KlibAbiDumpMerger().addIndividualDump( + Target("linuxX64"), file("/merge/diverging/merged.api") + ) + } + } +} diff --git a/src/test/resources/merge/diverging/androidNativeArm64.api b/src/test/resources/merge/diverging/androidNativeArm64.api new file mode 100644 index 00000000..9bc95b04 --- /dev/null +++ b/src/test/resources/merge/diverging/androidNativeArm64.api @@ -0,0 +1,18 @@ +// Rendering settings: +// - Signature version: 2 +// - Show manifest properties: false +// - Show declarations: true + +// Library unique name: +final class org.example/ShardedClass { // org.example/ShardedClass|null[0] + final val value // org.example/ShardedClass.value|{}value[0] + final fun (): kotlin/Int // org.example/ShardedClass.value.|(){}[0] + constructor (kotlin/Int) // org.example/ShardedClass.|(kotlin.Int){}[0] + final fun add(kotlin/Int): kotlin/Int // org.example/ShardedClass.add|add(kotlin.Int){}[0] +} +final class org.example/X { // org.example/X|null[0] + constructor (kotlin/Int) // org.example/X.|(kotlin.Int){}[0] +} +final fun org.example/ShardedClass(kotlin/Int, kotlin/Float, kotlin/Long): org.example/ShardedClass // org.example/ShardedClass|ShardedClass(kotlin.Int;kotlin.Float;kotlin.Long){}[0] +final fun org.example/add(org.example/X, org.example/X): org.example/X // org.example/add|add(org.example.X;org.example.X){}[0] +final fun (org.example/X).org.example/add(org.example/X): org.example/X // org.example/add|add@org.example.X(org.example.X){}[0] diff --git a/src/test/resources/merge/diverging/linuxArm64.api b/src/test/resources/merge/diverging/linuxArm64.api new file mode 100644 index 00000000..cdaeeaba --- /dev/null +++ b/src/test/resources/merge/diverging/linuxArm64.api @@ -0,0 +1,15 @@ +// Rendering settings: +// - Signature version: 2 +// - Show manifest properties: false +// - Show declarations: true + +// Library unique name: +final class org.example/ShardedClass { // org.example/ShardedClass|null[0] + final val value // org.example/ShardedClass.value|{}value[0] + final fun (): kotlin/Int // org.example/ShardedClass.value.|(){}[0] + constructor (kotlin/Int) // org.example/ShardedClass.|(kotlin.Int){}[0] + final fun add(kotlin/Int): kotlin/Int // org.example/ShardedClass.add|add(kotlin.Int){}[0] +} +final fun org.example/ShardedClass(kotlin/Int, kotlin/Float, kotlin/Long): org.example/ShardedClass // org.example/ShardedClass|ShardedClass(kotlin.Int;kotlin.Float;kotlin.Long){}[0] +final fun org.example/add(kotlin/Long, kotlin/Long): kotlin/Long // org.example/add|add(kotlin.Long;kotlin.Long){}[0] +final fun (kotlin/Long).org.example/add(kotlin/Long): kotlin/Long // org.example/add|add@kotlin.Long(kotlin.Long){}[0] diff --git a/src/test/resources/merge/diverging/linuxX64.api b/src/test/resources/merge/diverging/linuxX64.api new file mode 100644 index 00000000..cdaeeaba --- /dev/null +++ b/src/test/resources/merge/diverging/linuxX64.api @@ -0,0 +1,15 @@ +// Rendering settings: +// - Signature version: 2 +// - Show manifest properties: false +// - Show declarations: true + +// Library unique name: +final class org.example/ShardedClass { // org.example/ShardedClass|null[0] + final val value // org.example/ShardedClass.value|{}value[0] + final fun (): kotlin/Int // org.example/ShardedClass.value.|(){}[0] + constructor (kotlin/Int) // org.example/ShardedClass.|(kotlin.Int){}[0] + final fun add(kotlin/Int): kotlin/Int // org.example/ShardedClass.add|add(kotlin.Int){}[0] +} +final fun org.example/ShardedClass(kotlin/Int, kotlin/Float, kotlin/Long): org.example/ShardedClass // org.example/ShardedClass|ShardedClass(kotlin.Int;kotlin.Float;kotlin.Long){}[0] +final fun org.example/add(kotlin/Long, kotlin/Long): kotlin/Long // org.example/add|add(kotlin.Long;kotlin.Long){}[0] +final fun (kotlin/Long).org.example/add(kotlin/Long): kotlin/Long // org.example/add|add@kotlin.Long(kotlin.Long){}[0] diff --git a/src/test/resources/merge/diverging/merged.abi b/src/test/resources/merge/diverging/merged.abi new file mode 100644 index 00000000..ab99abaa --- /dev/null +++ b/src/test/resources/merge/diverging/merged.abi @@ -0,0 +1,31 @@ +// Merged KLib ABI Dump +// Targets: [androidNativeArm64, linuxArm64, linuxX64, tvOsX64] +// Rendering settings: +// - Signature version: 2 +// - Show manifest properties: false +// - Show declarations: true + +// Library unique name: +final class org.example/ShardedClass { // org.example/ShardedClass|null[0] + constructor (kotlin/Int) // org.example/ShardedClass.|(kotlin.Int){}[0] + final fun add(kotlin/Int): kotlin/Int // org.example/ShardedClass.add|add(kotlin.Int){}[0] + final val value // org.example/ShardedClass.value|{}value[0] + final fun (): kotlin/Int // org.example/ShardedClass.value.|(){}[0] +} +final fun org.example/ShardedClass(kotlin/Int, kotlin/Float, kotlin/Long): org.example/ShardedClass // org.example/ShardedClass|ShardedClass(kotlin.Int;kotlin.Float;kotlin.Long){}[0] +// Targets: [linuxArm64, linuxX64] +final fun (kotlin/Long).org.example/add(kotlin/Long): kotlin/Long // org.example/add|add@kotlin.Long(kotlin.Long){}[0] +// Targets: [linuxArm64, linuxX64] +final fun org.example/add(kotlin/Long, kotlin/Long): kotlin/Long // org.example/add|add(kotlin.Long;kotlin.Long){}[0] +// Targets: [androidNativeArm64] +final class org.example/X { // org.example/X|null[0] + constructor (kotlin/Int) // org.example/X.|(kotlin.Int){}[0] +} +// Targets: [androidNativeArm64] +final fun (org.example/X).org.example/add(org.example/X): org.example/X // org.example/add|add@org.example.X(org.example.X){}[0] +// Targets: [androidNativeArm64] +final fun org.example/add(org.example/X, org.example/X): org.example/X // org.example/add|add(org.example.X;org.example.X){}[0] +// Targets: [tvOsX64] +final fun (kotlin/Int).org.example/add(kotlin/Int): kotlin/Int // org.example/add|add@kotlin.Int(kotlin.Int){}[0] +// Targets: [tvOsX64] +final fun org.example/add(kotlin/Int, kotlin/Int): kotlin/Int // org.example/add|add(kotlin.Int;kotlin.Int){}[0] diff --git a/src/test/resources/merge/diverging/merged_with_aliases.abi b/src/test/resources/merge/diverging/merged_with_aliases.abi new file mode 100644 index 00000000..868e8f88 --- /dev/null +++ b/src/test/resources/merge/diverging/merged_with_aliases.abi @@ -0,0 +1,33 @@ +// Merged KLib ABI Dump +// Targets: [androidNativeArm64, linuxArm64, linuxX64, tvOsX64] +// Alias: native => [androidNativeArm64, linuxArm64, linuxX64] +// Alias: linux => [linuxArm64, linuxX64] +// Rendering settings: +// - Signature version: 2 +// - Show manifest properties: false +// - Show declarations: true + +// Library unique name: +final class org.example/ShardedClass { // org.example/ShardedClass|null[0] + constructor (kotlin/Int) // org.example/ShardedClass.|(kotlin.Int){}[0] + final fun add(kotlin/Int): kotlin/Int // org.example/ShardedClass.add|add(kotlin.Int){}[0] + final val value // org.example/ShardedClass.value|{}value[0] + final fun (): kotlin/Int // org.example/ShardedClass.value.|(){}[0] +} +final fun org.example/ShardedClass(kotlin/Int, kotlin/Float, kotlin/Long): org.example/ShardedClass // org.example/ShardedClass|ShardedClass(kotlin.Int;kotlin.Float;kotlin.Long){}[0] +// Targets: [linux] +final fun (kotlin/Long).org.example/add(kotlin/Long): kotlin/Long // org.example/add|add@kotlin.Long(kotlin.Long){}[0] +// Targets: [linux] +final fun org.example/add(kotlin/Long, kotlin/Long): kotlin/Long // org.example/add|add(kotlin.Long;kotlin.Long){}[0] +// Targets: [androidNativeArm64] +final class org.example/X { // org.example/X|null[0] + constructor (kotlin/Int) // org.example/X.|(kotlin.Int){}[0] +} +// Targets: [androidNativeArm64] +final fun (org.example/X).org.example/add(org.example/X): org.example/X // org.example/add|add@org.example.X(org.example.X){}[0] +// Targets: [androidNativeArm64] +final fun org.example/add(org.example/X, org.example/X): org.example/X // org.example/add|add(org.example.X;org.example.X){}[0] +// Targets: [tvOsX64] +final fun (kotlin/Int).org.example/add(kotlin/Int): kotlin/Int // org.example/add|add@kotlin.Int(kotlin.Int){}[0] +// Targets: [tvOsX64] +final fun org.example/add(kotlin/Int, kotlin/Int): kotlin/Int // org.example/add|add(kotlin.Int;kotlin.Int){}[0] diff --git a/src/test/resources/merge/diverging/tvOsX64.api b/src/test/resources/merge/diverging/tvOsX64.api new file mode 100644 index 00000000..a23dd25d --- /dev/null +++ b/src/test/resources/merge/diverging/tvOsX64.api @@ -0,0 +1,15 @@ +// Rendering settings: +// - Signature version: 2 +// - Show manifest properties: false +// - Show declarations: true + +// Library unique name: +final class org.example/ShardedClass { // org.example/ShardedClass|null[0] + final val value // org.example/ShardedClass.value|{}value[0] + final fun (): kotlin/Int // org.example/ShardedClass.value.|(){}[0] + constructor (kotlin/Int) // org.example/ShardedClass.|(kotlin.Int){}[0] + final fun add(kotlin/Int): kotlin/Int // org.example/ShardedClass.add|add(kotlin.Int){}[0] +} +final fun org.example/ShardedClass(kotlin/Int, kotlin/Float, kotlin/Long): org.example/ShardedClass // org.example/ShardedClass|ShardedClass(kotlin.Int;kotlin.Float;kotlin.Long){}[0] +final fun org.example/add(kotlin/Int, kotlin/Int): kotlin/Int // org.example/add|add(kotlin.Int;kotlin.Int){}[0] +final fun (kotlin/Int).org.example/add(kotlin/Int): kotlin/Int // org.example/add|add@kotlin.Int(kotlin.Int){}[0] diff --git a/src/test/resources/merge/guess/common.api b/src/test/resources/merge/guess/common.api new file mode 100644 index 00000000..75a14693 --- /dev/null +++ b/src/test/resources/merge/guess/common.api @@ -0,0 +1,14 @@ +// Merged KLib ABI Dump +// Targets: [androidNativeArm64, linuxX64, tvOsX64] +// Rendering settings: +// - Signature version: 2 +// - Show manifest properties: false +// - Show declarations: true + +// Library unique name: +final class org.example/ShardedClass { // org.example/ShardedClass|null[0] + constructor (kotlin/Int) // org.example/ShardedClass.|(kotlin.Int){}[0] + final fun add(kotlin/Int): kotlin/Int // org.example/ShardedClass.add|add(kotlin.Int){}[0] + final val value // org.example/ShardedClass.value|{}value[0] + final fun (): kotlin/Int // org.example/ShardedClass.value.|(){}[0] +} diff --git a/src/test/resources/merge/guess/guessed.api b/src/test/resources/merge/guess/guessed.api new file mode 100644 index 00000000..c06e2247 --- /dev/null +++ b/src/test/resources/merge/guess/guessed.api @@ -0,0 +1,15 @@ +// Rendering settings: +// - Signature version: 2 +// - Show manifest properties: false +// - Show declarations: true + +// Library unique name: +final class org.example/ShardedClass { // org.example/ShardedClass|null[0] + constructor (kotlin/Int) // org.example/ShardedClass.|(kotlin.Int){}[0] + final fun add(kotlin/Int): kotlin/Int // org.example/ShardedClass.add|add(kotlin.Int){}[0] + final fun addNarrow(kotlin/Int): kotlin/Int // org.example/ShardedClass.addNarrow|addNarrow(kotlin.Int){}[0] + final val value // org.example/ShardedClass.value|{}value[0] + final fun (): kotlin/Int // org.example/ShardedClass.value.|(){}[0] +} +final fun (org.example/X).org.example/add(org.example/X): org.example/X // org.example/add|add@org.example.X(org.example.X){}[0] +final fun org.example/add(org.example/X, org.example/X): org.example/X // org.example/add|add(org.example.X;org.example.X){}[0] diff --git a/src/test/resources/merge/guess/linuxArm64Specific.api b/src/test/resources/merge/guess/linuxArm64Specific.api new file mode 100644 index 00000000..51e56fbe --- /dev/null +++ b/src/test/resources/merge/guess/linuxArm64Specific.api @@ -0,0 +1,13 @@ +// Merged KLib ABI Dump +// Targets: [linuxArm64] +// Rendering settings: +// - Signature version: 2 +// - Show manifest properties: false +// - Show declarations: true + +// Library unique name: +final class org.example/ShardedClass { // org.example/ShardedClass|null[0] + final fun addNarrow(kotlin/Int): kotlin/Int // org.example/ShardedClass.addNarrow|addNarrow(kotlin.Int){}[0] +} +final fun (org.example/X).org.example/add(org.example/X): org.example/X // org.example/add|add@org.example.X(org.example.X){}[0] +final fun org.example/add(org.example/X, org.example/X): org.example/X // org.example/add|add(org.example.X;org.example.X){}[0] diff --git a/src/test/resources/merge/guess/merged.api b/src/test/resources/merge/guess/merged.api new file mode 100644 index 00000000..9279929a --- /dev/null +++ b/src/test/resources/merge/guess/merged.api @@ -0,0 +1,24 @@ +// Merged KLib ABI Dump +// Targets: [androidNativeArm64, linuxArm64, linuxX64, tvOsX64] +// Rendering settings: +// - Signature version: 2 +// - Show manifest properties: false +// - Show declarations: true + +// Library unique name: +final class org.example/ShardedClass { // org.example/ShardedClass|null[0] + constructor (kotlin/Int) // org.example/ShardedClass.|(kotlin.Int){}[0] + final fun add(kotlin/Int): kotlin/Int // org.example/ShardedClass.add|add(kotlin.Int){}[0] + final val value // org.example/ShardedClass.value|{}value[0] + final fun (): kotlin/Int // org.example/ShardedClass.value.|(){}[0] + // Targets: [linuxArm64, linuxX64] + final fun addNarrow(kotlin/Int): kotlin/Int // org.example/ShardedClass.addNarrow|addNarrow(kotlin.Int){}[0] +} +// Targets: [linuxArm64] +final fun (org.example/X).org.example/add(org.example/X): org.example/X // org.example/add|add@org.example.X(org.example.X){}[0] +// Targets: [linuxArm64, tvOsX64] +final fun org.example/add(org.example/X, org.example/X): org.example/X // org.example/add|add(org.example.X;org.example.X){}[0] +// Targets: [tvOsX64] +final fun (kotlin/Int).org.example/add(kotlin/Int): kotlin/Int // org.example/add|add@kotlin.Int(kotlin.Int){}[0] +// Targets: [tvOsX64] +final fun org.example/add(kotlin/Int, kotlin/Int): kotlin/Int // org.example/add|add(kotlin.Int;kotlin.Int){}[0] diff --git a/src/test/resources/merge/header-mismatch/v1.abi b/src/test/resources/merge/header-mismatch/v1.abi new file mode 100644 index 00000000..cbc8741a --- /dev/null +++ b/src/test/resources/merge/header-mismatch/v1.abi @@ -0,0 +1,13 @@ +// Rendering settings: +// - Signature version: 1 +// - Show manifest properties: false +// - Show declarations: true + +// Library unique name: +final class org.example/ShardedClass { // org.example/ShardedClass|null[0] + final val value // org.example/ShardedClass.value|1987073854177347439[0] + final fun (): kotlin/Int // org.example/ShardedClass.value.|3260093555963109437[0] + constructor (kotlin/Int) // org.example/ShardedClass.|-5182794243525578284[0] + final fun add(kotlin/Int): kotlin/Int // org.example/ShardedClass.add|4888650976871417104[0] +} +final fun org.example/ShardedClass(kotlin/Int, kotlin/Float, kotlin/Long): org.example/ShardedClass // org.example/ShardedClass|-4796080257537853433[0] diff --git a/src/test/resources/merge/header-mismatch/v2.abi b/src/test/resources/merge/header-mismatch/v2.abi new file mode 100644 index 00000000..086d9048 --- /dev/null +++ b/src/test/resources/merge/header-mismatch/v2.abi @@ -0,0 +1,13 @@ +// Rendering settings: +// - Signature version: 2 +// - Show manifest properties: false +// - Show declarations: true + +// Library unique name: +final class org.example/ShardedClass { // org.example/ShardedClass|null[0] + final val value // org.example/ShardedClass.value|{}value[0] + final fun (): kotlin/Int // org.example/ShardedClass.value.|(){}[0] + constructor (kotlin/Int) // org.example/ShardedClass.|(kotlin.Int){}[0] + final fun add(kotlin/Int): kotlin/Int // org.example/ShardedClass.add|add(kotlin.Int){}[0] +} +final fun org.example/ShardedClass(kotlin/Int, kotlin/Float, kotlin/Long): org.example/ShardedClass // org.example/ShardedClass|ShardedClass(kotlin.Int;kotlin.Float;kotlin.Long){}[0] diff --git a/src/test/resources/merge/idempotent/bcv-klib-test.abi b/src/test/resources/merge/idempotent/bcv-klib-test.abi new file mode 100644 index 00000000..8a4abf9a --- /dev/null +++ b/src/test/resources/merge/idempotent/bcv-klib-test.abi @@ -0,0 +1,31 @@ +// Merged KLib ABI Dump +// Targets: [androidNativeArm32, androidNativeArm64, iosSimulatorArm64, linuxArm64, linuxX64, macosArm64, tvosArm64, tvosSimulatorArm64, tvosX64] +// Rendering settings: +// - Signature version: 2 +// - Show manifest properties: false +// - Show declarations: true + +// Library unique name: +final class org.example/ShardedClass { // org.example/ShardedClass|null[0] + constructor (kotlin/Int) // org.example/ShardedClass.|(kotlin.Int){}[0] + final fun add(kotlin/Int): kotlin/Int // org.example/ShardedClass.add|add(kotlin.Int){}[0] + final val value // org.example/ShardedClass.value|{}value[0] + final fun (): kotlin/Int // org.example/ShardedClass.value.|(){}[0] +} +final fun org.example/ShardedClass(kotlin/Int, kotlin/Float, kotlin/Long): org.example/ShardedClass // org.example/ShardedClass|ShardedClass(kotlin.Int;kotlin.Float;kotlin.Long){}[0] +// Targets: [iosSimulatorArm64, macosArm64, tvosArm64, tvosSimulatorArm64, tvosX64] +final fun (kotlin/Int).org.example/add(kotlin/Int): kotlin/Int // org.example/add|add@kotlin.Int(kotlin.Int){}[0] +// Targets: [iosSimulatorArm64, macosArm64, tvosArm64, tvosSimulatorArm64, tvosX64] +final fun org.example/add(kotlin/Int, kotlin/Int): kotlin/Int // org.example/add|add(kotlin.Int;kotlin.Int){}[0] +// Targets: [androidNativeArm32, androidNativeArm64] +final class org.example/X { // org.example/X|null[0] + constructor (kotlin/Int) // org.example/X.|(kotlin.Int){}[0] +} +// Targets: [androidNativeArm32, androidNativeArm64] +final fun (org.example/X).org.example/add(org.example/X): org.example/X // org.example/add|add@org.example.X(org.example.X){}[0] +// Targets: [androidNativeArm32, androidNativeArm64] +final fun org.example/add(org.example/X, org.example/X): org.example/X // org.example/add|add(org.example.X;org.example.X){}[0] +// Targets: [linuxArm64, linuxX64] +final fun (kotlin/Long).org.example/add(kotlin/Long): kotlin/Long // org.example/add|add@kotlin.Long(kotlin.Long){}[0] +// Targets: [linuxArm64, linuxX64] +final fun org.example/add(kotlin/Long, kotlin/Long): kotlin/Long // org.example/add|add(kotlin.Long;kotlin.Long){}[0] diff --git a/src/test/resources/merge/identical/dump.abi b/src/test/resources/merge/identical/dump.abi new file mode 100644 index 00000000..086d9048 --- /dev/null +++ b/src/test/resources/merge/identical/dump.abi @@ -0,0 +1,13 @@ +// Rendering settings: +// - Signature version: 2 +// - Show manifest properties: false +// - Show declarations: true + +// Library unique name: +final class org.example/ShardedClass { // org.example/ShardedClass|null[0] + final val value // org.example/ShardedClass.value|{}value[0] + final fun (): kotlin/Int // org.example/ShardedClass.value.|(){}[0] + constructor (kotlin/Int) // org.example/ShardedClass.|(kotlin.Int){}[0] + final fun add(kotlin/Int): kotlin/Int // org.example/ShardedClass.add|add(kotlin.Int){}[0] +} +final fun org.example/ShardedClass(kotlin/Int, kotlin/Float, kotlin/Long): org.example/ShardedClass // org.example/ShardedClass|ShardedClass(kotlin.Int;kotlin.Float;kotlin.Long){}[0] diff --git a/src/test/resources/merge/identical/merged.abi b/src/test/resources/merge/identical/merged.abi new file mode 100644 index 00000000..6a72f1c6 --- /dev/null +++ b/src/test/resources/merge/identical/merged.abi @@ -0,0 +1,15 @@ +// Merged KLib ABI Dump +// Targets: [linuxX64, macosArm64] +// Rendering settings: +// - Signature version: 2 +// - Show manifest properties: false +// - Show declarations: true + +// Library unique name: +final class org.example/ShardedClass { // org.example/ShardedClass|null[0] + constructor (kotlin/Int) // org.example/ShardedClass.|(kotlin.Int){}[0] + final fun add(kotlin/Int): kotlin/Int // org.example/ShardedClass.add|add(kotlin.Int){}[0] + final val value // org.example/ShardedClass.value|{}value[0] + final fun (): kotlin/Int // org.example/ShardedClass.value.|(){}[0] +} +final fun org.example/ShardedClass(kotlin/Int, kotlin/Float, kotlin/Long): org.example/ShardedClass // org.example/ShardedClass|ShardedClass(kotlin.Int;kotlin.Float;kotlin.Long){}[0] diff --git a/src/test/resources/merge/illegalFiles/emptyFile.txt b/src/test/resources/merge/illegalFiles/emptyFile.txt new file mode 100644 index 00000000..e69de29b diff --git a/src/test/resources/merge/illegalFiles/nonDumpFile.txt b/src/test/resources/merge/illegalFiles/nonDumpFile.txt new file mode 100644 index 00000000..1a4cac6c --- /dev/null +++ b/src/test/resources/merge/illegalFiles/nonDumpFile.txt @@ -0,0 +1 @@ +I'm not a dump diff --git a/src/test/resources/merge/parseNarrowChildrenDecls/merged.abi b/src/test/resources/merge/parseNarrowChildrenDecls/merged.abi new file mode 100644 index 00000000..25f9df1e --- /dev/null +++ b/src/test/resources/merge/parseNarrowChildrenDecls/merged.abi @@ -0,0 +1,16 @@ +// Merged KLib ABI Dump +// Targets: [androidNativeArm64, linuxArm64, linuxX64, tvOsX64] +// Rendering settings: +// - Signature version: 2 +// - Show manifest properties: false +// - Show declarations: true + +// Library unique name: +final class org.example/ShardedClass { // org.example/ShardedClass|null[0] + constructor (kotlin/Int) // org.example/ShardedClass.|(kotlin.Int){}[0] + final fun add(kotlin/Int): kotlin/Int // org.example/ShardedClass.add|add(kotlin.Int){}[0] + final val value // org.example/ShardedClass.value|{}value[0] + final fun (): kotlin/Int // org.example/ShardedClass.value.|(){}[0] + // Targets: [linuxArm64, linuxX64] + final fun addNarrow(kotlin/Int): kotlin/Int // org.example/ShardedClass.addNarrow|addNarrow(kotlin.Int){}[0] +} diff --git a/src/test/resources/merge/parseNarrowChildrenDecls/withoutLinuxAll.abi b/src/test/resources/merge/parseNarrowChildrenDecls/withoutLinuxAll.abi new file mode 100644 index 00000000..6ff8e005 --- /dev/null +++ b/src/test/resources/merge/parseNarrowChildrenDecls/withoutLinuxAll.abi @@ -0,0 +1,14 @@ +// Merged KLib ABI Dump +// Targets: [androidNativeArm64, tvOsX64] +// Rendering settings: +// - Signature version: 2 +// - Show manifest properties: false +// - Show declarations: true + +// Library unique name: +final class org.example/ShardedClass { // org.example/ShardedClass|null[0] + constructor (kotlin/Int) // org.example/ShardedClass.|(kotlin.Int){}[0] + final fun add(kotlin/Int): kotlin/Int // org.example/ShardedClass.add|add(kotlin.Int){}[0] + final val value // org.example/ShardedClass.value|{}value[0] + final fun (): kotlin/Int // org.example/ShardedClass.value.|(){}[0] +} diff --git a/src/test/resources/merge/parseNarrowChildrenDecls/withoutLinuxArm64.abi b/src/test/resources/merge/parseNarrowChildrenDecls/withoutLinuxArm64.abi new file mode 100644 index 00000000..812f852d --- /dev/null +++ b/src/test/resources/merge/parseNarrowChildrenDecls/withoutLinuxArm64.abi @@ -0,0 +1,16 @@ +// Merged KLib ABI Dump +// Targets: [androidNativeArm64, linuxX64, tvOsX64] +// Rendering settings: +// - Signature version: 2 +// - Show manifest properties: false +// - Show declarations: true + +// Library unique name: +final class org.example/ShardedClass { // org.example/ShardedClass|null[0] + constructor (kotlin/Int) // org.example/ShardedClass.|(kotlin.Int){}[0] + final fun add(kotlin/Int): kotlin/Int // org.example/ShardedClass.add|add(kotlin.Int){}[0] + final val value // org.example/ShardedClass.value|{}value[0] + final fun (): kotlin/Int // org.example/ShardedClass.value.|(){}[0] + // Targets: [linuxX64] + final fun addNarrow(kotlin/Int): kotlin/Int // org.example/ShardedClass.addNarrow|addNarrow(kotlin.Int){}[0] +} From ba998f0b96d4b7e32a7cee583a7d91ff11977be8 Mon Sep 17 00:00:00 2001 From: Filipp Zhinkin Date: Wed, 31 Jan 2024 12:22:36 +0100 Subject: [PATCH 03/53] Added Klib-related settings --- src/main/kotlin/ApiValidationExtension.kt | 45 +++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/src/main/kotlin/ApiValidationExtension.kt b/src/main/kotlin/ApiValidationExtension.kt index ed09ba42..f1dba2dd 100644 --- a/src/main/kotlin/ApiValidationExtension.kt +++ b/src/main/kotlin/ApiValidationExtension.kt @@ -71,4 +71,49 @@ public open class ApiValidationExtension { * By default, it's `api`. */ public var apiDumpDirectory: String = "api" + + /** + * KLib ABI validation settings. + * + * @see KlibValidationSettings + */ + public val klib: KlibValidationSettings = KlibValidationSettings() + + /** + * Configure KLib AVI validation settings. + */ + public inline fun klib(block: KlibValidationSettings.() -> Unit) { + block(this.klib) + } +} + +/** + * Settings affecting KLib ABI validation. + */ +open class KlibValidationSettings { + /** + * Enables KLib ABI validation checks. + */ + public var enabled: Boolean = false + /** + * Specify which version of signature KLib ABI dump should contain. + */ + public var signatureVersion: Int = 2 + /** + * Fail validation if some build targets are not supported by the host compiler. + * By default, ABI dumped only for supported files will be validated. This option makes validation behavior + * more strict and treat having unsupported targets as an error. + */ + public var strictValidation: Boolean = false + /** + * For declarations available only on some targets, replace a comment listing all targets + * with a short alias. + * + * Group aliases are based on [the default hierarchy template](https://kotlinlang.org/docs/multiplatform-hierarchy.html#see-the-full-hierarchy-template) + * and enabled by default. + * + * Grouping will not be applied if a project has a target with a custom name that clashes + * with one of the group names. + */ + public var useTargetGroupAliases: Boolean = true } From 51f77564d32fecff8566595daebe807a3a0b2857 Mon Sep 17 00:00:00 2001 From: Filipp Zhinkin Date: Wed, 31 Jan 2024 12:22:57 +0100 Subject: [PATCH 04/53] Added a more precise Copy task --- src/main/kotlin/CopyFile.kt | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 src/main/kotlin/CopyFile.kt diff --git a/src/main/kotlin/CopyFile.kt b/src/main/kotlin/CopyFile.kt new file mode 100644 index 00000000..df9f6d79 --- /dev/null +++ b/src/main/kotlin/CopyFile.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2016-2024 JetBrains s.r.o. + * Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file. + */ + +package kotlinx.validation + +import org.gradle.api.DefaultTask +import org.gradle.api.tasks.InputFiles +import org.gradle.api.tasks.OutputFile +import org.gradle.api.tasks.TaskAction +import java.io.File +import java.nio.file.Files +import java.nio.file.StandardCopyOption + +// Built-in Gradle's Copy/Sync tasks accepts only a destination directory (not a single file) +// and registers it as an output dependency. If there's another task reading from that particular +// directory or writing into it, their input/output dependencies would clash and as long as +// there will be no explicit ordering or dependencies between these tasks, Gradle would be unhappy. +internal open class CopyFile : DefaultTask() { + @InputFiles + lateinit var from: File + + @OutputFile + lateinit var to: File + + @TaskAction + fun copy() { + Files.copy(from.toPath(), to.toPath(), StandardCopyOption.REPLACE_EXISTING) + } +} From c8249264059f4167d4a40e5e76d859eddf8439e1 Mon Sep 17 00:00:00 2001 From: Filipp Zhinkin Date: Wed, 31 Jan 2024 12:23:17 +0100 Subject: [PATCH 05/53] Implemented Klib validation --- build.gradle.kts | 1 + .../validation/api/BaseKotlinGradleTest.kt | 13 + .../kotlin/kotlinx/validation/api/TestDsl.kt | 43 ++ .../validation/test/IgnoredClassesTests.kt | 1 + .../validation/test/KLibVerificationTests.kt | 663 ++++++++++++++++++ .../validation/test/NonPublicMarkersTest.kt | 33 + .../validation/test/PublicMarkersTest.kt | 35 + .../AnotherBuildConfig.klib.clash.dump | 14 + .../AnotherBuildConfig.klib.custom.dump | 14 + .../classes/AnotherBuildConfig.klib.dump | 16 + ...AnotherBuildConfigLinux.klib.grouping.dump | 18 + .../classes/AnotherBuildConfigLinuxArm64.kt | 8 + ...notherBuildConfigLinuxArm64Extra.klib.dump | 18 + .../classes/ClassWithPublicMarkers.klib.dump | 45 ++ .../examples/classes/Empty.klib.dump | 6 + .../classes/HiddenDeclarations.klib.dump | 13 + .../examples/classes/HiddenDeclarations.kt | 43 ++ .../examples/classes/NonPublicMarkers.kt | 34 + .../examples/classes/Properties.klib.dump | 17 + .../resources/examples/classes/SubPackage.kt | 10 + .../examples/classes/Subclasses.dump | 7 + .../examples/classes/Subclasses.klib.dump | 16 + .../resources/examples/classes/Subclasses.kt | 16 + .../TopLevelDeclarations.klib.all.dump | 74 ++ .../classes/TopLevelDeclarations.klib.dump | 68 ++ .../TopLevelDeclarations.klib.unsup.dump | 67 ++ .../classes/TopLevelDeclarations.klib.v1.dump | 67 ++ .../TopLevelDeclarations.klib.with.linux.dump | 69 ++ .../examples/classes/TopLevelDeclarations.kt | 38 + .../enableJvmInWithNativePlugin.gradle.kts | 9 + .../gradle/base/withNativePlugin.gradle.kts | 38 + .../withNativePluginAndNoTargets.gradle.kts | 30 + ...withNativePluginAndSingleTarget.gradle.kts | 32 + .../appleTargets/targets.gradle.kts | 20 + .../grouping/clashingTargetNames.gradle.kts | 9 + .../grouping/customTargetNames.gradle.kts | 17 + .../ignoreSubclasses/ignore.gradle.kts | 9 + .../nonPublicMarkers/klib.gradle.kts | 13 + .../signatures/invalid.gradle.kts | 8 + .../configuration/signatures/v1.gradle.kts | 8 + .../unsupported/enforce.gradle.kts | 8 + .../BinaryCompatibilityValidatorPlugin.kt | 353 +++++++++- src/main/kotlin/BuildTaskBase.kt | 58 ++ src/main/kotlin/KotlinApiBuildTask.kt | 65 +- src/main/kotlin/KotlinApiCompareTask.kt | 77 +- src/main/kotlin/KotlinKlibAbiBuildTask.kt | 97 +++ ...otlinKlibExtractSupportedTargetsAbiTask.kt | 63 ++ ...linKlibInferAbiForUnsupportedTargetTask.kt | 113 +++ src/main/kotlin/KotlinKlibMergeAbiTask.kt | 57 ++ src/test/kotlin/tests/AbiTest.kt | 30 + 50 files changed, 2453 insertions(+), 128 deletions(-) create mode 100644 src/functionalTest/kotlin/kotlinx/validation/test/KLibVerificationTests.kt create mode 100644 src/functionalTest/resources/examples/classes/AnotherBuildConfig.klib.clash.dump create mode 100644 src/functionalTest/resources/examples/classes/AnotherBuildConfig.klib.custom.dump create mode 100644 src/functionalTest/resources/examples/classes/AnotherBuildConfig.klib.dump create mode 100644 src/functionalTest/resources/examples/classes/AnotherBuildConfigLinux.klib.grouping.dump create mode 100644 src/functionalTest/resources/examples/classes/AnotherBuildConfigLinuxArm64.kt create mode 100644 src/functionalTest/resources/examples/classes/AnotherBuildConfigLinuxArm64Extra.klib.dump create mode 100644 src/functionalTest/resources/examples/classes/ClassWithPublicMarkers.klib.dump create mode 100644 src/functionalTest/resources/examples/classes/Empty.klib.dump create mode 100644 src/functionalTest/resources/examples/classes/HiddenDeclarations.klib.dump create mode 100644 src/functionalTest/resources/examples/classes/HiddenDeclarations.kt create mode 100644 src/functionalTest/resources/examples/classes/NonPublicMarkers.kt create mode 100644 src/functionalTest/resources/examples/classes/Properties.klib.dump create mode 100644 src/functionalTest/resources/examples/classes/SubPackage.kt create mode 100644 src/functionalTest/resources/examples/classes/Subclasses.dump create mode 100644 src/functionalTest/resources/examples/classes/Subclasses.klib.dump create mode 100644 src/functionalTest/resources/examples/classes/Subclasses.kt create mode 100644 src/functionalTest/resources/examples/classes/TopLevelDeclarations.klib.all.dump create mode 100644 src/functionalTest/resources/examples/classes/TopLevelDeclarations.klib.dump create mode 100644 src/functionalTest/resources/examples/classes/TopLevelDeclarations.klib.unsup.dump create mode 100644 src/functionalTest/resources/examples/classes/TopLevelDeclarations.klib.v1.dump create mode 100644 src/functionalTest/resources/examples/classes/TopLevelDeclarations.klib.with.linux.dump create mode 100644 src/functionalTest/resources/examples/classes/TopLevelDeclarations.kt create mode 100644 src/functionalTest/resources/examples/gradle/base/enableJvmInWithNativePlugin.gradle.kts create mode 100644 src/functionalTest/resources/examples/gradle/base/withNativePlugin.gradle.kts create mode 100644 src/functionalTest/resources/examples/gradle/base/withNativePluginAndNoTargets.gradle.kts create mode 100644 src/functionalTest/resources/examples/gradle/base/withNativePluginAndSingleTarget.gradle.kts create mode 100644 src/functionalTest/resources/examples/gradle/configuration/appleTargets/targets.gradle.kts create mode 100644 src/functionalTest/resources/examples/gradle/configuration/grouping/clashingTargetNames.gradle.kts create mode 100644 src/functionalTest/resources/examples/gradle/configuration/grouping/customTargetNames.gradle.kts create mode 100644 src/functionalTest/resources/examples/gradle/configuration/ignoreSubclasses/ignore.gradle.kts create mode 100644 src/functionalTest/resources/examples/gradle/configuration/nonPublicMarkers/klib.gradle.kts create mode 100644 src/functionalTest/resources/examples/gradle/configuration/signatures/invalid.gradle.kts create mode 100644 src/functionalTest/resources/examples/gradle/configuration/signatures/v1.gradle.kts create mode 100644 src/functionalTest/resources/examples/gradle/configuration/unsupported/enforce.gradle.kts create mode 100644 src/main/kotlin/BuildTaskBase.kt create mode 100644 src/main/kotlin/KotlinKlibAbiBuildTask.kt create mode 100644 src/main/kotlin/KotlinKlibExtractSupportedTargetsAbiTask.kt create mode 100644 src/main/kotlin/KotlinKlibInferAbiForUnsupportedTargetTask.kt create mode 100644 src/main/kotlin/KotlinKlibMergeAbiTask.kt create mode 100644 src/test/kotlin/tests/AbiTest.kt diff --git a/build.gradle.kts b/build.gradle.kts index 1e18b4e0..d2f10b02 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -62,6 +62,7 @@ val createClasspathManifest = tasks.register("createClasspathManifest") { dependencies { implementation(gradleApi()) implementation(libs.kotlinx.metadata) + compileOnly("org.jetbrains.kotlin:kotlin-compiler-embeddable") implementation(libs.ow2.asm) implementation(libs.ow2.asmTree) implementation(libs.javaDiffUtils) diff --git a/src/functionalTest/kotlin/kotlinx/validation/api/BaseKotlinGradleTest.kt b/src/functionalTest/kotlin/kotlinx/validation/api/BaseKotlinGradleTest.kt index 929f08cb..3709dcb4 100644 --- a/src/functionalTest/kotlin/kotlinx/validation/api/BaseKotlinGradleTest.kt +++ b/src/functionalTest/kotlin/kotlinx/validation/api/BaseKotlinGradleTest.kt @@ -5,6 +5,8 @@ package kotlinx.validation.api +import kotlinx.validation.API_DIR +import kotlinx.validation.KLIB_PHONY_TARGET_NAME import org.junit.Rule import org.junit.rules.TemporaryFolder import java.io.File @@ -17,4 +19,15 @@ public open class BaseKotlinGradleTest { internal val rootProjectDir: File get() = testProjectDir.root internal val rootProjectApiDump: File get() = rootProjectDir.resolve("$API_DIR/${rootProjectDir.name}.api") + + internal fun rootProjectAbiDump(target: String, project: String = rootProjectDir.name): File { + // TODO: rewrite + val suffix = if (target != KLIB_PHONY_TARGET_NAME) "api" else "klib.api" + return rootProjectDir.resolve("$API_DIR/$target/$project.$suffix") + } + + internal fun rootProjectAbiDump(project: String = rootProjectDir.name): File { + // TODO: rewrite + return rootProjectDir.resolve("$API_DIR/$project.klib.api") + } } diff --git a/src/functionalTest/kotlin/kotlinx/validation/api/TestDsl.kt b/src/functionalTest/kotlin/kotlinx/validation/api/TestDsl.kt index cfa25799..38aecb9f 100644 --- a/src/functionalTest/kotlin/kotlinx/validation/api/TestDsl.kt +++ b/src/functionalTest/kotlin/kotlinx/validation/api/TestDsl.kt @@ -123,6 +123,23 @@ internal fun FileContainer.apiFile(projectName: String, fn: AppendableScope.() - } } +/** + * Shortcut for creating a `api//.api` descriptor using [file][FileContainer.file] + */ +internal fun FileContainer.abiFile(projectName: String, target: String, fn: AppendableScope.() -> Unit) { + dir(API_DIR) { + dir(target) { + file("$projectName.klib.api", fn) + } + } +} + +internal fun FileContainer.abiFile(projectName: String, fn: AppendableScope.() -> Unit) { + dir(API_DIR) { + file("$projectName.klib.api", fn) + } +} + // not using default argument in apiFile for clarity in tests (explicit "empty" in the name) /** * Shortcut for creating an empty `api/.api` descriptor by using [file][FileContainer.file] @@ -195,3 +212,29 @@ private fun GradleRunner.addPluginTestRuntimeClasspath() = apply { val pluginClasspath = pluginClasspath + cpResource.readLines().map { File(it) } withPluginClasspath(pluginClasspath) } + +internal val commonNativeTargets = listOf( + "linuxX64", + "linuxArm64", + "mingwX64", + "androidNativeArm32", + "androidNativeArm64", + "androidNativeX64", + "androidNativeX86" +) + +internal val appleNativeTarget = listOf( + "macosX64", + "macosArm64", + "iosX64", + "iosArm64", + "iosSimulatorArm64", + "tvosX64", + "tvosArm64", + "tvosSimulatorArm64", + "watchosArm32", + "watchosArm64", + "watchosX64", + "watchosSimulatorArm64", + "watchosDeviceArm64", +) diff --git a/src/functionalTest/kotlin/kotlinx/validation/test/IgnoredClassesTests.kt b/src/functionalTest/kotlin/kotlinx/validation/test/IgnoredClassesTests.kt index e2f1a3ef..06b3f86c 100644 --- a/src/functionalTest/kotlin/kotlinx/validation/test/IgnoredClassesTests.kt +++ b/src/functionalTest/kotlin/kotlinx/validation/test/IgnoredClassesTests.kt @@ -15,6 +15,7 @@ import kotlinx.validation.api.resolve import kotlinx.validation.api.runner import kotlinx.validation.api.test import org.assertj.core.api.Assertions +import org.junit.Ignore import org.junit.Test import kotlin.test.assertTrue diff --git a/src/functionalTest/kotlin/kotlinx/validation/test/KLibVerificationTests.kt b/src/functionalTest/kotlin/kotlinx/validation/test/KLibVerificationTests.kt new file mode 100644 index 00000000..c88591ad --- /dev/null +++ b/src/functionalTest/kotlin/kotlinx/validation/test/KLibVerificationTests.kt @@ -0,0 +1,663 @@ +/* + * Copyright 2016-2023 JetBrains s.r.o. + * Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file. + */ + +package kotlinx.validation.test + +import kotlinx.validation.API_DIR +import kotlinx.validation.BANNED_TARGETS_PROPERTY_NAME +import kotlinx.validation.KLIB_PHONY_TARGET_NAME +import kotlinx.validation.api.* +import kotlinx.validation.api.buildGradleKts +import kotlinx.validation.api.resolve +import kotlinx.validation.api.test +import org.assertj.core.api.Assertions +import org.gradle.testkit.runner.BuildResult +import org.jetbrains.kotlin.konan.target.HostManager +import org.jetbrains.kotlin.konan.target.KonanTarget +import org.junit.Assume +import org.junit.Test +import kotlin.test.assertTrue + +private fun KLibVerificationTests.checkKlibDump(buildResult: BuildResult, expectedDumpFileName: String, + projectName: String = "testproject", + dumpTask: String = ":apiDump", + projectWithMultipleDumps: Boolean = false) { + buildResult.assertTaskSuccess(dumpTask) + + val generatedDump = if (projectWithMultipleDumps) { + rootProjectAbiDump(target = KLIB_PHONY_TARGET_NAME, project = projectName) + } else { + rootProjectAbiDump(projectName) + } + assertTrue(generatedDump.exists(), "There are no dumps generated for KLibs") + + val expected = readFileList(expectedDumpFileName) + + Assertions.assertThat(generatedDump.readText()).isEqualToIgnoringNewLines(expected) +} + +internal class KLibVerificationTests : BaseKotlinGradleTest() { + @Test + fun `apiDump for native targets`() { + val runner = test { + settingsGradleKts { + resolve("examples/gradle/settings/settings-name-testproject.gradle.kts") + } + buildGradleKts { + resolve("examples/gradle/base/withNativePlugin.gradle.kts") + } + kotlin("TopLevelDeclarations.kt", "commonMain") { + resolve("examples/classes/TopLevelDeclarations.kt") + } + runner { + arguments.add(":apiDump") + } + } + + checkKlibDump(runner.build(), "examples/classes/TopLevelDeclarations.klib.with.linux.dump") + } + + @Test + fun `apiCheck for native targets`() { + val runner = test { + settingsGradleKts { + resolve("examples/gradle/settings/settings-name-testproject.gradle.kts") + } + buildGradleKts { + resolve("examples/gradle/base/withNativePlugin.gradle.kts") + } + kotlin("TopLevelDeclarations.kt", "commonMain") { + resolve("examples/classes/TopLevelDeclarations.kt") + } + + abiFile(projectName = "testproject") { + resolve("examples/classes/TopLevelDeclarations.klib.dump") + } + + runner { + arguments.add(":apiCheck") + } + } + + runner.build().apply { + assertTaskSuccess(":apiCheck") + } + } + + @Test + fun `apiCheck for native targets should fail when a class is not in a dump`() { + val runner = test { + settingsGradleKts { + resolve("examples/gradle/settings/settings-name-testproject.gradle.kts") + } + buildGradleKts { + resolve("examples/gradle/base/withNativePlugin.gradle.kts") + } + kotlin("BuildConfig.kt", "commonMain") { + resolve("examples/classes/BuildConfig.kt") + } + + abiFile(projectName = "testproject") { + resolve("examples/classes/Empty.klib.dump") + } + + runner { + arguments.add(":apiCheck") + } + } + + runner.buildAndFail().apply { + + Assertions.assertThat(output) + .contains("+final class com.company/BuildConfig { // com.company/BuildConfig|null[0]") + tasks.filter { it.path.endsWith("ApiCheck") } + .forEach { + assertTaskFailure(it.path) + } + } + } + + @Test + fun `apiDump should include target-specific sources`() { + val runner = test { + settingsGradleKts { + resolve("examples/gradle/settings/settings-name-testproject.gradle.kts") + } + buildGradleKts { + resolve("examples/gradle/base/withNativePlugin.gradle.kts") + } + kotlin("AnotherBuildConfig.kt", "commonMain") { + resolve("examples/classes/AnotherBuildConfig.kt") + } + kotlin("AnotherBuildConfigLinuxArm64.kt", "linuxArm64Main") { + resolve("examples/classes/AnotherBuildConfigLinuxArm64.kt") + } + + runner { + arguments.add(":apiDump") + } + } + + runner.build().apply { + assertTaskSuccess(":apiDump") + + // not common, but built from the common source set + val dump = rootProjectAbiDump("testproject") + assertTrue(dump.exists(), "Dump does not exist") + + val expectedDump = readFileList("examples/classes/AnotherBuildConfigLinuxArm64Extra.klib.dump") + Assertions.assertThat(dump.readText()).isEqualToIgnoringNewLines(expectedDump) + } + } + + @Test + fun `apiDump with native targets along with JVM target`() { + val runner = test { + settingsGradleKts { + resolve("examples/gradle/settings/settings-name-testproject.gradle.kts") + } + buildGradleKts { + resolve("examples/gradle/base/withNativePlugin.gradle.kts") + resolve("examples/gradle/base/enableJvmInWithNativePlugin.gradle.kts") + } + kotlin("AnotherBuildConfig.kt", "commonMain") { + resolve("examples/classes/AnotherBuildConfig.kt") + } + runner { + arguments.add(":apiDump") + } + } + + runner.build().apply { + checkKlibDump(this, "examples/classes/AnotherBuildConfig.klib.dump", + projectWithMultipleDumps = false) + + val jvmApiDump = rootProjectDir.resolve("$API_DIR/testproject.api") + assertTrue(jvmApiDump.exists(), "No API dump for JVM") + + val jvmExpected = readFileList("examples/classes/AnotherBuildConfig.dump") + Assertions.assertThat(jvmApiDump.readText()).isEqualToIgnoringNewLines(jvmExpected) + } + } + + @Test + fun `apiDump should ignore a class listed in ignoredClasses`() { + val runner = test { + settingsGradleKts { + resolve("examples/gradle/settings/settings-name-testproject.gradle.kts") + } + buildGradleKts { + resolve("examples/gradle/base/withNativePlugin.gradle.kts") + resolve("examples/gradle/configuration/ignoredClasses/oneValidFullyQualifiedClass.gradle.kts") + } + kotlin("BuildConfig.kt", "commonMain") { + resolve("examples/classes/BuildConfig.kt") + } + kotlin("AnotherBuildConfig.kt", "commonMain") { + resolve("examples/classes/AnotherBuildConfig.kt") + } + + runner { + arguments.add(":apiDump") + } + } + + checkKlibDump(runner.build(), "examples/classes/AnotherBuildConfig.klib.dump") + } + + @Test + fun `apiDump should succeed if a class listed in ignoredClasses is not found`() { + val runner = test { + settingsGradleKts { + resolve("examples/gradle/settings/settings-name-testproject.gradle.kts") + } + buildGradleKts { + resolve("examples/gradle/base/withNativePlugin.gradle.kts") + resolve("examples/gradle/configuration/ignoredClasses/oneValidFullyQualifiedClass.gradle.kts") + } + kotlin("AnotherBuildConfig.kt", "commonMain") { + resolve("examples/classes/AnotherBuildConfig.kt") + } + + runner { + arguments.add(":apiDump") + } + } + + checkKlibDump(runner.build(), "examples/classes/AnotherBuildConfig.klib.dump") + } + + @Test + fun `apiDump should ignore all entities from a package listed in ingoredPackages`() { + val runner = test { + settingsGradleKts { + resolve("examples/gradle/settings/settings-name-testproject.gradle.kts") + } + buildGradleKts { + resolve("examples/gradle/base/withNativePlugin.gradle.kts") + resolve("examples/gradle/configuration/ignoredPackages/oneValidPackage.gradle.kts") + } + kotlin("BuildConfig.kt", "commonMain") { + resolve("examples/classes/BuildConfig.kt") + } + kotlin("AnotherBuildConfig.kt", "commonMain") { + resolve("examples/classes/AnotherBuildConfig.kt") + } + kotlin("SubPackage.kt", "commonMain") { + resolve("examples/classes/SubPackage.kt") + } + + runner { + arguments.add(":apiDump") + } + } + + checkKlibDump(runner.build(), "examples/classes/AnotherBuildConfig.klib.dump") + } + + @Test + fun `apiDump should ignore all entities annotated with non-public markers`() { + val runner = test { + settingsGradleKts { + resolve("examples/gradle/settings/settings-name-testproject.gradle.kts") + } + buildGradleKts { + resolve("examples/gradle/base/withNativePlugin.gradle.kts") + resolve("examples/gradle/configuration/nonPublicMarkers/klib.gradle.kts") + } + kotlin("HiddenDeclarations.kt", "commonMain") { + resolve("examples/classes/HiddenDeclarations.kt") + } + kotlin("NonPublicMarkers.kt", "commonMain") { + resolve("examples/classes/NonPublicMarkers.kt") + } + + runner { + arguments.add(":apiDump") + } + } + + checkKlibDump(runner.build(), "examples/classes/HiddenDeclarations.klib.dump") + } + + @Test + fun `apiDump should not dump subclasses excluded via ignoredClasses`() { + val runner = test { + settingsGradleKts { + resolve("examples/gradle/settings/settings-name-testproject.gradle.kts") + } + buildGradleKts { + resolve("examples/gradle/base/withNativePlugin.gradle.kts") + resolve("examples/gradle/configuration/ignoreSubclasses/ignore.gradle.kts") + } + kotlin("Subclasses.kt", "commonMain") { + resolve("examples/classes/Subclasses.kt") + } + + runner { + arguments.add(":apiDump") + } + } + + checkKlibDump(runner.build(), "examples/classes/Subclasses.klib.dump") + } + + @Test + fun `apiCheck for native targets using v1 signatures`() { + val runner = test { + settingsGradleKts { + resolve("examples/gradle/settings/settings-name-testproject.gradle.kts") + } + buildGradleKts { + resolve("examples/gradle/base/withNativePlugin.gradle.kts") + resolve("examples/gradle/configuration/signatures/v1.gradle.kts") + } + kotlin("TopLevelDeclarations.kt", "commonMain") { + resolve("examples/classes/TopLevelDeclarations.kt") + } + + abiFile(projectName = "testproject") { + resolve("examples/classes/TopLevelDeclarations.klib.v1.dump") + } + + runner { + arguments.add(":apiCheck") + } + } + + runner.build().apply { + assertTaskSuccess(":apiCheck") + } + } + + @Test + fun `apiDump for native targets should fail when using invalid signature version`() { + val runner = test { + settingsGradleKts { + resolve("examples/gradle/settings/settings-name-testproject.gradle.kts") + } + buildGradleKts { + resolve("examples/gradle/base/withNativePlugin.gradle.kts") + resolve("examples/gradle/configuration/signatures/invalid.gradle.kts") + } + kotlin("TopLevelDeclarations.kt", "commonMain") { + resolve("examples/classes/TopLevelDeclarations.kt") + } + + runner { + arguments.add(":apiDump") + } + } + + runner.buildAndFail() + } + + @Test + fun `apiDump should work for Apple-targets`() { + Assume.assumeTrue(HostManager().isEnabled(KonanTarget.MACOS_ARM64)) + val runner = test { + settingsGradleKts { + resolve("examples/gradle/settings/settings-name-testproject.gradle.kts") + } + buildGradleKts { + resolve("examples/gradle/base/withNativePlugin.gradle.kts") + resolve("examples/gradle/configuration/appleTargets/targets.gradle.kts") + } + kotlin("TopLevelDeclarations.kt", "commonMain") { + resolve("examples/classes/TopLevelDeclarations.kt") + } + runner { + arguments.add(":apiDump") + } + } + + checkKlibDump(runner.build(), "examples/classes/TopLevelDeclarations.klib.all.dump") + } + + @Test + fun `apiCheck should work for Apple-targets`() { + Assume.assumeTrue(HostManager().isEnabled(KonanTarget.MACOS_ARM64)) + val runner = test { + settingsGradleKts { + resolve("examples/gradle/settings/settings-name-testproject.gradle.kts") + } + buildGradleKts { + resolve("examples/gradle/base/withNativePlugin.gradle.kts") + resolve("examples/gradle/configuration/appleTargets/targets.gradle.kts") + } + kotlin("TopLevelDeclarations.kt", "commonMain") { + resolve("examples/classes/TopLevelDeclarations.kt") + } + abiFile(projectName = "testproject") { + resolve("examples/classes/TopLevelDeclarations.klib.all.dump") + } + runner { + arguments.add(":apiCheck") + } + } + + runner.build().apply { + assertTaskSuccess(":apiCheck") + } + } + + @Test + fun `apiCheck should fail if a target is not supported`() { + val runner = test { + settingsGradleKts { + resolve("examples/gradle/settings/settings-name-testproject.gradle.kts") + } + buildGradleKts { + resolve("examples/gradle/base/withNativePlugin.gradle.kts") + } + kotlin("TopLevelDeclarations.kt", "commonMain") { + resolve("examples/classes/TopLevelDeclarations.kt") + } + abiFile(projectName = "testproject", target = KLIB_PHONY_TARGET_NAME) { + } + runner { + arguments.add("-P$BANNED_TARGETS_PROPERTY_NAME=linuxArm64") + arguments.add(":apiCheck") + } + } + + runner.buildAndFail() + } + + @Test + fun `apiCheck should ignore unsupported targets by default`() { + val runner = test { + settingsGradleKts { + resolve("examples/gradle/settings/settings-name-testproject.gradle.kts") + } + buildGradleKts { + resolve("examples/gradle/base/withNativePlugin.gradle.kts") + } + kotlin("TopLevelDeclarations.kt", "commonMain") { + resolve("examples/classes/TopLevelDeclarations.kt") + } + abiFile(projectName = "testproject") { + // note that the regular dump is used, where linuxArm64 is presented + resolve("examples/classes/TopLevelDeclarations.klib.dump") + } + runner { + arguments.add("-P$BANNED_TARGETS_PROPERTY_NAME=linuxArm64") + arguments.add(":apiCheck") + } + } + + runner.build().apply { + assertTaskSuccess(":apiCheck") + } + } + + @Test + fun `apiCheck should fail for unsupported targets with strict mode turned on`() { + val runner = test { + settingsGradleKts { + resolve("examples/gradle/settings/settings-name-testproject.gradle.kts") + } + buildGradleKts { + resolve("examples/gradle/base/withNativePlugin.gradle.kts") + resolve("examples/gradle/configuration/unsupported/enforce.gradle.kts") + } + kotlin("TopLevelDeclarations.kt", "commonMain") { + resolve("examples/classes/TopLevelDeclarations.kt") + } + abiFile(projectName = "testproject") { + // note that the regular dump is used, where linuxArm64 is presented + resolve("examples/classes/TopLevelDeclarations.klib.dump") + } + runner { + arguments.add("-P$BANNED_TARGETS_PROPERTY_NAME=linuxArm64") + arguments.add(":apiCheck") + } + } + + runner.buildAndFail().apply { + assertTaskFailure(":klibApiPrepareAbiForValidation") + } + } + + @Test + fun `klibDump should infer a dump for unsupported target from similar enough target`() { + val runner = test { + settingsGradleKts { + resolve("examples/gradle/settings/settings-name-testproject.gradle.kts") + } + buildGradleKts { + resolve("examples/gradle/base/withNativePlugin.gradle.kts") + } + kotlin("TopLevelDeclarations.kt", "commonMain") { + resolve("examples/classes/TopLevelDeclarations.kt") + } + kotlin("AnotherBuildConfigLinuxArm64.kt", "linuxArm64Main") { + resolve("examples/classes/AnotherBuildConfigLinuxArm64.kt") + } + runner { + arguments.add("-P$BANNED_TARGETS_PROPERTY_NAME=linuxArm64") + arguments.add(":klibApiDump") + } + } + + checkKlibDump(runner.build(), "examples/classes/TopLevelDeclarations.klib.with.linux.dump", + dumpTask = ":klibApiDump") + } + + @Test + fun `klibDump should fail when the only target in the project is disabled`() { + val runner = test { + settingsGradleKts { + resolve("examples/gradle/settings/settings-name-testproject.gradle.kts") + } + buildGradleKts { + resolve("examples/gradle/base/withNativePluginAndSingleTarget.gradle.kts") + } + kotlin("TopLevelDeclarations.kt", "commonMain") { + resolve("examples/classes/TopLevelDeclarations.kt") + } + kotlin("AnotherBuildConfigLinuxArm64.kt", "linuxArm64Main") { + resolve("examples/classes/AnotherBuildConfigLinuxArm64.kt") + } + runner { + arguments.add("-P$BANNED_TARGETS_PROPERTY_NAME=linuxArm64") + arguments.add(":klibApiDump") + } + } + + runner.buildAndFail().apply { + assertTaskFailure(":linuxArm64ApiInferAbiDump") + Assertions.assertThat(output).contains("The target linuxArm64 is not supported by the host compiler " + + "and there are no targets similar to linuxArm64 to infer a dump from it.") + } + } + + @Test + fun `klibDump if all klib-targets are unavailable`() { + val runner = test { + settingsGradleKts { + resolve("examples/gradle/settings/settings-name-testproject.gradle.kts") + } + buildGradleKts { + resolve("examples/gradle/base/withNativePlugin.gradle.kts") + } + kotlin("TopLevelDeclarations.kt", "commonMain") { + resolve("examples/classes/TopLevelDeclarations.kt") + } + runner { + arguments.add("-P$BANNED_TARGETS_PROPERTY_NAME=linuxArm64,linuxX64,mingwX64," + + "androidNativeArm32,androidNativeArm64,androidNativeX64,androidNativeX86") + arguments.add(":klibApiDump") + } + } + + runner.buildAndFail().apply { + Assertions.assertThat(output).contains( + "is not supported by the host compiler and there are no targets similar to" + ) + } + } + + @Test + fun `klibCheck if all klib-targets are unavailable`() { + val runner = test { + settingsGradleKts { + resolve("examples/gradle/settings/settings-name-testproject.gradle.kts") + } + buildGradleKts { + resolve("examples/gradle/base/withNativePlugin.gradle.kts") + } + kotlin("TopLevelDeclarations.kt", "commonMain") { + resolve("examples/classes/TopLevelDeclarations.kt") + } + abiFile(projectName = "testproject") { + // note that the regular dump is used, where linuxArm64 is presented + resolve("examples/classes/TopLevelDeclarations.klib.dump") + } + runner { + arguments.add("-P$BANNED_TARGETS_PROPERTY_NAME=linuxArm64,linuxX64,mingwX64," + + "androidNativeArm32,androidNativeArm64,androidNativeX64,androidNativeX86") + arguments.add(":klibApiCheck") + } + } + + runner.buildAndFail().apply { + Assertions.assertThat(output).contains( + "KLib ABI dump/validation requires at least enabled klib target, but none were found." + ) + } + } + + @Test + fun `target name grouping should be disabled on group name clash`() { + val runner = test { + settingsGradleKts { + resolve("examples/gradle/settings/settings-name-testproject.gradle.kts") + } + buildGradleKts { + resolve("examples/gradle/base/withNativePluginAndNoTargets.gradle.kts") + resolve("examples/gradle/configuration/grouping/clashingTargetNames.gradle.kts") + } + kotlin("AnotherBuildConfig.kt", "commonMain") { + resolve("examples/classes/AnotherBuildConfig.kt") + } + runner { + arguments.add(":klibApiDump") + } + } + + checkKlibDump(runner.build(), "examples/classes/AnotherBuildConfig.klib.clash.dump", + dumpTask = ":klibApiDump") + } + + @Test + fun `target name grouping with custom target names`() { + val runner = test { + settingsGradleKts { + resolve("examples/gradle/settings/settings-name-testproject.gradle.kts") + } + buildGradleKts { + resolve("examples/gradle/base/withNativePluginAndNoTargets.gradle.kts") + resolve("examples/gradle/configuration/grouping/customTargetNames.gradle.kts") + } + kotlin("AnotherBuildConfig.kt", "commonMain") { + resolve("examples/classes/AnotherBuildConfig.kt") + } + runner { + arguments.add(":klibApiDump") + } + } + + checkKlibDump(runner.build(), "examples/classes/AnotherBuildConfig.klib.custom.dump", + dumpTask = ":klibApiDump") + } + + @Test + fun `target name grouping`() { + val runner = test { + settingsGradleKts { + resolve("examples/gradle/settings/settings-name-testproject.gradle.kts") + } + buildGradleKts { + resolve("examples/gradle/base/withNativePlugin.gradle.kts") + } + kotlin("AnotherBuildConfig.kt", "commonMain") { + resolve("examples/classes/AnotherBuildConfig.kt") + } + kotlin("AnotherBuildConfigLinuxArm64.kt", "linuxArm64Main") { + resolve("examples/classes/AnotherBuildConfigLinuxArm64.kt") + } + kotlin("AnotherBuildConfigLinuxX64.kt", "linuxX64Main") { + resolve("examples/classes/AnotherBuildConfigLinuxArm64.kt") + } + runner { + arguments.add(":klibApiDump") + } + } + + checkKlibDump(runner.build(), "examples/classes/AnotherBuildConfigLinux.klib.grouping.dump", + dumpTask = ":klibApiDump") + } +} diff --git a/src/functionalTest/kotlin/kotlinx/validation/test/NonPublicMarkersTest.kt b/src/functionalTest/kotlin/kotlinx/validation/test/NonPublicMarkersTest.kt index 689d9384..ff7ca991 100644 --- a/src/functionalTest/kotlin/kotlinx/validation/test/NonPublicMarkersTest.kt +++ b/src/functionalTest/kotlin/kotlinx/validation/test/NonPublicMarkersTest.kt @@ -38,6 +38,39 @@ class NonPublicMarkersTest : BaseKotlinGradleTest() { } } + @Test + @Ignore("https://youtrack.jetbrains.com/issue/KT-62259") + fun testIgnoredMarkersOnPropertiesForNativeTargets() { + val runner = test { + settingsGradleKts { + resolve("examples/gradle/settings/settings-name-testproject.gradle.kts") + } + + buildGradleKts { + resolve("examples/gradle/base/withNativePlugin.gradle.kts") + resolve("examples/gradle/configuration/nonPublicMarkers/markers.gradle.kts") + } + + kotlin("Properties.kt", sourceSet = "commonMain") { + resolve("examples/classes/Properties.kt") + } + + commonNativeTargets.forEach { + abiFile(projectName = "testproject", target = it) { + resolve("examples/classes/Properties.klib.dump") + } + } + + runner { + arguments.add(":apiCheck") + } + } + + runner.build().apply { + assertTaskSuccess(":apiCheck") + } + } + @Test fun testFiltrationByPackageLevelAnnotations() { val runner = test { diff --git a/src/functionalTest/kotlin/kotlinx/validation/test/PublicMarkersTest.kt b/src/functionalTest/kotlin/kotlinx/validation/test/PublicMarkersTest.kt index e03ee69e..9a0b9dcf 100644 --- a/src/functionalTest/kotlin/kotlinx/validation/test/PublicMarkersTest.kt +++ b/src/functionalTest/kotlin/kotlinx/validation/test/PublicMarkersTest.kt @@ -46,6 +46,41 @@ class PublicMarkersTest : BaseKotlinGradleTest() { } } + // Public markers are not supported in KLIB ABI dumps + @Test + fun testPublicMarkersForNativeTargets() { + val runner = test { + settingsGradleKts { + resolve("/examples/gradle/settings/settings-name-testproject.gradle.kts") + } + + buildGradleKts { + resolve("/examples/gradle/base/withNativePlugin.gradle.kts") + resolve("/examples/gradle/configuration/publicMarkers/markers.gradle.kts") + } + + kotlin("ClassWithPublicMarkers.kt", sourceSet = "commonMain") { + resolve("/examples/classes/ClassWithPublicMarkers.kt") + } + + kotlin("ClassInPublicPackage.kt", sourceSet = "commonMain") { + resolve("/examples/classes/ClassInPublicPackage.kt") + } + + abiFile(projectName = "testproject") { + resolve("/examples/classes/ClassWithPublicMarkers.klib.dump") + } + + runner { + arguments.add(":apiCheck") + } + } + + runner.withDebug(true).build().apply { + assertTaskSuccess(":apiCheck") + } + } + @Test fun testFiltrationByPackageLevelAnnotations() { val runner = test { diff --git a/src/functionalTest/resources/examples/classes/AnotherBuildConfig.klib.clash.dump b/src/functionalTest/resources/examples/classes/AnotherBuildConfig.klib.clash.dump new file mode 100644 index 00000000..10da55c9 --- /dev/null +++ b/src/functionalTest/resources/examples/classes/AnotherBuildConfig.klib.clash.dump @@ -0,0 +1,14 @@ +// Merged KLib ABI Dump +// Targets: [linux, linuxArm64] +// Rendering settings: +// - Signature version: 2 +// - Show manifest properties: false +// - Show declarations: true + +// Library unique name: +final class org.different.pack/BuildConfig { // org.different.pack/BuildConfig|null[0] + constructor () // org.different.pack/BuildConfig.|(){}[0] + final fun f1(): kotlin/Int // org.different.pack/BuildConfig.f1|f1(){}[0] + final val p1 // org.different.pack/BuildConfig.p1|{}p1[0] + final fun (): kotlin/Int // org.different.pack/BuildConfig.p1.|(){}[0] +} diff --git a/src/functionalTest/resources/examples/classes/AnotherBuildConfig.klib.custom.dump b/src/functionalTest/resources/examples/classes/AnotherBuildConfig.klib.custom.dump new file mode 100644 index 00000000..2d6ba9d3 --- /dev/null +++ b/src/functionalTest/resources/examples/classes/AnotherBuildConfig.klib.custom.dump @@ -0,0 +1,14 @@ +// Merged KLib ABI Dump +// Targets: [linuxA, linuxB] +// Rendering settings: +// - Signature version: 2 +// - Show manifest properties: false +// - Show declarations: true + +// Library unique name: +final class org.different.pack/BuildConfig { // org.different.pack/BuildConfig|null[0] + constructor () // org.different.pack/BuildConfig.|(){}[0] + final fun f1(): kotlin/Int // org.different.pack/BuildConfig.f1|f1(){}[0] + final val p1 // org.different.pack/BuildConfig.p1|{}p1[0] + final fun (): kotlin/Int // org.different.pack/BuildConfig.p1.|(){}[0] +} diff --git a/src/functionalTest/resources/examples/classes/AnotherBuildConfig.klib.dump b/src/functionalTest/resources/examples/classes/AnotherBuildConfig.klib.dump new file mode 100644 index 00000000..7a1b5aa2 --- /dev/null +++ b/src/functionalTest/resources/examples/classes/AnotherBuildConfig.klib.dump @@ -0,0 +1,16 @@ +// Merged KLib ABI Dump +// Targets: [androidNativeArm32, androidNativeArm64, androidNativeX64, androidNativeX86, linuxArm64, linuxX64, mingwX64] +// Alias: androidNative => [androidNativeArm32, androidNativeArm64, androidNativeX64, androidNativeX86] +// Alias: linux => [linuxArm64, linuxX64] +// Rendering settings: +// - Signature version: 2 +// - Show manifest properties: false +// - Show declarations: true + +// Library unique name: +final class org.different.pack/BuildConfig { // org.different.pack/BuildConfig|null[0] + constructor () // org.different.pack/BuildConfig.|(){}[0] + final fun f1(): kotlin/Int // org.different.pack/BuildConfig.f1|f1(){}[0] + final val p1 // org.different.pack/BuildConfig.p1|{}p1[0] + final fun (): kotlin/Int // org.different.pack/BuildConfig.p1.|(){}[0] +} diff --git a/src/functionalTest/resources/examples/classes/AnotherBuildConfigLinux.klib.grouping.dump b/src/functionalTest/resources/examples/classes/AnotherBuildConfigLinux.klib.grouping.dump new file mode 100644 index 00000000..98d3c553 --- /dev/null +++ b/src/functionalTest/resources/examples/classes/AnotherBuildConfigLinux.klib.grouping.dump @@ -0,0 +1,18 @@ +// Merged KLib ABI Dump +// Targets: [androidNativeArm32, androidNativeArm64, androidNativeX64, androidNativeX86, linuxArm64, linuxX64, mingwX64] +// Alias: androidNative => [androidNativeArm32, androidNativeArm64, androidNativeX64, androidNativeX86] +// Alias: linux => [linuxArm64, linuxX64] +// Rendering settings: +// - Signature version: 2 +// - Show manifest properties: false +// - Show declarations: true + +// Library unique name: +final class org.different.pack/BuildConfig { // org.different.pack/BuildConfig|null[0] + constructor () // org.different.pack/BuildConfig.|(){}[0] + final fun f1(): kotlin/Int // org.different.pack/BuildConfig.f1|f1(){}[0] + final val p1 // org.different.pack/BuildConfig.p1|{}p1[0] + final fun (): kotlin/Int // org.different.pack/BuildConfig.p1.|(){}[0] +} +// Targets: [linux] +final fun (org.different.pack/BuildConfig).org.different.pack/linuxArm64Specific(): kotlin/Int // org.different.pack/linuxArm64Specific|linuxArm64Specific@org.different.pack.BuildConfig(){}[0] diff --git a/src/functionalTest/resources/examples/classes/AnotherBuildConfigLinuxArm64.kt b/src/functionalTest/resources/examples/classes/AnotherBuildConfigLinuxArm64.kt new file mode 100644 index 00000000..f5352c0b --- /dev/null +++ b/src/functionalTest/resources/examples/classes/AnotherBuildConfigLinuxArm64.kt @@ -0,0 +1,8 @@ +/* + * Copyright 2016-2023 JetBrains s.r.o. + * Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file. + */ + +package org.different.pack + +fun BuildConfig.linuxArm64Specific(): Int = 42 diff --git a/src/functionalTest/resources/examples/classes/AnotherBuildConfigLinuxArm64Extra.klib.dump b/src/functionalTest/resources/examples/classes/AnotherBuildConfigLinuxArm64Extra.klib.dump new file mode 100644 index 00000000..70842122 --- /dev/null +++ b/src/functionalTest/resources/examples/classes/AnotherBuildConfigLinuxArm64Extra.klib.dump @@ -0,0 +1,18 @@ +// Merged KLib ABI Dump +// Targets: [androidNativeArm32, androidNativeArm64, androidNativeX64, androidNativeX86, linuxArm64, linuxX64, mingwX64] +// Alias: androidNative => [androidNativeArm32, androidNativeArm64, androidNativeX64, androidNativeX86] +// Alias: linux => [linuxArm64, linuxX64] +// Rendering settings: +// - Signature version: 2 +// - Show manifest properties: false +// - Show declarations: true + +// Library unique name: +final class org.different.pack/BuildConfig { // org.different.pack/BuildConfig|null[0] + constructor () // org.different.pack/BuildConfig.|(){}[0] + final fun f1(): kotlin/Int // org.different.pack/BuildConfig.f1|f1(){}[0] + final val p1 // org.different.pack/BuildConfig.p1|{}p1[0] + final fun (): kotlin/Int // org.different.pack/BuildConfig.p1.|(){}[0] +} +// Targets: [linuxArm64] +final fun (org.different.pack/BuildConfig).org.different.pack/linuxArm64Specific(): kotlin/Int // org.different.pack/linuxArm64Specific|linuxArm64Specific@org.different.pack.BuildConfig(){}[0] diff --git a/src/functionalTest/resources/examples/classes/ClassWithPublicMarkers.klib.dump b/src/functionalTest/resources/examples/classes/ClassWithPublicMarkers.klib.dump new file mode 100644 index 00000000..94cccd63 --- /dev/null +++ b/src/functionalTest/resources/examples/classes/ClassWithPublicMarkers.klib.dump @@ -0,0 +1,45 @@ +// Merged KLib ABI Dump +// Targets: [androidNativeArm32, androidNativeArm64, androidNativeX64, androidNativeX86, linuxArm64, linuxX64, mingwX64] +// Alias: androidNative => [androidNativeArm32, androidNativeArm64, androidNativeX64, androidNativeX86] +// Alias: linux => [linuxArm64, linuxX64] +// Rendering settings: +// - Signature version: 2 +// - Show manifest properties: false +// - Show declarations: true + +// Library unique name: +final class foo.api/ClassInPublicPackage { // foo.api/ClassInPublicPackage|null[0] + constructor () // foo.api/ClassInPublicPackage.|(){}[0] + final class Inner { // foo.api/ClassInPublicPackage.Inner|null[0] + constructor () // foo.api/ClassInPublicPackage.Inner.|(){}[0] + } +} +final class foo/ClassWithPublicMarkers { // foo/ClassWithPublicMarkers|null[0] + constructor () // foo/ClassWithPublicMarkers.|(){}[0] + final class MarkedClass { // foo/ClassWithPublicMarkers.MarkedClass|null[0] + constructor () // foo/ClassWithPublicMarkers.MarkedClass.|(){}[0] + final val bar1 // foo/ClassWithPublicMarkers.MarkedClass.bar1|{}bar1[0] + final fun (): kotlin/Int // foo/ClassWithPublicMarkers.MarkedClass.bar1.|(){}[0] + } + final class NotMarkedClass { // foo/ClassWithPublicMarkers.NotMarkedClass|null[0] + constructor () // foo/ClassWithPublicMarkers.NotMarkedClass.|(){}[0] + } + final var bar1 // foo/ClassWithPublicMarkers.bar1|{}bar1[0] + final fun (): kotlin/Int // foo/ClassWithPublicMarkers.bar1.|(){}[0] + final fun (kotlin/Int) // foo/ClassWithPublicMarkers.bar1.|(kotlin.Int){}[0] + final var bar2 // foo/ClassWithPublicMarkers.bar2|{}bar2[0] + final fun (): kotlin/Int // foo/ClassWithPublicMarkers.bar2.|(){}[0] + final fun (kotlin/Int) // foo/ClassWithPublicMarkers.bar2.|(kotlin.Int){}[0] + final var notMarkedPublic // foo/ClassWithPublicMarkers.notMarkedPublic|{}notMarkedPublic[0] + final fun (): kotlin/Int // foo/ClassWithPublicMarkers.notMarkedPublic.|(){}[0] + final fun (kotlin/Int) // foo/ClassWithPublicMarkers.notMarkedPublic.|(kotlin.Int){}[0] +} +open annotation class foo/PublicClass : kotlin/Annotation { // foo/PublicClass|null[0] + constructor () // foo/PublicClass.|(){}[0] +} +open annotation class foo/PublicField : kotlin/Annotation { // foo/PublicField|null[0] + constructor () // foo/PublicField.|(){}[0] +} +open annotation class foo/PublicProperty : kotlin/Annotation { // foo/PublicProperty|null[0] + constructor () // foo/PublicProperty.|(){}[0] +} diff --git a/src/functionalTest/resources/examples/classes/Empty.klib.dump b/src/functionalTest/resources/examples/classes/Empty.klib.dump new file mode 100644 index 00000000..f3ba7066 --- /dev/null +++ b/src/functionalTest/resources/examples/classes/Empty.klib.dump @@ -0,0 +1,6 @@ +// Merged KLib ABI Dump +// Targets: [mingwX64] +// Rendering settings: +// - Signature version: 2 +// - Show manifest properties: false +// - Show declarations: true diff --git a/src/functionalTest/resources/examples/classes/HiddenDeclarations.klib.dump b/src/functionalTest/resources/examples/classes/HiddenDeclarations.klib.dump new file mode 100644 index 00000000..32f3d71f --- /dev/null +++ b/src/functionalTest/resources/examples/classes/HiddenDeclarations.klib.dump @@ -0,0 +1,13 @@ +// Merged KLib ABI Dump +// Targets: [androidNativeArm32, androidNativeArm64, androidNativeX64, androidNativeX86, linuxArm64, linuxX64, mingwX64] +// Alias: androidNative => [androidNativeArm32, androidNativeArm64, androidNativeX64, androidNativeX86] +// Alias: linux => [linuxArm64, linuxX64] +// Rendering settings: +// - Signature version: 2 +// - Show manifest properties: false +// - Show declarations: true + +// Library unique name: +final class examples.classes/VC { // examples.classes/VC|null[0] + final var prop // examples.classes/VC.prop|{}prop[0] +} diff --git a/src/functionalTest/resources/examples/classes/HiddenDeclarations.kt b/src/functionalTest/resources/examples/classes/HiddenDeclarations.kt new file mode 100644 index 00000000..702ed05a --- /dev/null +++ b/src/functionalTest/resources/examples/classes/HiddenDeclarations.kt @@ -0,0 +1,43 @@ +/* + * Copyright 2016-2023 JetBrains s.r.o. + * Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file. + */ + +package examples.classes + +import annotations.* + +@HiddenFunction +public fun hidden() = Unit + +@HiddenProperty +public val v: Int = 42 + +@HiddenClass +public class HC + +public class VC @HiddenCtor constructor() { + @HiddenProperty + public val v: Int = 42 + + public var prop: Int = 0 + @HiddenGetter + get() = field + @HiddenSetter + set(value) { + field = value + } + + @HiddenProperty + public var fullyHiddenProp: Int = 0 + + @HiddenFunction + public fun m() = Unit +} + +@HiddenClass +public class HiddenOuterClass { + public class HiddenInnerClass { + + } +} diff --git a/src/functionalTest/resources/examples/classes/NonPublicMarkers.kt b/src/functionalTest/resources/examples/classes/NonPublicMarkers.kt new file mode 100644 index 00000000..fdf82887 --- /dev/null +++ b/src/functionalTest/resources/examples/classes/NonPublicMarkers.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2016-2023 JetBrains s.r.o. + * Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file. + */ + +package annotations + +@HiddenClass +@Target(AnnotationTarget.CLASS) +annotation class HiddenClass + +@HiddenClass +@Target(AnnotationTarget.FUNCTION) +annotation class HiddenFunction + +@HiddenClass +@Target(AnnotationTarget.CONSTRUCTOR) +annotation class HiddenCtor + +@HiddenClass +@Target(AnnotationTarget.PROPERTY) +annotation class HiddenProperty + +@HiddenClass +@Target(AnnotationTarget.FIELD) +annotation class HiddenField + +@HiddenClass +@Target(AnnotationTarget.PROPERTY_GETTER) +annotation class HiddenGetter + +@HiddenClass +@Target(AnnotationTarget.PROPERTY_SETTER) +annotation class HiddenSetter diff --git a/src/functionalTest/resources/examples/classes/Properties.klib.dump b/src/functionalTest/resources/examples/classes/Properties.klib.dump new file mode 100644 index 00000000..70189bb8 --- /dev/null +++ b/src/functionalTest/resources/examples/classes/Properties.klib.dump @@ -0,0 +1,17 @@ +// Merged KLib ABI Dump +// Targets: [androidNativeArm32, androidNativeArm64, androidNativeX64, androidNativeX86, linuxArm64, linuxX64, mingwX64] +// Rendering settings: +// - Signature version: 2 +// - Show manifest properties: false +// - Show declarations: true + +// Library unique name: +final class foo/ClassWithProperties { // foo/ClassWithProperties|null[0] + constructor () // foo/ClassWithProperties.|(){}[0] +} +open annotation class foo/HiddenField : kotlin/Annotation { // foo/HiddenField|null[0] + constructor () // foo/HiddenField.|(){}[0] +} +open annotation class foo/HiddenProperty : kotlin/Annotation { // foo/HiddenProperty|null[0] + constructor () // foo/HiddenProperty.|(){}[0] +} diff --git a/src/functionalTest/resources/examples/classes/SubPackage.kt b/src/functionalTest/resources/examples/classes/SubPackage.kt new file mode 100644 index 00000000..c5a298b5 --- /dev/null +++ b/src/functionalTest/resources/examples/classes/SubPackage.kt @@ -0,0 +1,10 @@ +/* + * Copyright 2016-2023 JetBrains s.r.o. + * Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file. + */ + +package com.company.division + +public class ClassWithinSubPackage { +} + diff --git a/src/functionalTest/resources/examples/classes/Subclasses.dump b/src/functionalTest/resources/examples/classes/Subclasses.dump new file mode 100644 index 00000000..04cb1523 --- /dev/null +++ b/src/functionalTest/resources/examples/classes/Subclasses.dump @@ -0,0 +1,7 @@ +public final class subclasses/A { + public fun ()V +} + +public final class subclasses/A$D { + public fun ()V +} diff --git a/src/functionalTest/resources/examples/classes/Subclasses.klib.dump b/src/functionalTest/resources/examples/classes/Subclasses.klib.dump new file mode 100644 index 00000000..1a9cd04c --- /dev/null +++ b/src/functionalTest/resources/examples/classes/Subclasses.klib.dump @@ -0,0 +1,16 @@ +// Merged KLib ABI Dump +// Targets: [androidNativeArm32, androidNativeArm64, androidNativeX64, androidNativeX86, linuxArm64, linuxX64, mingwX64] +// Alias: androidNative => [androidNativeArm32, androidNativeArm64, androidNativeX64, androidNativeX86] +// Alias: linux => [linuxArm64, linuxX64] +// Rendering settings: +// - Signature version: 2 +// - Show manifest properties: false +// - Show declarations: true + +// Library unique name: +final class subclasses/A { // subclasses/A|null[0] + constructor () // subclasses/A.|(){}[0] + final class D { // subclasses/A.D|null[0] + constructor () // subclasses/A.D.|(){}[0] + } +} diff --git a/src/functionalTest/resources/examples/classes/Subclasses.kt b/src/functionalTest/resources/examples/classes/Subclasses.kt new file mode 100644 index 00000000..7f3f6392 --- /dev/null +++ b/src/functionalTest/resources/examples/classes/Subclasses.kt @@ -0,0 +1,16 @@ +/* + * Copyright 2016-2023 JetBrains s.r.o. + * Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file. + */ + +package subclasses + +public class A { + public class B { + public class C + } + + public class D { + + } +} diff --git a/src/functionalTest/resources/examples/classes/TopLevelDeclarations.klib.all.dump b/src/functionalTest/resources/examples/classes/TopLevelDeclarations.klib.all.dump new file mode 100644 index 00000000..1b6b628e --- /dev/null +++ b/src/functionalTest/resources/examples/classes/TopLevelDeclarations.klib.all.dump @@ -0,0 +1,74 @@ +// Merged KLib ABI Dump +// Targets: [androidNativeArm32, androidNativeArm64, androidNativeX64, androidNativeX86, iosArm64, iosSimulatorArm64, iosX64, linuxArm64, linuxX64, macosArm64, macosX64, mingwX64, tvosArm64, tvosSimulatorArm64, tvosX64, watchosArm32, watchosArm64, watchosDeviceArm64, watchosSimulatorArm64, watchosX64] +// Alias: apple => [iosArm64, iosSimulatorArm64, iosX64, macosArm64, macosX64, tvosArm64, tvosSimulatorArm64, tvosX64, watchosArm32, watchosArm64, watchosDeviceArm64, watchosSimulatorArm64, watchosX64] +// Alias: watchos => [watchosArm32, watchosArm64, watchosDeviceArm64, watchosSimulatorArm64, watchosX64] +// Alias: ios => [iosArm64, iosSimulatorArm64, iosX64] +// Alias: androidNative => [androidNativeArm32, androidNativeArm64, androidNativeX64, androidNativeX86] +// Alias: tvos => [tvosArm64, tvosSimulatorArm64, tvosX64] +// Alias: linux => [linuxArm64, linuxX64] +// Alias: macos => [macosArm64, macosX64] +// Rendering settings: +// - Signature version: 2 +// - Show manifest properties: false +// - Show declarations: true + +// Library unique name: +abstract class examples.classes/AC { // examples.classes/AC|null[0] + abstract fun a() // examples.classes/AC.a|a(){}[0] + constructor () // examples.classes/AC.|(){}[0] + final fun b() // examples.classes/AC.b|b(){}[0] +} +abstract interface examples.classes/I // examples.classes/I|null[0] +final class examples.classes/C { // examples.classes/C|null[0] + constructor (kotlin/Any) // examples.classes/C.|(kotlin.Any){}[0] + final fun m() // examples.classes/C.m|m(){}[0] + final val v // examples.classes/C.v|{}v[0] + final fun (): kotlin/Any // examples.classes/C.v.|(){}[0] +} +final class examples.classes/D { // examples.classes/D|null[0] + constructor (kotlin/Int) // examples.classes/D.|(kotlin.Int){}[0] + final fun component1(): kotlin/Int // examples.classes/D.component1|component1(){}[0] + final fun copy(kotlin/Int =...): examples.classes/D // examples.classes/D.copy|copy(kotlin.Int){}[0] + final fun equals(kotlin/Any?): kotlin/Boolean // examples.classes/D.equals|equals(kotlin.Any?){}[0] + final fun hashCode(): kotlin/Int // examples.classes/D.hashCode|hashCode(){}[0] + final fun toString(): kotlin/String // examples.classes/D.toString|toString(){}[0] + final val x // examples.classes/D.x|{}x[0] + final fun (): kotlin/Int // examples.classes/D.x.|(){}[0] +} +final class examples.classes/Outer { // examples.classes/Outer|null[0] + constructor () // examples.classes/Outer.|(){}[0] + final class Nested { // examples.classes/Outer.Nested|null[0] + constructor () // examples.classes/Outer.Nested.|(){}[0] + final inner class Inner { // examples.classes/Outer.Nested.Inner|null[0] + constructor () // examples.classes/Outer.Nested.Inner.|(){}[0] + } + } +} +final const val examples.classes/con // examples.classes/con|{}con[0] + final fun (): kotlin/String // examples.classes/con.|(){}[0] +final enum class examples.classes/E : kotlin/Enum { // examples.classes/E|null[0] + enum entry A // examples.classes/E.A|null[0] + enum entry B // examples.classes/E.B|null[0] + enum entry C // examples.classes/E.C|null[0] + final fun valueOf(kotlin/String): examples.classes/E // examples.classes/E.valueOf|valueOf#static(kotlin.String){}[0] + final fun values(): kotlin/Array // examples.classes/E.values|values#static(){}[0] + final val entries // examples.classes/E.entries|#static{}entries[0] + final fun (): kotlin.enums/EnumEntries // examples.classes/E.entries.|#static(){}[0] +} +final fun <#A: kotlin/Any?> examples.classes/consume(#A) // examples.classes/consume|consume(0:0){0§}[0] +final fun examples.classes/testFun(): kotlin/Int // examples.classes/testFun|testFun(){}[0] +final inline fun examples.classes/testInlineFun() // examples.classes/testInlineFun|testInlineFun(){}[0] +final object examples.classes/O // examples.classes/O|null[0] +final val examples.classes/l // examples.classes/l|{}l[0] + final fun (): kotlin/Long // examples.classes/l.|(){}[0] +final var examples.classes/r // examples.classes/r|{}r[0] + final fun (): kotlin/Float // examples.classes/r.|(){}[0] + final fun (kotlin/Float) // examples.classes/r.|(kotlin.Float){}[0] +open annotation class examples.classes/A : kotlin/Annotation { // examples.classes/A|null[0] + constructor () // examples.classes/A.|(){}[0] +} +open class examples.classes/OC { // examples.classes/OC|null[0] + constructor () // examples.classes/OC.|(){}[0] + final fun c() // examples.classes/OC.c|c(){}[0] + open fun o(): kotlin/Int // examples.classes/OC.o|o(){}[0] +} diff --git a/src/functionalTest/resources/examples/classes/TopLevelDeclarations.klib.dump b/src/functionalTest/resources/examples/classes/TopLevelDeclarations.klib.dump new file mode 100644 index 00000000..247b5e2c --- /dev/null +++ b/src/functionalTest/resources/examples/classes/TopLevelDeclarations.klib.dump @@ -0,0 +1,68 @@ +// Merged KLib ABI Dump +// Targets: [androidNativeArm32, androidNativeArm64, androidNativeX64, androidNativeX86, linuxArm64, linuxX64, mingwX64] +// Alias: androidNative => [androidNativeArm32, androidNativeArm64, androidNativeX64, androidNativeX86] +// Rendering settings: +// - Signature version: 2 +// - Show manifest properties: false +// - Show declarations: true + +// Library unique name: +abstract class examples.classes/AC { // examples.classes/AC|null[0] + abstract fun a() // examples.classes/AC.a|a(){}[0] + constructor () // examples.classes/AC.|(){}[0] + final fun b() // examples.classes/AC.b|b(){}[0] +} +abstract interface examples.classes/I // examples.classes/I|null[0] +final class examples.classes/C { // examples.classes/C|null[0] + constructor (kotlin/Any) // examples.classes/C.|(kotlin.Any){}[0] + final fun m() // examples.classes/C.m|m(){}[0] + final val v // examples.classes/C.v|{}v[0] + final fun (): kotlin/Any // examples.classes/C.v.|(){}[0] +} +final class examples.classes/D { // examples.classes/D|null[0] + constructor (kotlin/Int) // examples.classes/D.|(kotlin.Int){}[0] + final fun component1(): kotlin/Int // examples.classes/D.component1|component1(){}[0] + final fun copy(kotlin/Int =...): examples.classes/D // examples.classes/D.copy|copy(kotlin.Int){}[0] + final fun equals(kotlin/Any?): kotlin/Boolean // examples.classes/D.equals|equals(kotlin.Any?){}[0] + final fun hashCode(): kotlin/Int // examples.classes/D.hashCode|hashCode(){}[0] + final fun toString(): kotlin/String // examples.classes/D.toString|toString(){}[0] + final val x // examples.classes/D.x|{}x[0] + final fun (): kotlin/Int // examples.classes/D.x.|(){}[0] +} +final class examples.classes/Outer { // examples.classes/Outer|null[0] + constructor () // examples.classes/Outer.|(){}[0] + final class Nested { // examples.classes/Outer.Nested|null[0] + constructor () // examples.classes/Outer.Nested.|(){}[0] + final inner class Inner { // examples.classes/Outer.Nested.Inner|null[0] + constructor () // examples.classes/Outer.Nested.Inner.|(){}[0] + } + } +} +final const val examples.classes/con // examples.classes/con|{}con[0] + final fun (): kotlin/String // examples.classes/con.|(){}[0] +final enum class examples.classes/E : kotlin/Enum { // examples.classes/E|null[0] + enum entry A // examples.classes/E.A|null[0] + enum entry B // examples.classes/E.B|null[0] + enum entry C // examples.classes/E.C|null[0] + final fun valueOf(kotlin/String): examples.classes/E // examples.classes/E.valueOf|valueOf#static(kotlin.String){}[0] + final fun values(): kotlin/Array // examples.classes/E.values|values#static(){}[0] + final val entries // examples.classes/E.entries|#static{}entries[0] + final fun (): kotlin.enums/EnumEntries // examples.classes/E.entries.|#static(){}[0] +} +final fun <#A: kotlin/Any?> examples.classes/consume(#A) // examples.classes/consume|consume(0:0){0§}[0] +final fun examples.classes/testFun(): kotlin/Int // examples.classes/testFun|testFun(){}[0] +final inline fun examples.classes/testInlineFun() // examples.classes/testInlineFun|testInlineFun(){}[0] +final object examples.classes/O // examples.classes/O|null[0] +final val examples.classes/l // examples.classes/l|{}l[0] + final fun (): kotlin/Long // examples.classes/l.|(){}[0] +final var examples.classes/r // examples.classes/r|{}r[0] + final fun (): kotlin/Float // examples.classes/r.|(){}[0] + final fun (kotlin/Float) // examples.classes/r.|(kotlin.Float){}[0] +open annotation class examples.classes/A : kotlin/Annotation { // examples.classes/A|null[0] + constructor () // examples.classes/A.|(){}[0] +} +open class examples.classes/OC { // examples.classes/OC|null[0] + constructor () // examples.classes/OC.|(){}[0] + final fun c() // examples.classes/OC.c|c(){}[0] + open fun o(): kotlin/Int // examples.classes/OC.o|o(){}[0] +} diff --git a/src/functionalTest/resources/examples/classes/TopLevelDeclarations.klib.unsup.dump b/src/functionalTest/resources/examples/classes/TopLevelDeclarations.klib.unsup.dump new file mode 100644 index 00000000..31a322a8 --- /dev/null +++ b/src/functionalTest/resources/examples/classes/TopLevelDeclarations.klib.unsup.dump @@ -0,0 +1,67 @@ +// Merged KLib ABI Dump +// Targets: [androidNativeArm32, androidNativeArm64, androidNativeX64, androidNativeX86, linuxX64, mingwX64] +// Rendering settings: +// - Signature version: 2 +// - Show manifest properties: false +// - Show declarations: true + +// Library unique name: +abstract class examples.classes/AC { // examples.classes/AC|null[0] + abstract fun a() // examples.classes/AC.a|a(){}[0] + constructor () // examples.classes/AC.|(){}[0] + final fun b() // examples.classes/AC.b|b(){}[0] +} +abstract interface examples.classes/I // examples.classes/I|null[0] +final class examples.classes/C { // examples.classes/C|null[0] + constructor (kotlin/Any) // examples.classes/C.|(kotlin.Any){}[0] + final fun m() // examples.classes/C.m|m(){}[0] + final val v // examples.classes/C.v|{}v[0] + final fun (): kotlin/Any // examples.classes/C.v.|(){}[0] +} +final class examples.classes/D { // examples.classes/D|null[0] + constructor (kotlin/Int) // examples.classes/D.|(kotlin.Int){}[0] + final fun component1(): kotlin/Int // examples.classes/D.component1|component1(){}[0] + final fun copy(kotlin/Int =...): examples.classes/D // examples.classes/D.copy|copy(kotlin.Int){}[0] + final fun equals(kotlin/Any?): kotlin/Boolean // examples.classes/D.equals|equals(kotlin.Any?){}[0] + final fun hashCode(): kotlin/Int // examples.classes/D.hashCode|hashCode(){}[0] + final fun toString(): kotlin/String // examples.classes/D.toString|toString(){}[0] + final val x // examples.classes/D.x|{}x[0] + final fun (): kotlin/Int // examples.classes/D.x.|(){}[0] +} +final class examples.classes/Outer { // examples.classes/Outer|null[0] + constructor () // examples.classes/Outer.|(){}[0] + final class Nested { // examples.classes/Outer.Nested|null[0] + constructor () // examples.classes/Outer.Nested.|(){}[0] + final inner class Inner { // examples.classes/Outer.Nested.Inner|null[0] + constructor () // examples.classes/Outer.Nested.Inner.|(){}[0] + } + } +} +final const val examples.classes/con // examples.classes/con|{}con[0] + final fun (): kotlin/String // examples.classes/con.|(){}[0] +final enum class examples.classes/E : kotlin/Enum { // examples.classes/E|null[0] + enum entry A // examples.classes/E.A|null[0] + enum entry B // examples.classes/E.B|null[0] + enum entry C // examples.classes/E.C|null[0] + final fun valueOf(kotlin/String): examples.classes/E // examples.classes/E.valueOf|valueOf#static(kotlin.String){}[0] + final fun values(): kotlin/Array // examples.classes/E.values|values#static(){}[0] + final val entries // examples.classes/E.entries|#static{}entries[0] + final fun (): kotlin.enums/EnumEntries // examples.classes/E.entries.|#static(){}[0] +} +final fun <#A: kotlin/Any?> examples.classes/consume(#A) // examples.classes/consume|consume(0:0){0§}[0] +final fun examples.classes/testFun(): kotlin/Int // examples.classes/testFun|testFun(){}[0] +final inline fun examples.classes/testInlineFun() // examples.classes/testInlineFun|testInlineFun(){}[0] +final object examples.classes/O // examples.classes/O|null[0] +final val examples.classes/l // examples.classes/l|{}l[0] + final fun (): kotlin/Long // examples.classes/l.|(){}[0] +final var examples.classes/r // examples.classes/r|{}r[0] + final fun (): kotlin/Float // examples.classes/r.|(){}[0] + final fun (kotlin/Float) // examples.classes/r.|(kotlin.Float){}[0] +open annotation class examples.classes/A : kotlin/Annotation { // examples.classes/A|null[0] + constructor () // examples.classes/A.|(){}[0] +} +open class examples.classes/OC { // examples.classes/OC|null[0] + constructor () // examples.classes/OC.|(){}[0] + final fun c() // examples.classes/OC.c|c(){}[0] + open fun o(): kotlin/Int // examples.classes/OC.o|o(){}[0] +} diff --git a/src/functionalTest/resources/examples/classes/TopLevelDeclarations.klib.v1.dump b/src/functionalTest/resources/examples/classes/TopLevelDeclarations.klib.v1.dump new file mode 100644 index 00000000..1c85eb12 --- /dev/null +++ b/src/functionalTest/resources/examples/classes/TopLevelDeclarations.klib.v1.dump @@ -0,0 +1,67 @@ +// Merged KLib ABI Dump +// Targets: [androidNativeArm32, androidNativeArm64, androidNativeX64, androidNativeX86, linuxArm64, linuxX64, mingwX64] +// Rendering settings: +// - Signature version: 1 +// - Show manifest properties: false +// - Show declarations: true + +// Library unique name: +abstract class examples.classes/AC { // examples.classes/AC|null[0] + abstract fun a() // examples.classes/AC.a|-4432112437378250461[0] + constructor () // examples.classes/AC.|-5645683436151566731[0] + final fun b() // examples.classes/AC.b|4789657038926421504[0] +} +abstract interface examples.classes/I // examples.classes/I|null[0] +final class examples.classes/C { // examples.classes/C|null[0] + constructor (kotlin/Any) // examples.classes/C.|4518179880532599055[0] + final fun m() // examples.classes/C.m|-1029306787563722981[0] + final val v // examples.classes/C.v|138869847852828796[0] + final fun (): kotlin/Any // examples.classes/C.v.|4964732996156868941[0] +} +final class examples.classes/D { // examples.classes/D|null[0] + constructor (kotlin/Int) // examples.classes/D.|-5182794243525578284[0] + final fun component1(): kotlin/Int // examples.classes/D.component1|162597135895221648[0] + final fun copy(kotlin/Int =...): examples.classes/D // examples.classes/D.copy|-6971662324481626298[0] + final fun equals(kotlin/Any?): kotlin/Boolean // examples.classes/D.equals|4638265728071529943[0] + final fun hashCode(): kotlin/Int // examples.classes/D.hashCode|3409210261493131192[0] + final fun toString(): kotlin/String // examples.classes/D.toString|-1522858123163872138[0] + final val x // examples.classes/D.x|-8060530855978347579[0] + final fun (): kotlin/Int // examples.classes/D.x.|1482705010654679335[0] +} +final class examples.classes/Outer { // examples.classes/Outer|null[0] + constructor () // examples.classes/Outer.|-5645683436151566731[0] + final class Nested { // examples.classes/Outer.Nested|null[0] + constructor () // examples.classes/Outer.Nested.|-5645683436151566731[0] + final inner class Inner { // examples.classes/Outer.Nested.Inner|null[0] + constructor () // examples.classes/Outer.Nested.Inner.|-5645683436151566731[0] + } + } +} +final const val examples.classes/con // examples.classes/con|-2899158152154217071[0] + final fun (): kotlin/String // examples.classes/con.|-2604863570302238407[0] +final enum class examples.classes/E : kotlin/Enum { // examples.classes/E|null[0] + enum entry A // examples.classes/E.A|null[0] + enum entry B // examples.classes/E.B|null[0] + enum entry C // examples.classes/E.C|null[0] + final fun valueOf(kotlin/String): examples.classes/E // examples.classes/E.valueOf|-4683474617854611729[0] + final fun values(): kotlin/Array // examples.classes/E.values|-8715569000920726747[0] + final val entries // examples.classes/E.entries|-5134227801081826149[0] + final fun (): kotlin.enums/EnumEntries // examples.classes/E.entries.|-6068527377476727729[0] +} +final fun <#A: kotlin/Any?> examples.classes/consume(#A) // examples.classes/consume|8042761629495509481[0] +final fun examples.classes/testFun(): kotlin/Int // examples.classes/testFun|6322333980269160703[0] +final inline fun examples.classes/testInlineFun() // examples.classes/testInlineFun|-9193388292326484960[0] +final object examples.classes/O // examples.classes/O|null[0] +final val examples.classes/l // examples.classes/l|3307215303229595169[0] + final fun (): kotlin/Long // examples.classes/l.|3795442967620585[0] +final var examples.classes/r // examples.classes/r|-8117627916896159533[0] + final fun (): kotlin/Float // examples.classes/r.|-7424184448774736572[0] + final fun (kotlin/Float) // examples.classes/r.|9171637170963327464[0] +open annotation class examples.classes/A : kotlin/Annotation { // examples.classes/A|null[0] + constructor () // examples.classes/A.|-5645683436151566731[0] +} +open class examples.classes/OC { // examples.classes/OC|null[0] + constructor () // examples.classes/OC.|-5645683436151566731[0] + final fun c() // examples.classes/OC.c|-2724918380551733646[0] + open fun o(): kotlin/Int // examples.classes/OC.o|-3264635847192431671[0] +} diff --git a/src/functionalTest/resources/examples/classes/TopLevelDeclarations.klib.with.linux.dump b/src/functionalTest/resources/examples/classes/TopLevelDeclarations.klib.with.linux.dump new file mode 100644 index 00000000..6c1f6dbb --- /dev/null +++ b/src/functionalTest/resources/examples/classes/TopLevelDeclarations.klib.with.linux.dump @@ -0,0 +1,69 @@ +// Merged KLib ABI Dump +// Targets: [androidNativeArm32, androidNativeArm64, androidNativeX64, androidNativeX86, linuxArm64, linuxX64, mingwX64] +// Alias: androidNative => [androidNativeArm32, androidNativeArm64, androidNativeX64, androidNativeX86] +// Alias: linux => [linuxArm64, linuxX64] +// Rendering settings: +// - Signature version: 2 +// - Show manifest properties: false +// - Show declarations: true + +// Library unique name: +abstract class examples.classes/AC { // examples.classes/AC|null[0] + abstract fun a() // examples.classes/AC.a|a(){}[0] + constructor () // examples.classes/AC.|(){}[0] + final fun b() // examples.classes/AC.b|b(){}[0] +} +abstract interface examples.classes/I // examples.classes/I|null[0] +final class examples.classes/C { // examples.classes/C|null[0] + constructor (kotlin/Any) // examples.classes/C.|(kotlin.Any){}[0] + final fun m() // examples.classes/C.m|m(){}[0] + final val v // examples.classes/C.v|{}v[0] + final fun (): kotlin/Any // examples.classes/C.v.|(){}[0] +} +final class examples.classes/D { // examples.classes/D|null[0] + constructor (kotlin/Int) // examples.classes/D.|(kotlin.Int){}[0] + final fun component1(): kotlin/Int // examples.classes/D.component1|component1(){}[0] + final fun copy(kotlin/Int =...): examples.classes/D // examples.classes/D.copy|copy(kotlin.Int){}[0] + final fun equals(kotlin/Any?): kotlin/Boolean // examples.classes/D.equals|equals(kotlin.Any?){}[0] + final fun hashCode(): kotlin/Int // examples.classes/D.hashCode|hashCode(){}[0] + final fun toString(): kotlin/String // examples.classes/D.toString|toString(){}[0] + final val x // examples.classes/D.x|{}x[0] + final fun (): kotlin/Int // examples.classes/D.x.|(){}[0] +} +final class examples.classes/Outer { // examples.classes/Outer|null[0] + constructor () // examples.classes/Outer.|(){}[0] + final class Nested { // examples.classes/Outer.Nested|null[0] + constructor () // examples.classes/Outer.Nested.|(){}[0] + final inner class Inner { // examples.classes/Outer.Nested.Inner|null[0] + constructor () // examples.classes/Outer.Nested.Inner.|(){}[0] + } + } +} +final const val examples.classes/con // examples.classes/con|{}con[0] + final fun (): kotlin/String // examples.classes/con.|(){}[0] +final enum class examples.classes/E : kotlin/Enum { // examples.classes/E|null[0] + enum entry A // examples.classes/E.A|null[0] + enum entry B // examples.classes/E.B|null[0] + enum entry C // examples.classes/E.C|null[0] + final fun valueOf(kotlin/String): examples.classes/E // examples.classes/E.valueOf|valueOf#static(kotlin.String){}[0] + final fun values(): kotlin/Array // examples.classes/E.values|values#static(){}[0] + final val entries // examples.classes/E.entries|#static{}entries[0] + final fun (): kotlin.enums/EnumEntries // examples.classes/E.entries.|#static(){}[0] +} +final fun <#A: kotlin/Any?> examples.classes/consume(#A) // examples.classes/consume|consume(0:0){0§}[0] +final fun examples.classes/testFun(): kotlin/Int // examples.classes/testFun|testFun(){}[0] +final inline fun examples.classes/testInlineFun() // examples.classes/testInlineFun|testInlineFun(){}[0] +final object examples.classes/O // examples.classes/O|null[0] +final val examples.classes/l // examples.classes/l|{}l[0] + final fun (): kotlin/Long // examples.classes/l.|(){}[0] +final var examples.classes/r // examples.classes/r|{}r[0] + final fun (): kotlin/Float // examples.classes/r.|(){}[0] + final fun (kotlin/Float) // examples.classes/r.|(kotlin.Float){}[0] +open annotation class examples.classes/A : kotlin/Annotation { // examples.classes/A|null[0] + constructor () // examples.classes/A.|(){}[0] +} +open class examples.classes/OC { // examples.classes/OC|null[0] + constructor () // examples.classes/OC.|(){}[0] + final fun c() // examples.classes/OC.c|c(){}[0] + open fun o(): kotlin/Int // examples.classes/OC.o|o(){}[0] +} diff --git a/src/functionalTest/resources/examples/classes/TopLevelDeclarations.kt b/src/functionalTest/resources/examples/classes/TopLevelDeclarations.kt new file mode 100644 index 00000000..067bd288 --- /dev/null +++ b/src/functionalTest/resources/examples/classes/TopLevelDeclarations.kt @@ -0,0 +1,38 @@ +/* + * Copyright 2016-2023 JetBrains s.r.o. + * Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file. + */ + +package examples.classes + +public fun testFun(): Int = 42 +public fun consume(arg: T) = Unit +public inline fun testInlineFun() = Unit +public const val con: String = "I'm a constant!" +public val l: Long = 0xc001 +public var r: Float = 3.14f + +public annotation class A +public interface I +public data class D(val x: Int) +public class C(public val v: Any) { + public fun m() = Unit +} + +public object O +public enum class E { A, B, C } +public abstract class AC { + public abstract fun a() + public fun b() = Unit +} +public open class OC { + public open fun o(): Int = 42 + public fun c() = Unit +} +public class Outer { + public class Nested { + public inner class Inner { + + } + } +} diff --git a/src/functionalTest/resources/examples/gradle/base/enableJvmInWithNativePlugin.gradle.kts b/src/functionalTest/resources/examples/gradle/base/enableJvmInWithNativePlugin.gradle.kts new file mode 100644 index 00000000..439d731f --- /dev/null +++ b/src/functionalTest/resources/examples/gradle/base/enableJvmInWithNativePlugin.gradle.kts @@ -0,0 +1,9 @@ +/* + * Copyright 2016-2023 JetBrains s.r.o. + * Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file. + */ + +kotlin { + jvm { + } +} diff --git a/src/functionalTest/resources/examples/gradle/base/withNativePlugin.gradle.kts b/src/functionalTest/resources/examples/gradle/base/withNativePlugin.gradle.kts new file mode 100644 index 00000000..ed1e174c --- /dev/null +++ b/src/functionalTest/resources/examples/gradle/base/withNativePlugin.gradle.kts @@ -0,0 +1,38 @@ +/* + * Copyright 2016-2023 JetBrains s.r.o. + * Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file. + */ + +plugins { + kotlin("multiplatform") version "1.9.10" + id("org.jetbrains.kotlinx.binary-compatibility-validator") +} + +repositories { + mavenCentral() +} + +kotlin { + linuxX64() + linuxArm64() + mingwX64() + androidNativeArm32() + androidNativeArm64() + androidNativeX64() + androidNativeX86() + + sourceSets { + val commonMain by getting + val commonTest by getting { + dependencies { + implementation(kotlin("stdlib")) + implementation(kotlin("test-common")) + implementation(kotlin("test-annotations-common")) + } + } + } +} + +apiValidation { + klib.enabled = true +} diff --git a/src/functionalTest/resources/examples/gradle/base/withNativePluginAndNoTargets.gradle.kts b/src/functionalTest/resources/examples/gradle/base/withNativePluginAndNoTargets.gradle.kts new file mode 100644 index 00000000..50668766 --- /dev/null +++ b/src/functionalTest/resources/examples/gradle/base/withNativePluginAndNoTargets.gradle.kts @@ -0,0 +1,30 @@ +/* + * Copyright 2016-2023 JetBrains s.r.o. + * Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file. + */ + +plugins { + kotlin("multiplatform") version "1.9.10" + id("org.jetbrains.kotlinx.binary-compatibility-validator") +} + +repositories { + mavenCentral() +} + +kotlin { + sourceSets { + val commonMain by getting + val commonTest by getting { + dependencies { + implementation(kotlin("stdlib")) + implementation(kotlin("test-common")) + implementation(kotlin("test-annotations-common")) + } + } + } +} + +apiValidation { + klib.enabled = true +} diff --git a/src/functionalTest/resources/examples/gradle/base/withNativePluginAndSingleTarget.gradle.kts b/src/functionalTest/resources/examples/gradle/base/withNativePluginAndSingleTarget.gradle.kts new file mode 100644 index 00000000..db44005d --- /dev/null +++ b/src/functionalTest/resources/examples/gradle/base/withNativePluginAndSingleTarget.gradle.kts @@ -0,0 +1,32 @@ +/* + * Copyright 2016-2023 JetBrains s.r.o. + * Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file. + */ + +plugins { + kotlin("multiplatform") version "1.9.10" + id("org.jetbrains.kotlinx.binary-compatibility-validator") +} + +repositories { + mavenCentral() +} + +kotlin { + linuxArm64() + + sourceSets { + val commonMain by getting + val commonTest by getting { + dependencies { + implementation(kotlin("stdlib")) + implementation(kotlin("test-common")) + implementation(kotlin("test-annotations-common")) + } + } + } +} + +apiValidation { + klib.enabled = true +} diff --git a/src/functionalTest/resources/examples/gradle/configuration/appleTargets/targets.gradle.kts b/src/functionalTest/resources/examples/gradle/configuration/appleTargets/targets.gradle.kts new file mode 100644 index 00000000..c3384eb8 --- /dev/null +++ b/src/functionalTest/resources/examples/gradle/configuration/appleTargets/targets.gradle.kts @@ -0,0 +1,20 @@ +/* + * Copyright 2016-2023 JetBrains s.r.o. + * Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file. + */ + +kotlin { + macosX64() + macosArm64() + iosX64() + iosArm64() + iosSimulatorArm64() + tvosX64() + tvosArm64() + tvosSimulatorArm64() + watchosArm32() + watchosArm64() + watchosX64() + watchosSimulatorArm64() + watchosDeviceArm64() +} diff --git a/src/functionalTest/resources/examples/gradle/configuration/grouping/clashingTargetNames.gradle.kts b/src/functionalTest/resources/examples/gradle/configuration/grouping/clashingTargetNames.gradle.kts new file mode 100644 index 00000000..5c85af92 --- /dev/null +++ b/src/functionalTest/resources/examples/gradle/configuration/grouping/clashingTargetNames.gradle.kts @@ -0,0 +1,9 @@ +/* + * Copyright 2016-2024 JetBrains s.r.o. + * Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file. + */ + +kotlin { + linuxX64("linux") + linuxArm64() +} diff --git a/src/functionalTest/resources/examples/gradle/configuration/grouping/customTargetNames.gradle.kts b/src/functionalTest/resources/examples/gradle/configuration/grouping/customTargetNames.gradle.kts new file mode 100644 index 00000000..73aafef9 --- /dev/null +++ b/src/functionalTest/resources/examples/gradle/configuration/grouping/customTargetNames.gradle.kts @@ -0,0 +1,17 @@ +/* + * Copyright 2016-2024 JetBrains s.r.o. + * Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file. + */ + +kotlin { + linuxX64("linuxA") { + attributes { + attribute(Attribute.of("variant", String::class.java), "a") + } + } + linuxX64("linuxB") { + attributes { + attribute(Attribute.of("variant", String::class.java), "b") + } + } +} diff --git a/src/functionalTest/resources/examples/gradle/configuration/ignoreSubclasses/ignore.gradle.kts b/src/functionalTest/resources/examples/gradle/configuration/ignoreSubclasses/ignore.gradle.kts new file mode 100644 index 00000000..da82967c --- /dev/null +++ b/src/functionalTest/resources/examples/gradle/configuration/ignoreSubclasses/ignore.gradle.kts @@ -0,0 +1,9 @@ +/* + * Copyright 2016-2023 JetBrains s.r.o. + * Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file. + */ + +configure { + ignoredClasses.add("subclasses.A.B") + //ignoredClasses.add("subclasses.A\$B") +} diff --git a/src/functionalTest/resources/examples/gradle/configuration/nonPublicMarkers/klib.gradle.kts b/src/functionalTest/resources/examples/gradle/configuration/nonPublicMarkers/klib.gradle.kts new file mode 100644 index 00000000..e8533816 --- /dev/null +++ b/src/functionalTest/resources/examples/gradle/configuration/nonPublicMarkers/klib.gradle.kts @@ -0,0 +1,13 @@ +/* + * Copyright 2016-2023 JetBrains s.r.o. + * Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file. + */ + +configure { + nonPublicMarkers.add("annotations.HiddenClass") + nonPublicMarkers.add("annotations.HiddenCtor") + nonPublicMarkers.add("annotations.HiddenProperty") + nonPublicMarkers.add("annotations.HiddenGetter") + nonPublicMarkers.add("annotations.HiddenSetter") + nonPublicMarkers.add("annotations.HiddenFunction") +} diff --git a/src/functionalTest/resources/examples/gradle/configuration/signatures/invalid.gradle.kts b/src/functionalTest/resources/examples/gradle/configuration/signatures/invalid.gradle.kts new file mode 100644 index 00000000..1576719d --- /dev/null +++ b/src/functionalTest/resources/examples/gradle/configuration/signatures/invalid.gradle.kts @@ -0,0 +1,8 @@ +/* + * Copyright 2016-2023 JetBrains s.r.o. + * Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file. + */ + +configure { + klibSignatureVersion = 100500 +} diff --git a/src/functionalTest/resources/examples/gradle/configuration/signatures/v1.gradle.kts b/src/functionalTest/resources/examples/gradle/configuration/signatures/v1.gradle.kts new file mode 100644 index 00000000..f7277c58 --- /dev/null +++ b/src/functionalTest/resources/examples/gradle/configuration/signatures/v1.gradle.kts @@ -0,0 +1,8 @@ +/* + * Copyright 2016-2023 JetBrains s.r.o. + * Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file. + */ + +configure { + klib.signatureVersion = 1 +} diff --git a/src/functionalTest/resources/examples/gradle/configuration/unsupported/enforce.gradle.kts b/src/functionalTest/resources/examples/gradle/configuration/unsupported/enforce.gradle.kts new file mode 100644 index 00000000..99270cde --- /dev/null +++ b/src/functionalTest/resources/examples/gradle/configuration/unsupported/enforce.gradle.kts @@ -0,0 +1,8 @@ +/* + * Copyright 2016-2023 JetBrains s.r.o. + * Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file. + */ + +configure { + klib.strictValidation = true +} diff --git a/src/main/kotlin/BinaryCompatibilityValidatorPlugin.kt b/src/main/kotlin/BinaryCompatibilityValidatorPlugin.kt index c117659f..19443f86 100644 --- a/src/main/kotlin/BinaryCompatibilityValidatorPlugin.kt +++ b/src/main/kotlin/BinaryCompatibilityValidatorPlugin.kt @@ -11,10 +11,20 @@ import org.gradle.api.provider.* import org.gradle.api.tasks.* import org.jetbrains.kotlin.gradle.dsl.* import org.jetbrains.kotlin.gradle.plugin.* +import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget +import org.jetbrains.kotlin.konan.target.HostManager +import org.jetbrains.kotlin.library.abi.ExperimentalLibraryAbiReader +import org.jetbrains.kotlin.library.abi.LibraryAbiReader import java.io.* +import kotlin.text.split + +const val API_DIR = "api" +const val KLIB_PHONY_TARGET_NAME = "klib" +const val KLIB_ALL_PHONY_TARGET_NAME = "klib-all" public class BinaryCompatibilityValidatorPlugin : Plugin { + @ExperimentalLibraryAbiReader override fun apply(target: Project): Unit = with(target) { val extension = extensions.create("apiValidation", ApiValidationExtension::class.java) validateExtension(extension) @@ -23,6 +33,7 @@ public class BinaryCompatibilityValidatorPlugin : Plugin { } } + @ExperimentalLibraryAbiReader private fun Project.validateExtension(extension: ApiValidationExtension) { afterEvaluate { val ignored = extension.ignoredProjects @@ -30,6 +41,16 @@ public class BinaryCompatibilityValidatorPlugin : Plugin { for (project in ignored) { require(project in all) { "Cannot find excluded project $project in all projects: $all" } } + if (extension.klib.enabled) { + try { + LibraryAbiReader.javaClass + } catch (e: NoClassDefFoundError) { + throw IllegalStateException( + "KLib validation is not available. " + + "Make sure the project use at least Kotlin 1.9.20.", e + ) + } + } } } @@ -54,7 +75,7 @@ public class BinaryCompatibilityValidatorPlugin : Plugin { extension: ApiValidationExtension ) = configurePlugin("kotlin-multiplatform", project, extension) { if (project.name in extension.ignoredProjects) return@configurePlugin - val kotlin = project.extensions.getByName("kotlin") as KotlinMultiplatformExtension + val kotlin = project.kotlinMultiplatform // Create common tasks for multiplatform val commonApiDump = project.tasks.register("apiDump") { @@ -76,16 +97,15 @@ public class BinaryCompatibilityValidatorPlugin : Plugin { } } - val dirConfig = jvmTargetCountProvider.map { + val jvmDirConfig = jvmTargetCountProvider.map { if (it == 1) DirConfig.COMMON else DirConfig.TARGET_DIR } + val klibDirConfig = project.provider { DirConfig.COMMON } - kotlin.targets.matching { - it.platformType == KotlinPlatformType.jvm || it.platformType == KotlinPlatformType.androidJvm - }.all { target -> - val targetConfig = TargetConfig(project, extension, target.name, dirConfig) + kotlin.targets.matching { it.jvmBased }.all { target -> + val targetConfig = TargetConfig(project, extension, target.name, jvmDirConfig) if (target.platformType == KotlinPlatformType.jvm) { - target.compilations.matching { it.name == "main" }.all { + target.mainCompilations.all { project.configureKotlinCompilation(it, extension, targetConfig, commonApiDump, commonApiCheck) } } else if (target.platformType == KotlinPlatformType.androidJvm) { @@ -101,6 +121,7 @@ public class BinaryCompatibilityValidatorPlugin : Plugin { } } } + KlibValidationPipelineBuilder(klibDirConfig, extension).configureTasks(project, commonApiDump, commonApiCheck) } private fun configureAndroidPlugin( @@ -180,7 +201,7 @@ private enum class DirConfig { * the resulting paths will be * `/api/jvm/project.api` and `/api/android/project.api` */ - TARGET_DIR, + TARGET_DIR } private fun Project.configureKotlinCompilation( @@ -192,13 +213,17 @@ private fun Project.configureKotlinCompilation( useOutput: Boolean = false, ) { val projectName = project.name + val dumpFileName = project.jvmDumpFileName val apiDirProvider = targetConfig.apiDir val apiBuildDir = apiDirProvider.map { layout.buildDirectory.asFile.get().resolve(it) } val apiBuild = task(targetConfig.apiTaskName("Build")) { // Do not enable task for empty umbrella modules isEnabled = - apiCheckEnabled(projectName, extension) && compilation.allKotlinSourceSets.any { it.kotlin.srcDirs.any { it.exists() } } + apiCheckEnabled( + projectName, + extension + ) && compilation.allKotlinSourceSets.any { it.kotlin.srcDirs.any { it.exists() } } // 'group' is not specified deliberately, so it will be hidden from ./gradlew tasks description = "Builds Kotlin API for 'main' compilations of $projectName. Complementary task and shouldn't be called manually" @@ -214,7 +239,7 @@ private fun Project.configureKotlinCompilation( inputDependencies = files(provider { if (isEnabled) compilation.compileDependencyFiles else emptyList() }) } - outputApiDir = apiBuildDir.get() + outputApiFile = apiBuildDir.get().resolve(dumpFileName) } configureCheckTasks(apiBuildDir, apiBuild, extension, targetConfig, commonApiDump, commonApiCheck) } @@ -231,11 +256,15 @@ internal val Project.apiValidationExtensionOrNull: ApiValidationExtension? private fun apiCheckEnabled(projectName: String, extension: ApiValidationExtension): Boolean = projectName !in extension.ignoredProjects && !extension.validationDisabled +fun klibAbiCheckEnabled(projectName: String, extension: ApiValidationExtension): Boolean = + projectName !in extension.ignoredProjects && !extension.validationDisabled && extension.klib.enabled + private fun Project.configureApiTasks( extension: ApiValidationExtension, targetConfig: TargetConfig = TargetConfig(this, extension), ) { val projectName = project.name + val dumpFileName = project.jvmDumpFileName val apiBuildDir = targetConfig.apiDir.map { layout.buildDirectory.asFile.get().resolve(it) } val sourceSetsOutputsProvider = project.provider { sourceSets @@ -249,8 +278,9 @@ private fun Project.configureApiTasks( description = "Builds Kotlin API for 'main' compilations of $projectName. Complementary task and shouldn't be called manually" inputClassesDirs = files(provider { if (isEnabled) sourceSetsOutputsProvider.get() else emptyList() }) - inputDependencies = files(provider { if (isEnabled) sourceSetsOutputsProvider.get() else emptyList() }) - outputApiDir = apiBuildDir.get() + inputDependencies = + files(provider { if (isEnabled) sourceSetsOutputsProvider.get() else emptyList() }) + outputApiFile = apiBuildDir.get().resolve(dumpFileName) } configureCheckTasks(apiBuildDir, apiBuild, extension, targetConfig) @@ -258,7 +288,7 @@ private fun Project.configureApiTasks( private fun Project.configureCheckTasks( apiBuildDir: Provider, - apiBuild: TaskProvider, + apiBuild: TaskProvider<*>, extension: ApiValidationExtension, targetConfig: TargetConfig, commonApiDump: TaskProvider? = null, @@ -274,16 +304,18 @@ private fun Project.configureCheckTasks( isEnabled = apiCheckEnabled(projectName, extension) && apiBuild.map { it.enabled }.getOrElse(true) group = "verification" description = "Checks signatures of public API against the golden value in API folder for $projectName" - compareApiDumps(apiReferenceDir = apiCheckDir.get(), apiBuildDir = apiBuildDir.get()) + projectApiFile = apiCheckDir.get().resolve(jvmDumpFileName) + generatedApiFile = apiBuildDir.get().resolve(jvmDumpFileName) dependsOn(apiBuild) } - val apiDump = task(targetConfig.apiTaskName("Dump")) { + val dumpFileName = project.jvmDumpFileName + val apiDump = task(targetConfig.apiTaskName("Dump")) { isEnabled = apiCheckEnabled(projectName, extension) && apiBuild.map { it.enabled }.getOrElse(true) group = "other" description = "Syncs API from build dir to ${targetConfig.apiDir} dir for $projectName" - from(apiBuildDir) - into(apiCheckDir) + from = apiBuildDir.get().resolve(dumpFileName) + to = apiCheckDir.get().resolve(dumpFileName) dependsOn(apiBuild) } @@ -299,3 +331,290 @@ private inline fun Project.task( name: String, noinline configuration: T.() -> Unit, ): TaskProvider = tasks.register(name, T::class.java, Action(configuration)) + +const val BANNED_TARGETS_PROPERTY_NAME = "binary.compatibility.validator.klib.targets.blacklist.for.testing" + +private class KlibValidationPipelineBuilder( + val dirConfig: Provider?, + val extension: ApiValidationExtension +) { + lateinit var intermediateFilesConfig: Provider + + fun configureTasks(project: Project, commonApiDump: TaskProvider, commonApiCheck: TaskProvider) { + // In the intermediate phase of Klib dump generation there are always multiple targets, thus we need + // target-based directory tree. + intermediateFilesConfig = project.provider { DirConfig.TARGET_DIR } + val klibApiDirConfig = dirConfig?.map { TargetConfig(project, KLIB_PHONY_TARGET_NAME, dirConfig) } + val klibDumpConfig = TargetConfig(project, KLIB_PHONY_TARGET_NAME, intermediateFilesConfig) + val klibDumpAllConfig = TargetConfig(project, KLIB_ALL_PHONY_TARGET_NAME, intermediateFilesConfig) + + val projectDir = project.projectDir + val klibApiDir = klibApiDirConfig?.map { + projectDir.resolve(it.apiDir.get()) + }!! + val klibMergeDir = project.buildDir.resolve(klibDumpConfig.apiDir.get()) + val klibMergeAllDir = project.buildDir.resolve(klibDumpAllConfig.apiDir.get()) + val klibExtractedFileDir = klibMergeAllDir.resolve("extracted") + + val klibMerge = project.mergeKlibsUmbrellaTask(klibDumpConfig, klibMergeDir) + val klibMergeAll = project.mergeAllKlibsUmbrellaTask(klibDumpConfig, klibMergeAllDir) + val klibDump = project.dumpKlibsTask(klibDumpConfig, klibApiDir, klibMergeAllDir) + val klibExtractAbiForSupportedTargets = project.extractAbi(klibDumpConfig, klibApiDir, klibExtractedFileDir) + val klibCheck = project.checkKlibsTask(klibDumpConfig, project.provider { klibExtractedFileDir }, klibMergeDir) + + commonApiDump.configure { it.dependsOn(klibDump) } + commonApiCheck.configure { it.dependsOn(klibCheck) } + + klibDump.configure { it.dependsOn(klibMergeAll) } + klibCheck.configure { + it.dependsOn(klibExtractAbiForSupportedTargets) + it.dependsOn(klibMerge) + } + + project.configureTargets(klibApiDir, klibMerge, klibMergeAll) + } + + private fun Project.checkKlibsTask( + klibDumpConfig: TargetConfig, + klibApiDir: Provider, + klibMergeDir: File + ) = project.task(klibDumpConfig.apiTaskName("Check")) { + isEnabled = klibAbiCheckEnabled(project.name, extension) + group = "verification" + description = "Checks signatures of public klib ABI against the golden value in ABI folder for " + + project.name + projectApiFile = klibApiDir.get().resolve(klibDumpFileName) + generatedApiFile = klibMergeDir.resolve(klibDumpFileName) + } + + private fun Project.dumpKlibsTask( + klibDumpConfig: TargetConfig, + klibApiDir: Provider, + klibMergeDir: File + ) = project.task(klibDumpConfig.apiTaskName("Dump")) { + isEnabled = klibAbiCheckEnabled(project.name, extension) + description = "Syncs klib ABI dump from build dir to ${klibDumpConfig.apiDir} dir for ${project.name}" + group = "other" + from = klibMergeDir.resolve(klibDumpFileName) + to = klibApiDir.get().resolve(klibDumpFileName) + } + + private fun Project.extractAbi( + klibDumpConfig: TargetConfig, + klibApiDir: Provider, + klibOutputDir: File + ) = project.task(klibDumpConfig.apiTaskName("PrepareAbiForValidation")) { + isEnabled = klibAbiCheckEnabled(project.name, extension) + description = "Prepare a reference ABI file by removing all unsupported targets from it" + group = "other" + strictValidation = extension.klib.strictValidation + groupTargetNames = extension.klib.useTargetGroupAliases + targets = supportedTargets() + inputAbiFile = klibApiDir.get().resolve(klibDumpFileName) + outputAbiFile = klibOutputDir.resolve(klibDumpFileName) + } + + private fun Project.mergeAllKlibsUmbrellaTask( + klibDumpConfig: TargetConfig, + klibMergeDir: File, + ) = project.task( + klibDumpConfig.apiTaskName("MergeAll") + ) + { + isEnabled = klibAbiCheckEnabled(project.name, extension) + description = "Merges multiple klib ABI dump files generated for " + + "different targets (including files substituting dumps for unsupported target) " + + "into a single multi-target dump" + dumpFileName = klibDumpFileName + mergedFile = klibMergeDir.resolve(klibDumpFileName) + groupTargetNames = extension.klib.useTargetGroupAliases + } + + private fun Project.mergeKlibsUmbrellaTask( + klibDumpConfig: TargetConfig, + klibMergeDir: File + ) = project.task(klibDumpConfig.apiTaskName("Merge")) { + isEnabled = klibAbiCheckEnabled(project.name, extension) + description = "Merges multiple klib ABI dump files generated for " + + "different targets into a single multi-target dump" + dumpFileName = klibDumpFileName + mergedFile = klibMergeDir.resolve(klibDumpFileName) + groupTargetNames = extension.klib.useTargetGroupAliases + } + + fun Project.bannedTargets(): Set { + val prop = project.properties[BANNED_TARGETS_PROPERTY_NAME] as String? + prop ?: return emptySet() + return prop.split(",").map { it.trim() }.toSet().also { + if (it.isNotEmpty()) { + project.logger.warn( + "WARNING: Following property is not empty: $BANNED_TARGETS_PROPERTY_NAME. " + + "If you're don't know what it means, please make sure that its value is empty." + ) + } + } + } + + fun Project.configureTargets( + klibApiDir: Provider, + mergeTask: TaskProvider, + mergeFakeTask: TaskProvider + ) { + val kotlin = project.kotlinMultiplatform + + val supportedTargets = supportedTargets() + kotlin.targets.matching { it.emitsKlib }.configureEach { currentTarget -> + val mainCompilations = currentTarget.mainCompilations + if (mainCompilations.none()) { + return@configureEach + } + + val targetName = currentTarget.targetName + val targetConfig = TargetConfig(project, targetName, intermediateFilesConfig) + val apiBuildDir = targetConfig.apiDir.map { project.buildDir.resolve(it) }.get() + + val targetSupported = targetName in supportedTargets.get() + if (targetSupported) { + mainCompilations.all { + val buildTargetAbi = configureKlibCompilation( + it, extension, targetConfig, + apiBuildDir + ) + mergeTask.configure { + it.addInput(targetName, apiBuildDir) + it.dependsOn(buildTargetAbi) + } + mergeFakeTask.configure { + it.addInput(targetName, apiBuildDir) + it.dependsOn(buildTargetAbi) + } + } + return@configureEach + } + + val unsupportedTargetStub = mergeDependencyForUnsupportedTarget(targetConfig) + mergeTask.configure { + it.dependsOn(unsupportedTargetStub) + } + val proxy = unsupportedTargetDumpProxy(klibApiDir, targetConfig, apiBuildDir, supportedTargets.get()) + mergeFakeTask.configure { + it.addInput(targetName, apiBuildDir) + it.dependsOn(proxy) + } + } + mergeTask.configure { + it.doFirst { + if (supportedTargets.get().isEmpty()) { + throw IllegalStateException( + "KLib ABI dump/validation requires at least enabled klib target, but none were found." + ) + } + } + } + } + + private fun Project.supportedTargets(): Provider> { + val banned = bannedTargets() + return project.provider { + val hm = HostManager() + project.kotlinMultiplatform.targets.matching { it.emitsKlib } + .asSequence() + .filter { + if (it is KotlinNativeTarget) { + hm.isEnabled(it.konanTarget) && it.targetName !in banned + } else { + true + } + } + .map { + it.targetName + }.toSet() + } + } + + + private fun Project.configureKlibCompilation( + compilation: KotlinCompilation, + extension: ApiValidationExtension, + targetConfig: TargetConfig, + apiBuildDir: File + ): TaskProvider { + val projectName = project.name + val buildTask = project.task(targetConfig.apiTaskName("Build")) { + target = targetConfig.targetName!! + // Do not enable task for empty umbrella modules + isEnabled = + klibAbiCheckEnabled( + projectName, + extension + ) && compilation.allKotlinSourceSets.any { it.kotlin.srcDirs.any { it.exists() } } + // 'group' is not specified deliberately, so it will be hidden from ./gradlew tasks + description = "Builds Kotlin KLib ABI for 'main' compilations of $projectName. " + + "Complementary task and shouldn't be called manually" + klibFile = project.files(project.provider { compilation.output.classesDirs }) + compilationDependencies = project.files(project.provider { compilation.compileDependencyFiles }) + signatureVersion = extension.klib.signatureVersion + outputApiFile = apiBuildDir.resolve(klibDumpFileName) + } + return buildTask + } + + private fun Project.mergeDependencyForUnsupportedTarget(targetConfig: TargetConfig): TaskProvider { + return project.task(targetConfig.apiTaskName("Build")) { + isEnabled = apiCheckEnabled(project.name, extension) + + doLast { + logger.warn( + "Target ${targetConfig.targetName} is not supported by the host compiler and the " + + "KLib ABI dump could not be generated for it." + ) + } + } + } + + private fun Project.unsupportedTargetDumpProxy( + klibApiDir: Provider, + targetConfig: TargetConfig, apiBuildDir: File, + supportedTargets: Set + ): TaskProvider { + val targetName = targetConfig.targetName!! + return project.task(targetConfig.apiTaskName("InferAbiDump")) { + isEnabled = klibAbiCheckEnabled(project.name, extension) + description = "Try to replace the dump for unsupported target $targetName with the dump " + + "generated for one of the supported targets." + group = "other" + this.supportedTargets = supportedTargets + inputImageFile = klibApiDir.get().resolve(klibDumpFileName) + outputApiDir = apiBuildDir.toString() + outputFile = apiBuildDir.resolve(klibDumpFileName) + unsupportedTarget = targetConfig.targetName + dumpFileName = klibDumpFileName + dependsOn(project.tasks.withType(KotlinKlibAbiBuildTask::class.java)) + } + } +} + +private val KotlinTarget.emitsKlib: Boolean + get() { + val platformType = this.platformType + return platformType == KotlinPlatformType.native || + platformType == KotlinPlatformType.wasm || + platformType == KotlinPlatformType.js + } + +private val KotlinTarget.jvmBased: Boolean + get() { + val platformType = this.platformType + return platformType == KotlinPlatformType.jvm || platformType == KotlinPlatformType.androidJvm + } + +private val Project.kotlinMultiplatform + get() = extensions.getByName("kotlin") as KotlinMultiplatformExtension + +private val KotlinTarget.mainCompilations + get() = compilations.matching { it.name == "main" } + +private val Project.jvmDumpFileName: String + get() = "$name.api" +private val Project.klibDumpFileName: String + get() = "$name.klib.api" diff --git a/src/main/kotlin/BuildTaskBase.kt b/src/main/kotlin/BuildTaskBase.kt new file mode 100644 index 00000000..b726a45c --- /dev/null +++ b/src/main/kotlin/BuildTaskBase.kt @@ -0,0 +1,58 @@ +/* + * Copyright 2016-2023 JetBrains s.r.o. + * Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file. + */ + +package kotlinx.validation + +import org.gradle.api.DefaultTask +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.Internal +import org.gradle.api.tasks.OutputFile +import java.io.File + +abstract class BuildTaskBase : DefaultTask() { + private val extension = project.apiValidationExtensionOrNull + + @OutputFile + lateinit var outputApiFile: File + + private var _ignoredPackages: Set? = null + @get:Input + var ignoredPackages : Set + get() = _ignoredPackages ?: extension?.ignoredPackages ?: emptySet() + set(value) { _ignoredPackages = value } + + private var _nonPublicMarkes: Set? = null + @get:Input + var nonPublicMarkers : Set + get() = _nonPublicMarkes ?: extension?.nonPublicMarkers ?: emptySet() + set(value) { _nonPublicMarkes = value } + + private var _ignoredClasses: Set? = null + @get:Input + var ignoredClasses : Set + get() = _ignoredClasses ?: extension?.ignoredClasses ?: emptySet() + set(value) { _ignoredClasses = value } + + private var _publicPackages: Set? = null + @get:Input + var publicPackages: Set + get() = _publicPackages ?: extension?.publicPackages ?: emptySet() + set(value) { _publicPackages = value } + + private var _publicMarkers: Set? = null + @get:Input + var publicMarkers: Set + get() = _publicMarkers ?: extension?.publicMarkers ?: emptySet() + set(value) { _publicMarkers = value} + + private var _publicClasses: Set? = null + @get:Input + var publicClasses: Set + get() = _publicClasses ?: extension?.publicClasses ?: emptySet() + set(value) { _publicClasses = value } + + @get:Internal + internal val projectName = project.name +} diff --git a/src/main/kotlin/KotlinApiBuildTask.kt b/src/main/kotlin/KotlinApiBuildTask.kt index 0df744bb..04f8deb8 100644 --- a/src/main/kotlin/KotlinApiBuildTask.kt +++ b/src/main/kotlin/KotlinApiBuildTask.kt @@ -9,14 +9,11 @@ import kotlinx.validation.api.* import org.gradle.api.* import org.gradle.api.file.* import org.gradle.api.tasks.* -import java.io.* import java.util.jar.JarFile import javax.inject.Inject public open class KotlinApiBuildTask @Inject constructor( -) : DefaultTask() { - - private val extension = project.apiValidationExtensionOrNull +) : BuildTaskBase() { @InputFiles @Optional @@ -32,52 +29,10 @@ public open class KotlinApiBuildTask @Inject constructor( @PathSensitive(PathSensitivity.RELATIVE) public lateinit var inputDependencies: FileCollection - @OutputDirectory - public lateinit var outputApiDir: File - - private var _ignoredPackages: Set? = null - @get:Input - internal var ignoredPackages : Set - get() = _ignoredPackages ?: extension?.ignoredPackages ?: emptySet() - set(value) { _ignoredPackages = value } - - private var _nonPublicMarkes: Set? = null - @get:Input - internal var nonPublicMarkers : Set - get() = _nonPublicMarkes ?: extension?.nonPublicMarkers ?: emptySet() - set(value) { _nonPublicMarkes = value } - - private var _ignoredClasses: Set? = null - @get:Input - internal var ignoredClasses : Set - get() = _ignoredClasses ?: extension?.ignoredClasses ?: emptySet() - set(value) { _ignoredClasses = value } - - private var _publicPackages: Set? = null - @get:Input - internal var publicPackages: Set - get() = _publicPackages ?: extension?.publicPackages ?: emptySet() - set(value) { _publicPackages = value } - - private var _publicMarkers: Set? = null - @get:Input - internal var publicMarkers: Set - get() = _publicMarkers ?: extension?.publicMarkers ?: emptySet() - set(value) { _publicMarkers = value} - - private var _publicClasses: Set? = null - @get:Input - internal var publicClasses: Set - get() = _publicClasses ?: extension?.publicClasses ?: emptySet() - set(value) { _publicClasses = value } - - @get:Internal - internal val projectName = project.name - @TaskAction internal fun generate() { - cleanup(outputApiDir) - outputApiDir.mkdirs() + outputApiFile.delete() + outputApiFile.parentFile.mkdirs() val inputClassesDirs = inputClassesDirs val signatures = when { @@ -104,7 +59,7 @@ public open class KotlinApiBuildTask @Inject constructor( .filterOutNonPublic(ignoredPackages + ignoredPackagesNames, ignoredClasses) .filterOutAnnotated(nonPublicMarkers.map(::replaceDots).toSet()) - outputApiDir.resolve("$projectName.api").bufferedWriter().use { writer -> + outputApiFile.bufferedWriter().use { writer -> filteredSignatures .sortedBy { it.name } .forEach { api -> @@ -116,17 +71,5 @@ public open class KotlinApiBuildTask @Inject constructor( } } } - - private fun cleanup(file: File) { - if (file.exists()) { - val listing = file.listFiles() - if (listing != null) { - for (sub in listing) { - cleanup(sub) - } - } - file.delete() - } - } } diff --git a/src/main/kotlin/KotlinApiCompareTask.kt b/src/main/kotlin/KotlinApiCompareTask.kt index ac4eed55..bf7c6d19 100644 --- a/src/main/kotlin/KotlinApiCompareTask.kt +++ b/src/main/kotlin/KotlinApiCompareTask.kt @@ -7,6 +7,7 @@ package kotlinx.validation import com.github.difflib.DiffUtils import com.github.difflib.UnifiedDiffUtils +import difflib.DiffUtils import java.io.* import java.util.TreeMap import javax.inject.Inject @@ -17,40 +18,12 @@ import org.gradle.api.tasks.* public open class KotlinApiCompareTask @Inject constructor(private val objects: ObjectFactory): DefaultTask() { - /* - * Nullability and optionality is a workaround for - * https://github.com/gradle/gradle/issues/2016 - * - * Unfortunately, there is no way to skip validation apart from setting 'null' - */ - @Optional - @InputDirectory + @InputFiles @PathSensitive(PathSensitivity.RELATIVE) - public var projectApiDir: File? = null - - // Used for diagnostic error message when projectApiDir doesn't exist - @Input - @Optional - public var nonExistingProjectApiDir: String? = null - - internal fun compareApiDumps(apiReferenceDir: File, apiBuildDir: File) { - if (apiReferenceDir.exists()) { - projectApiDir = apiReferenceDir - } else { - projectApiDir = null - nonExistingProjectApiDir = apiReferenceDir.toString() - } - this.apiBuildDir = apiBuildDir - } - - @InputDirectory - @PathSensitive(PathSensitivity.RELATIVE) - public lateinit var apiBuildDir: File + public lateinit var projectApiFile: File - @OutputFile - @Optional - @Suppress("unused") - public val dummyOutputFile: File? = null + @InputFiles + public lateinit var generatedApiFile: File private val projectName = project.name @@ -58,10 +31,15 @@ public open class KotlinApiCompareTask @Inject constructor(private val objects: @TaskAction internal fun verify() { - val projectApiDir = projectApiDir - ?: error("Expected folder with API declarations '$nonExistingProjectApiDir' does not exist.\n" + + val projectApiDir = projectApiFile.parentFile + if (!projectApiDir.exists()) { + error("Expected folder with API declarations '$projectApiDir' does not exist.\n" + "Please ensure that ':apiDump' was executed in order to get API dump to compare the build against") - + } + val buildApiDir = generatedApiFile.parentFile + if (!buildApiDir.exists()) { + error("Expected folder with generate API declarations '$buildApiDir' does not exist.") + } val subject = projectName /* @@ -72,35 +50,34 @@ public open class KotlinApiCompareTask @Inject constructor(private val objects: * To workaround that, we replace paths we are looking for the same paths that * actually exist on FS. */ - fun caseInsensitiveMap() = TreeMap { rp, rp2 -> - rp.toString().compareTo(rp2.toString(), true) + fun caseInsensitiveMap() = TreeMap { rp, rp2 -> + rp.compareTo(rp2, true) } val apiBuildDirFiles = caseInsensitiveMap() val expectedApiFiles = caseInsensitiveMap() - objects.fileTree().from(apiBuildDir).visit { file -> - apiBuildDirFiles[file.relativePath] = file.relativePath + objects.fileTree().from(buildApiDir).visit { file -> + apiBuildDirFiles[file.name] = file.relativePath } objects.fileTree().from(projectApiDir).visit { file -> - expectedApiFiles[file.relativePath] = file.relativePath - } - - if (apiBuildDirFiles.size != 1) { - error("Expected a single file $subject.api, but found: $expectedApiFiles") + expectedApiFiles[file.name] = file.relativePath } - var expectedApiDeclaration = apiBuildDirFiles.keys.single() - if (expectedApiDeclaration !in expectedApiFiles) { - error("File ${expectedApiDeclaration.lastName} is missing from ${projectApiDir.relativeDirPath()}, please run " + + if (!expectedApiFiles.containsKey(projectApiFile.name)) { + error("File ${projectApiFile.name} is missing from ${projectApiDir.relativeDirPath()}, please run " + ":$subject:apiDump task to generate one") } + if (!apiBuildDirFiles.containsKey(generatedApiFile.name)) { + error("File ${generatedApiFile.name} is missing from dump results.") + } + // Normalize case-sensitivity - expectedApiDeclaration = expectedApiFiles.getValue(expectedApiDeclaration) - val actualApiDeclaration = apiBuildDirFiles.getValue(expectedApiDeclaration) + val expectedApiDeclaration = expectedApiFiles.getValue(projectApiFile.name) + val actualApiDeclaration = apiBuildDirFiles.getValue(generatedApiFile.name) val diffSet = mutableSetOf() val expectedFile = expectedApiDeclaration.getFile(projectApiDir) - val actualFile = actualApiDeclaration.getFile(apiBuildDir) + val actualFile = actualApiDeclaration.getFile(buildApiDir) val diff = compareFiles(expectedFile, actualFile) if (diff != null) diffSet.add(diff) if (diffSet.isNotEmpty()) { diff --git a/src/main/kotlin/KotlinKlibAbiBuildTask.kt b/src/main/kotlin/KotlinKlibAbiBuildTask.kt new file mode 100644 index 00000000..102891d0 --- /dev/null +++ b/src/main/kotlin/KotlinKlibAbiBuildTask.kt @@ -0,0 +1,97 @@ +/* + * Copyright 2016-2023 JetBrains s.r.o. + * Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file. + */ + +package kotlinx.validation + +import org.gradle.api.file.FileCollection +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.InputFiles +import org.gradle.api.tasks.Optional +import org.gradle.api.tasks.TaskAction +import org.jetbrains.kotlin.library.abi.* + +/** + * Generates a text file with a ABI dump for a single klib. + */ +abstract class KotlinKlibAbiBuildTask : BuildTaskBase() { + + @InputFiles + lateinit var klibFile: FileCollection + + @InputFiles + lateinit var compilationDependencies: FileCollection + + @Optional + @get:Input + var signatureVersion: Int? = null + + @Input + lateinit var target: String + + @ExperimentalStdlibApi + @ExperimentalLibraryAbiReader + @TaskAction + fun generate() { + outputApiFile.delete() + outputApiFile.parentFile.mkdirs() + + val filters = buildList { + if (ignoredPackages.isNotEmpty()) { + add(AbiReadingFilter.ExcludedPackages(ignoredPackages.map { AbiCompoundName(it) })) + } + if (ignoredClasses.isNotEmpty()) { + add(AbiReadingFilter.ExcludedClasses(ignoredClasses.flatMap { + generateQualifiedNames(it) + })) + } + if (nonPublicMarkers.isNotEmpty()) { + add(AbiReadingFilter.NonPublicMarkerAnnotations(nonPublicMarkers.flatMap { + generateQualifiedNames(it) + })) + } + } + + val parsedAbi = try { + LibraryAbiReader.readAbiInfo(klibFile.singleFile, filters) + } catch (e: Exception) { + throw IllegalStateException("Can't read a KLib: ${klibFile.singleFile}", e) + } + + val supportedVersions = parsedAbi.signatureVersions.asSequence() + val sigVersion = if (signatureVersion != null) { + val versionNumbers = supportedVersions.map { it.versionNumber }.toSortedSet() + if (signatureVersion !in versionNumbers) { + throw IllegalArgumentException( + "Unsupported KLib signature version '$signatureVersion'. " + + "Supported versions are: $versionNumbers" + ) + } + AbiSignatureVersion.resolveByVersionNumber(signatureVersion!!) + } else { + supportedVersions.maxByOrNull(AbiSignatureVersion::versionNumber) + ?: throw IllegalStateException("Can't choose abiSignatureVersion") + } + + outputApiFile.bufferedWriter().use { + LibraryAbiRenderer.render(parsedAbi, it, AbiRenderingSettings(sigVersion)) + } + } +} + +@ExperimentalStdlibApi +@ExperimentalLibraryAbiReader +internal fun generateQualifiedNames(name: String): List { + if (!name.contains('.')) { + return listOf(AbiQualifiedName(AbiCompoundName(""), AbiCompoundName(name))) + } + val parts = name.split('.') + return buildList { + for (packageLength in parts.indices) { + val packageName = AbiCompoundName(parts.subList(0, packageLength).joinToString(".")) + val className = AbiCompoundName(parts.subList(packageLength, parts.size).joinToString(".")) + add(AbiQualifiedName(packageName, className)) + } + } +} diff --git a/src/main/kotlin/KotlinKlibExtractSupportedTargetsAbiTask.kt b/src/main/kotlin/KotlinKlibExtractSupportedTargetsAbiTask.kt new file mode 100644 index 00000000..6b661ea1 --- /dev/null +++ b/src/main/kotlin/KotlinKlibExtractSupportedTargetsAbiTask.kt @@ -0,0 +1,63 @@ +/* + * Copyright 2016-2024 JetBrains s.r.o. + * Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file. + */ + +package kotlinx.validation + +import kotlinx.validation.klib.KlibAbiDumpFormat +import kotlinx.validation.klib.KlibAbiDumpMerger +import kotlinx.validation.klib.TargetHierarchy +import org.gradle.api.DefaultTask +import org.gradle.api.provider.Provider +import org.gradle.api.tasks.* +import java.io.File + +/** + * Extracts dump for targets supported by the host compiler from a merged API dump stored in a project. + */ +abstract class KotlinKlibExtractSupportedTargetsAbiTask : DefaultTask() { + @get:Internal + internal val projectName = project.name + + @InputFiles + lateinit var inputAbiFile: File + + @OutputFile + lateinit var outputAbiFile: File + + @get:Input + lateinit var targets: Provider> + + @Input + var strictValidation: Boolean = false + + @Input + var groupTargetNames: Boolean = true + + @TaskAction + fun generate() { + if (inputAbiFile.length() == 0L) { + error("Project ABI file $inputAbiFile is empty.") + } + val dump = KlibAbiDumpMerger().apply { loadMergedDump(inputAbiFile) } + val enabledTargets = targets.get() + val targetsToRemove = dump.targets.filter { it.name !in enabledTargets } + if (targetsToRemove.isNotEmpty() && strictValidation) { + throw IllegalStateException( + "Validation could not be performed as some targets are not available " + + "and the strictValidation mode was enabled" + ) + } + for (target in targetsToRemove) { + dump.remove(target) + } + outputAbiFile.bufferedWriter().use { dump.dump(it, KlibAbiDumpFormat(useGroupAliases = canUseGroupAliases())) } + } + + private fun canUseGroupAliases(): Boolean { + if (!groupTargetNames) return false + val clashingTargets = targets.get().intersect(TargetHierarchy.nonLeafTargets()) + return clashingTargets.isEmpty() + } +} diff --git a/src/main/kotlin/KotlinKlibInferAbiForUnsupportedTargetTask.kt b/src/main/kotlin/KotlinKlibInferAbiForUnsupportedTargetTask.kt new file mode 100644 index 00000000..543dafee --- /dev/null +++ b/src/main/kotlin/KotlinKlibInferAbiForUnsupportedTargetTask.kt @@ -0,0 +1,113 @@ +/* + * Copyright 2016-2023 JetBrains s.r.o. + * Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file. + */ + +package kotlinx.validation + +import kotlinx.validation.klib.KlibAbiDumpFormat +import kotlinx.validation.klib.KlibAbiDumpMerger +import kotlinx.validation.klib.Target +import kotlinx.validation.klib.TargetHierarchy +import org.gradle.api.DefaultTask +import org.gradle.api.tasks.* +import org.jetbrains.kotlin.utils.keysToMap +import java.io.File + +/** + * Task infers a possible KLib ABI dump for an unsupported target. + * To infer a dump, tasks walks up the default targets hierarchy tree starting from the unsupported + * target until it finds a node corresponding to a group of targets having at least one supported target. + * After that, dumps generated for such supported targets are merged and declarations that are common to all + * of them are considered as a common ABI that most likely will be shared by the unsupported target. + * At the next step, if a project contains an old dump, declarations specific to the unsupported target are copied + * from it and merged into the common ABI extracted previously. + * The resulting dump is then used as an inferred dump for the unsupported target. + */ +abstract class KotlinKlibInferAbiForUnsupportedTargetTask : DefaultTask() { + @get:Internal + internal val projectName = project.name + + @Input + lateinit var unsupportedTarget: String + + @InputFiles + lateinit var outputApiDir: String + + @Input + lateinit var supportedTargets: Set + + @InputFiles + lateinit var inputImageFile: File + + @Input + lateinit var dumpFileName: String + + @OutputFile + lateinit var outputFile: File + + @TaskAction + fun generate() { + // find a set of supported targets that are closer to unsupported target in the hierarchy + val matchingTargets = findMatchingTargets() + val target2outFile = supportedTargets.keysToMap { + File(outputApiDir).parentFile.resolve(it).resolve(dumpFileName) + } + + // given a set of similar targets, combine their ABI files into a single merged dump and consider it + // a common ABI that should be shared by the unsupported target as well + val commonDump = KlibAbiDumpMerger() + for (target in matchingTargets) { + commonDump.addIndividualDump(Target(target), target2outFile[target]!!) + } + commonDump.retainCommonAbi() + + // load and old dump (that may contain the dump for the unsupported target) and remove all but the declarations + // specific to the unsupported target + val image = KlibAbiDumpMerger() + if (inputImageFile.exists()) { + if (inputImageFile.length() > 0L) { + image.loadMergedDump(inputImageFile) + image.retainTargetSpecificAbi(Target(unsupportedTarget)) + // merge common ABI with target-specific ABI + commonDump.mergeTargetSpecific(image) + } else { + logger.warn( + "Project's ABI file exists, but empty: $inputImageFile. " + + "The file will be ignored during ABI dump inference for the unsupported target " + + unsupportedTarget + ) + } + } + commonDump.overrideTargets(setOf(Target(unsupportedTarget))) + + outputFile.bufferedWriter().use { + commonDump.dump(it, KlibAbiDumpFormat(includeTargets = false)) + } + + logger.warn( + "An ABI dump for target $unsupportedTarget was inferred from the ABI generated for target " + + "[${matchingTargets.joinToString(",")}] " + + "as the former target is not supported by the host compiler. " + + "Inferred dump may not reflect actual ABI for the target $unsupportedTarget. " + + "It is recommended to regenerate the dump on the host supporting all required compilation target." + ) + } + + private fun findMatchingTargets(): Set { + var currentGroup: String? = unsupportedTarget + while (currentGroup != null) { + // If a current group has some supported targets, use them. + val groupTargets = TargetHierarchy.targets(currentGroup).intersect(supportedTargets) + if (groupTargets.isNotEmpty()) { + return groupTargets + } + // Otherwise, walk up the target hierarchy. + currentGroup = TargetHierarchy.parent(currentGroup) + } + throw IllegalStateException( + "The target $unsupportedTarget is not supported by the host compiler " + + "and there are no targets similar to $unsupportedTarget to infer a dump from it." + ) + } +} diff --git a/src/main/kotlin/KotlinKlibMergeAbiTask.kt b/src/main/kotlin/KotlinKlibMergeAbiTask.kt new file mode 100644 index 00000000..dbc48a62 --- /dev/null +++ b/src/main/kotlin/KotlinKlibMergeAbiTask.kt @@ -0,0 +1,57 @@ +/* + * Copyright 2016-2023 JetBrains s.r.o. + * Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file. + */ + +package kotlinx.validation + +import kotlinx.validation.klib.KlibAbiDumpFormat +import kotlinx.validation.klib.KlibAbiDumpMerger +import kotlinx.validation.klib.Target +import kotlinx.validation.klib.TargetHierarchy +import org.gradle.api.DefaultTask +import org.gradle.api.tasks.* +import java.io.File + +/** + * Merges multiple individual KLib ABI dumps into a single merged dump. + */ +abstract class KotlinKlibMergeAbiTask : DefaultTask() { + private val targetToFile = mutableMapOf() + + @get:Internal + internal val projectName = project.name + + @get:Input + val targets: Set + get() = targetToFile.keys + + @OutputFile + lateinit var mergedFile: File + + @Input + lateinit var dumpFileName: String + + @Input + var groupTargetNames: Boolean = true + + fun addInput(target: String, file: File) { + targetToFile[target] = file + } + + @TaskAction + fun merge() { + val builder = KlibAbiDumpMerger() + targets.forEach { targetName -> + val target = Target(targetName) + builder.addIndividualDump(target, targetToFile[targetName]!!.resolve(dumpFileName)) + } + mergedFile.bufferedWriter().use { builder.dump(it, KlibAbiDumpFormat(useGroupAliases = canUseGroupAliases())) } + } + + private fun canUseGroupAliases(): Boolean { + if (!groupTargetNames) return false + val clashingTargets = targets.intersect(TargetHierarchy.nonLeafTargets()) + return clashingTargets.isEmpty() + } +} diff --git a/src/test/kotlin/tests/AbiTest.kt b/src/test/kotlin/tests/AbiTest.kt new file mode 100644 index 00000000..aadacf1b --- /dev/null +++ b/src/test/kotlin/tests/AbiTest.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2016-2023 JetBrains s.r.o. + * Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file. + */ + +package kotlinx.validation.api.tests + +import kotlinx.validation.generateQualifiedNames +import org.jetbrains.kotlin.library.abi.* +import org.junit.Test +import kotlin.test.assertEquals + +class AbiTest { + @OptIn(ExperimentalLibraryAbiReader::class, ExperimentalStdlibApi::class) + @Test + fun generateAbiNames() { + assertEquals( + listOf( + AbiQualifiedName(AbiCompoundName(""), AbiCompoundName("foo.bar.Baz")), + AbiQualifiedName(AbiCompoundName("foo"), AbiCompoundName("bar.Baz")), + AbiQualifiedName(AbiCompoundName("foo.bar"), AbiCompoundName("Baz")) + ), + generateQualifiedNames("foo.bar.Baz") + ) + assertEquals( + listOf(AbiQualifiedName(AbiCompoundName(""), AbiCompoundName("Class"))), + generateQualifiedNames("Class") + ) + } +} From cb87220344379eb71e773c1bbd472e8026dc578b Mon Sep 17 00:00:00 2001 From: Filipp Zhinkin Date: Wed, 31 Jan 2024 12:47:21 +0100 Subject: [PATCH 06/53] Rebased to current dev --- api/binary-compatibility-validator.api | 119 ++++++++- build.gradle.kts | 3 +- gradle/libs.versions.toml | 1 + .../validation/api/BaseKotlinGradleTest.kt | 5 +- .../validation/test/KLibVerificationTests.kt | 235 +++++++++--------- src/main/kotlin/ApiValidationExtension.kt | 4 +- .../BinaryCompatibilityValidatorPlugin.kt | 24 +- src/main/kotlin/BuildTaskBase.kt | 16 +- src/main/kotlin/KotlinApiCompareTask.kt | 1 - src/main/kotlin/KotlinKlibAbiBuildTask.kt | 12 +- ...otlinKlibExtractSupportedTargetsAbiTask.kt | 14 +- ...linKlibInferAbiForUnsupportedTargetTask.kt | 16 +- src/main/kotlin/KotlinKlibMergeAbiTask.kt | 14 +- 13 files changed, 280 insertions(+), 184 deletions(-) diff --git a/api/binary-compatibility-validator.api b/api/binary-compatibility-validator.api index f226963e..4dd07406 100644 --- a/api/binary-compatibility-validator.api +++ b/api/binary-compatibility-validator.api @@ -5,11 +5,13 @@ public class kotlinx/validation/ApiValidationExtension { public final fun getIgnoredClasses ()Ljava/util/Set; public final fun getIgnoredPackages ()Ljava/util/Set; public final fun getIgnoredProjects ()Ljava/util/Set; + public final fun getKlib ()Lkotlinx/validation/KlibValidationSettings; public final fun getNonPublicMarkers ()Ljava/util/Set; public final fun getPublicClasses ()Ljava/util/Set; public final fun getPublicMarkers ()Ljava/util/Set; public final fun getPublicPackages ()Ljava/util/Set; public final fun getValidationDisabled ()Z + public final fun klib (Lkotlin/jvm/functions/Function1;)V public final fun setAdditionalSourceSets (Ljava/util/Set;)V public final fun setApiDumpDirectory (Ljava/lang/String;)V public final fun setIgnoredClasses (Ljava/util/Set;)V @@ -28,32 +30,125 @@ public final class kotlinx/validation/BinaryCompatibilityValidatorPlugin : org/g public fun apply (Lorg/gradle/api/Project;)V } +public abstract class kotlinx/validation/BuildTaskBase : org/gradle/api/DefaultTask { + public field outputApiFile Ljava/io/File; + public fun ()V + public final fun getIgnoredClasses ()Ljava/util/Set; + public final fun getIgnoredPackages ()Ljava/util/Set; + public final fun getNonPublicMarkers ()Ljava/util/Set; + public final fun getOutputApiFile ()Ljava/io/File; + public final fun getPublicClasses ()Ljava/util/Set; + public final fun getPublicMarkers ()Ljava/util/Set; + public final fun getPublicPackages ()Ljava/util/Set; + public final fun setIgnoredClasses (Ljava/util/Set;)V + public final fun setIgnoredPackages (Ljava/util/Set;)V + public final fun setNonPublicMarkers (Ljava/util/Set;)V + public final fun setOutputApiFile (Ljava/io/File;)V + public final fun setPublicClasses (Ljava/util/Set;)V + public final fun setPublicMarkers (Ljava/util/Set;)V + public final fun setPublicPackages (Ljava/util/Set;)V +} + public abstract interface annotation class kotlinx/validation/ExternalApi : java/lang/annotation/Annotation { } -public class kotlinx/validation/KotlinApiBuildTask : org/gradle/api/DefaultTask { +public class kotlinx/validation/KlibValidationSettings { + public fun ()V + public final fun getEnabled ()Z + public final fun getSignatureVersion ()I + public final fun getStrictValidation ()Z + public final fun getUseTargetGroupAliases ()Z + public final fun setEnabled (Z)V + public final fun setSignatureVersion (I)V + public final fun setStrictValidation (Z)V + public final fun setUseTargetGroupAliases (Z)V +} + +public class kotlinx/validation/KotlinApiBuildTask : kotlinx/validation/BuildTaskBase { public field inputDependencies Lorg/gradle/api/file/FileCollection; - public field outputApiDir Ljava/io/File; public fun ()V public final fun getInputClassesDirs ()Lorg/gradle/api/file/FileCollection; public final fun getInputDependencies ()Lorg/gradle/api/file/FileCollection; public final fun getInputJar ()Lorg/gradle/api/file/RegularFileProperty; - public final fun getOutputApiDir ()Ljava/io/File; public final fun setInputClassesDirs (Lorg/gradle/api/file/FileCollection;)V public final fun setInputDependencies (Lorg/gradle/api/file/FileCollection;)V - public final fun setOutputApiDir (Ljava/io/File;)V } public class kotlinx/validation/KotlinApiCompareTask : org/gradle/api/DefaultTask { - public field apiBuildDir Ljava/io/File; + public field generatedApiFile Ljava/io/File; + public field projectApiFile Ljava/io/File; public fun (Lorg/gradle/api/model/ObjectFactory;)V - public final fun getApiBuildDir ()Ljava/io/File; - public final fun getDummyOutputFile ()Ljava/io/File; - public final fun getNonExistingProjectApiDir ()Ljava/lang/String; - public final fun getProjectApiDir ()Ljava/io/File; - public final fun setApiBuildDir (Ljava/io/File;)V - public final fun setNonExistingProjectApiDir (Ljava/lang/String;)V - public final fun setProjectApiDir (Ljava/io/File;)V + public final fun getGeneratedApiFile ()Ljava/io/File; + public final fun getProjectApiFile ()Ljava/io/File; + public final fun setGeneratedApiFile (Ljava/io/File;)V + public final fun setProjectApiFile (Ljava/io/File;)V +} + +public abstract class kotlinx/validation/KotlinKlibAbiBuildTask : kotlinx/validation/BuildTaskBase { + public field compilationDependencies Lorg/gradle/api/file/FileCollection; + public field klibFile Lorg/gradle/api/file/FileCollection; + public field target Ljava/lang/String; + public fun ()V + public final fun getCompilationDependencies ()Lorg/gradle/api/file/FileCollection; + public final fun getKlibFile ()Lorg/gradle/api/file/FileCollection; + public final fun getSignatureVersion ()Ljava/lang/Integer; + public final fun getTarget ()Ljava/lang/String; + public final fun setCompilationDependencies (Lorg/gradle/api/file/FileCollection;)V + public final fun setKlibFile (Lorg/gradle/api/file/FileCollection;)V + public final fun setSignatureVersion (Ljava/lang/Integer;)V + public final fun setTarget (Ljava/lang/String;)V +} + +public abstract class kotlinx/validation/KotlinKlibExtractSupportedTargetsAbiTask : org/gradle/api/DefaultTask { + public field inputAbiFile Ljava/io/File; + public field outputAbiFile Ljava/io/File; + public field targets Lorg/gradle/api/provider/Provider; + public fun ()V + public final fun getGroupTargetNames ()Z + public final fun getInputAbiFile ()Ljava/io/File; + public final fun getOutputAbiFile ()Ljava/io/File; + public final fun getStrictValidation ()Z + public final fun getTargets ()Lorg/gradle/api/provider/Provider; + public final fun setGroupTargetNames (Z)V + public final fun setInputAbiFile (Ljava/io/File;)V + public final fun setOutputAbiFile (Ljava/io/File;)V + public final fun setStrictValidation (Z)V + public final fun setTargets (Lorg/gradle/api/provider/Provider;)V +} + +public abstract class kotlinx/validation/KotlinKlibInferAbiForUnsupportedTargetTask : org/gradle/api/DefaultTask { + public field dumpFileName Ljava/lang/String; + public field inputImageFile Ljava/io/File; + public field outputApiDir Ljava/lang/String; + public field outputFile Ljava/io/File; + public field supportedTargets Ljava/util/Set; + public field unsupportedTarget Ljava/lang/String; + public fun ()V + public final fun getDumpFileName ()Ljava/lang/String; + public final fun getInputImageFile ()Ljava/io/File; + public final fun getOutputApiDir ()Ljava/lang/String; + public final fun getOutputFile ()Ljava/io/File; + public final fun getSupportedTargets ()Ljava/util/Set; + public final fun getUnsupportedTarget ()Ljava/lang/String; + public final fun setDumpFileName (Ljava/lang/String;)V + public final fun setInputImageFile (Ljava/io/File;)V + public final fun setOutputApiDir (Ljava/lang/String;)V + public final fun setOutputFile (Ljava/io/File;)V + public final fun setSupportedTargets (Ljava/util/Set;)V + public final fun setUnsupportedTarget (Ljava/lang/String;)V +} + +public abstract class kotlinx/validation/KotlinKlibMergeAbiTask : org/gradle/api/DefaultTask { + public field dumpFileName Ljava/lang/String; + public field mergedFile Ljava/io/File; + public fun ()V + public final fun getDumpFileName ()Ljava/lang/String; + public final fun getGroupTargetNames ()Z + public final fun getMergedFile ()Ljava/io/File; + public final fun getTargets ()Ljava/util/Set; + public final fun setDumpFileName (Ljava/lang/String;)V + public final fun setGroupTargetNames (Z)V + public final fun setMergedFile (Ljava/io/File;)V } public final class kotlinx/validation/api/ClassBinarySignature { diff --git a/build.gradle.kts b/build.gradle.kts index d2f10b02..94e6c1f4 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -62,7 +62,7 @@ val createClasspathManifest = tasks.register("createClasspathManifest") { dependencies { implementation(gradleApi()) implementation(libs.kotlinx.metadata) - compileOnly("org.jetbrains.kotlin:kotlin-compiler-embeddable") + compileOnly(libs.kotlin.compiler.embeddable) implementation(libs.ow2.asm) implementation(libs.ow2.asmTree) implementation(libs.javaDiffUtils) @@ -163,6 +163,7 @@ testing { implementation(project()) implementation(libs.assertJ.core) implementation(libs.kotlin.test) + implementation(libs.kotlin.compiler.embeddable) } } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index bb028625..1d8e80ee 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -30,6 +30,7 @@ junit-jupiter = { module = "org.junit.jupiter:junit-jupiter", version.ref = "jun assertJ-core = { module = "org.assertj:assertj-core", version = "3.18.1" } kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" } +kotlin-compiler-embeddable = { module = "org.jetbrains.kotlin:kotlin-compiler-embeddable", version.ref = "kotlin" } ## endregion diff --git a/src/functionalTest/kotlin/kotlinx/validation/api/BaseKotlinGradleTest.kt b/src/functionalTest/kotlin/kotlinx/validation/api/BaseKotlinGradleTest.kt index 3709dcb4..02db79a0 100644 --- a/src/functionalTest/kotlin/kotlinx/validation/api/BaseKotlinGradleTest.kt +++ b/src/functionalTest/kotlin/kotlinx/validation/api/BaseKotlinGradleTest.kt @@ -5,12 +5,13 @@ package kotlinx.validation.api -import kotlinx.validation.API_DIR -import kotlinx.validation.KLIB_PHONY_TARGET_NAME import org.junit.Rule import org.junit.rules.TemporaryFolder import java.io.File +internal const val KLIB_PHONY_TARGET_NAME = "klib" +internal const val KLIB_ALL_PHONY_TARGET_NAME = "klib-all" + public open class BaseKotlinGradleTest { @Rule @JvmField diff --git a/src/functionalTest/kotlin/kotlinx/validation/test/KLibVerificationTests.kt b/src/functionalTest/kotlin/kotlinx/validation/test/KLibVerificationTests.kt index c88591ad..69768986 100644 --- a/src/functionalTest/kotlin/kotlinx/validation/test/KLibVerificationTests.kt +++ b/src/functionalTest/kotlin/kotlinx/validation/test/KLibVerificationTests.kt @@ -5,9 +5,6 @@ package kotlinx.validation.test -import kotlinx.validation.API_DIR -import kotlinx.validation.BANNED_TARGETS_PROPERTY_NAME -import kotlinx.validation.KLIB_PHONY_TARGET_NAME import kotlinx.validation.api.* import kotlinx.validation.api.buildGradleKts import kotlinx.validation.api.resolve @@ -20,6 +17,8 @@ import org.junit.Assume import org.junit.Test import kotlin.test.assertTrue +internal const val BANNED_TARGETS_PROPERTY_NAME = "binary.compatibility.validator.klib.targets.blacklist.for.testing" + private fun KLibVerificationTests.checkKlibDump(buildResult: BuildResult, expectedDumpFileName: String, projectName: String = "testproject", dumpTask: String = ":apiDump", @@ -43,37 +42,37 @@ internal class KLibVerificationTests : BaseKotlinGradleTest() { fun `apiDump for native targets`() { val runner = test { settingsGradleKts { - resolve("examples/gradle/settings/settings-name-testproject.gradle.kts") + resolve("/examples/gradle/settings/settings-name-testproject.gradle.kts") } buildGradleKts { - resolve("examples/gradle/base/withNativePlugin.gradle.kts") + resolve("/examples/gradle/base/withNativePlugin.gradle.kts") } kotlin("TopLevelDeclarations.kt", "commonMain") { - resolve("examples/classes/TopLevelDeclarations.kt") + resolve("/examples/classes/TopLevelDeclarations.kt") } runner { arguments.add(":apiDump") } } - checkKlibDump(runner.build(), "examples/classes/TopLevelDeclarations.klib.with.linux.dump") + checkKlibDump(runner.build(), "/examples/classes/TopLevelDeclarations.klib.with.linux.dump") } @Test fun `apiCheck for native targets`() { val runner = test { settingsGradleKts { - resolve("examples/gradle/settings/settings-name-testproject.gradle.kts") + resolve("/examples/gradle/settings/settings-name-testproject.gradle.kts") } buildGradleKts { - resolve("examples/gradle/base/withNativePlugin.gradle.kts") + resolve("/examples/gradle/base/withNativePlugin.gradle.kts") } kotlin("TopLevelDeclarations.kt", "commonMain") { - resolve("examples/classes/TopLevelDeclarations.kt") + resolve("/examples/classes/TopLevelDeclarations.kt") } abiFile(projectName = "testproject") { - resolve("examples/classes/TopLevelDeclarations.klib.dump") + resolve("/examples/classes/TopLevelDeclarations.klib.dump") } runner { @@ -90,17 +89,17 @@ internal class KLibVerificationTests : BaseKotlinGradleTest() { fun `apiCheck for native targets should fail when a class is not in a dump`() { val runner = test { settingsGradleKts { - resolve("examples/gradle/settings/settings-name-testproject.gradle.kts") + resolve("/examples/gradle/settings/settings-name-testproject.gradle.kts") } buildGradleKts { - resolve("examples/gradle/base/withNativePlugin.gradle.kts") + resolve("/examples/gradle/base/withNativePlugin.gradle.kts") } kotlin("BuildConfig.kt", "commonMain") { - resolve("examples/classes/BuildConfig.kt") + resolve("/examples/classes/BuildConfig.kt") } abiFile(projectName = "testproject") { - resolve("examples/classes/Empty.klib.dump") + resolve("/examples/classes/Empty.klib.dump") } runner { @@ -123,16 +122,16 @@ internal class KLibVerificationTests : BaseKotlinGradleTest() { fun `apiDump should include target-specific sources`() { val runner = test { settingsGradleKts { - resolve("examples/gradle/settings/settings-name-testproject.gradle.kts") + resolve("/examples/gradle/settings/settings-name-testproject.gradle.kts") } buildGradleKts { - resolve("examples/gradle/base/withNativePlugin.gradle.kts") + resolve("/examples/gradle/base/withNativePlugin.gradle.kts") } kotlin("AnotherBuildConfig.kt", "commonMain") { - resolve("examples/classes/AnotherBuildConfig.kt") + resolve("/examples/classes/AnotherBuildConfig.kt") } kotlin("AnotherBuildConfigLinuxArm64.kt", "linuxArm64Main") { - resolve("examples/classes/AnotherBuildConfigLinuxArm64.kt") + resolve("/examples/classes/AnotherBuildConfigLinuxArm64.kt") } runner { @@ -147,7 +146,7 @@ internal class KLibVerificationTests : BaseKotlinGradleTest() { val dump = rootProjectAbiDump("testproject") assertTrue(dump.exists(), "Dump does not exist") - val expectedDump = readFileList("examples/classes/AnotherBuildConfigLinuxArm64Extra.klib.dump") + val expectedDump = readFileList("/examples/classes/AnotherBuildConfigLinuxArm64Extra.klib.dump") Assertions.assertThat(dump.readText()).isEqualToIgnoringNewLines(expectedDump) } } @@ -156,14 +155,14 @@ internal class KLibVerificationTests : BaseKotlinGradleTest() { fun `apiDump with native targets along with JVM target`() { val runner = test { settingsGradleKts { - resolve("examples/gradle/settings/settings-name-testproject.gradle.kts") + resolve("/examples/gradle/settings/settings-name-testproject.gradle.kts") } buildGradleKts { - resolve("examples/gradle/base/withNativePlugin.gradle.kts") - resolve("examples/gradle/base/enableJvmInWithNativePlugin.gradle.kts") + resolve("/examples/gradle/base/withNativePlugin.gradle.kts") + resolve("/examples/gradle/base/enableJvmInWithNativePlugin.gradle.kts") } kotlin("AnotherBuildConfig.kt", "commonMain") { - resolve("examples/classes/AnotherBuildConfig.kt") + resolve("/examples/classes/AnotherBuildConfig.kt") } runner { arguments.add(":apiDump") @@ -171,13 +170,13 @@ internal class KLibVerificationTests : BaseKotlinGradleTest() { } runner.build().apply { - checkKlibDump(this, "examples/classes/AnotherBuildConfig.klib.dump", + checkKlibDump(this, "/examples/classes/AnotherBuildConfig.klib.dump", projectWithMultipleDumps = false) val jvmApiDump = rootProjectDir.resolve("$API_DIR/testproject.api") assertTrue(jvmApiDump.exists(), "No API dump for JVM") - val jvmExpected = readFileList("examples/classes/AnotherBuildConfig.dump") + val jvmExpected = readFileList("/examples/classes/AnotherBuildConfig.dump") Assertions.assertThat(jvmApiDump.readText()).isEqualToIgnoringNewLines(jvmExpected) } } @@ -186,17 +185,17 @@ internal class KLibVerificationTests : BaseKotlinGradleTest() { fun `apiDump should ignore a class listed in ignoredClasses`() { val runner = test { settingsGradleKts { - resolve("examples/gradle/settings/settings-name-testproject.gradle.kts") + resolve("/examples/gradle/settings/settings-name-testproject.gradle.kts") } buildGradleKts { - resolve("examples/gradle/base/withNativePlugin.gradle.kts") - resolve("examples/gradle/configuration/ignoredClasses/oneValidFullyQualifiedClass.gradle.kts") + resolve("/examples/gradle/base/withNativePlugin.gradle.kts") + resolve("/examples/gradle/configuration/ignoredClasses/oneValidFullyQualifiedClass.gradle.kts") } kotlin("BuildConfig.kt", "commonMain") { - resolve("examples/classes/BuildConfig.kt") + resolve("/examples/classes/BuildConfig.kt") } kotlin("AnotherBuildConfig.kt", "commonMain") { - resolve("examples/classes/AnotherBuildConfig.kt") + resolve("/examples/classes/AnotherBuildConfig.kt") } runner { @@ -204,21 +203,21 @@ internal class KLibVerificationTests : BaseKotlinGradleTest() { } } - checkKlibDump(runner.build(), "examples/classes/AnotherBuildConfig.klib.dump") + checkKlibDump(runner.build(), "/examples/classes/AnotherBuildConfig.klib.dump") } @Test fun `apiDump should succeed if a class listed in ignoredClasses is not found`() { val runner = test { settingsGradleKts { - resolve("examples/gradle/settings/settings-name-testproject.gradle.kts") + resolve("/examples/gradle/settings/settings-name-testproject.gradle.kts") } buildGradleKts { - resolve("examples/gradle/base/withNativePlugin.gradle.kts") - resolve("examples/gradle/configuration/ignoredClasses/oneValidFullyQualifiedClass.gradle.kts") + resolve("/examples/gradle/base/withNativePlugin.gradle.kts") + resolve("/examples/gradle/configuration/ignoredClasses/oneValidFullyQualifiedClass.gradle.kts") } kotlin("AnotherBuildConfig.kt", "commonMain") { - resolve("examples/classes/AnotherBuildConfig.kt") + resolve("/examples/classes/AnotherBuildConfig.kt") } runner { @@ -226,27 +225,27 @@ internal class KLibVerificationTests : BaseKotlinGradleTest() { } } - checkKlibDump(runner.build(), "examples/classes/AnotherBuildConfig.klib.dump") + checkKlibDump(runner.build(), "/examples/classes/AnotherBuildConfig.klib.dump") } @Test fun `apiDump should ignore all entities from a package listed in ingoredPackages`() { val runner = test { settingsGradleKts { - resolve("examples/gradle/settings/settings-name-testproject.gradle.kts") + resolve("/examples/gradle/settings/settings-name-testproject.gradle.kts") } buildGradleKts { - resolve("examples/gradle/base/withNativePlugin.gradle.kts") - resolve("examples/gradle/configuration/ignoredPackages/oneValidPackage.gradle.kts") + resolve("/examples/gradle/base/withNativePlugin.gradle.kts") + resolve("/examples/gradle/configuration/ignoredPackages/oneValidPackage.gradle.kts") } kotlin("BuildConfig.kt", "commonMain") { - resolve("examples/classes/BuildConfig.kt") + resolve("/examples/classes/BuildConfig.kt") } kotlin("AnotherBuildConfig.kt", "commonMain") { - resolve("examples/classes/AnotherBuildConfig.kt") + resolve("/examples/classes/AnotherBuildConfig.kt") } kotlin("SubPackage.kt", "commonMain") { - resolve("examples/classes/SubPackage.kt") + resolve("/examples/classes/SubPackage.kt") } runner { @@ -254,24 +253,24 @@ internal class KLibVerificationTests : BaseKotlinGradleTest() { } } - checkKlibDump(runner.build(), "examples/classes/AnotherBuildConfig.klib.dump") + checkKlibDump(runner.build(), "/examples/classes/AnotherBuildConfig.klib.dump") } @Test fun `apiDump should ignore all entities annotated with non-public markers`() { val runner = test { settingsGradleKts { - resolve("examples/gradle/settings/settings-name-testproject.gradle.kts") + resolve("/examples/gradle/settings/settings-name-testproject.gradle.kts") } buildGradleKts { - resolve("examples/gradle/base/withNativePlugin.gradle.kts") - resolve("examples/gradle/configuration/nonPublicMarkers/klib.gradle.kts") + resolve("/examples/gradle/base/withNativePlugin.gradle.kts") + resolve("/examples/gradle/configuration/nonPublicMarkers/klib.gradle.kts") } kotlin("HiddenDeclarations.kt", "commonMain") { - resolve("examples/classes/HiddenDeclarations.kt") + resolve("/examples/classes/HiddenDeclarations.kt") } kotlin("NonPublicMarkers.kt", "commonMain") { - resolve("examples/classes/NonPublicMarkers.kt") + resolve("/examples/classes/NonPublicMarkers.kt") } runner { @@ -279,21 +278,21 @@ internal class KLibVerificationTests : BaseKotlinGradleTest() { } } - checkKlibDump(runner.build(), "examples/classes/HiddenDeclarations.klib.dump") + checkKlibDump(runner.build(), "/examples/classes/HiddenDeclarations.klib.dump") } @Test fun `apiDump should not dump subclasses excluded via ignoredClasses`() { val runner = test { settingsGradleKts { - resolve("examples/gradle/settings/settings-name-testproject.gradle.kts") + resolve("/examples/gradle/settings/settings-name-testproject.gradle.kts") } buildGradleKts { - resolve("examples/gradle/base/withNativePlugin.gradle.kts") - resolve("examples/gradle/configuration/ignoreSubclasses/ignore.gradle.kts") + resolve("/examples/gradle/base/withNativePlugin.gradle.kts") + resolve("/examples/gradle/configuration/ignoreSubclasses/ignore.gradle.kts") } kotlin("Subclasses.kt", "commonMain") { - resolve("examples/classes/Subclasses.kt") + resolve("/examples/classes/Subclasses.kt") } runner { @@ -301,25 +300,25 @@ internal class KLibVerificationTests : BaseKotlinGradleTest() { } } - checkKlibDump(runner.build(), "examples/classes/Subclasses.klib.dump") + checkKlibDump(runner.build(), "/examples/classes/Subclasses.klib.dump") } @Test fun `apiCheck for native targets using v1 signatures`() { val runner = test { settingsGradleKts { - resolve("examples/gradle/settings/settings-name-testproject.gradle.kts") + resolve("/examples/gradle/settings/settings-name-testproject.gradle.kts") } buildGradleKts { - resolve("examples/gradle/base/withNativePlugin.gradle.kts") - resolve("examples/gradle/configuration/signatures/v1.gradle.kts") + resolve("/examples/gradle/base/withNativePlugin.gradle.kts") + resolve("/examples/gradle/configuration/signatures/v1.gradle.kts") } kotlin("TopLevelDeclarations.kt", "commonMain") { - resolve("examples/classes/TopLevelDeclarations.kt") + resolve("/examples/classes/TopLevelDeclarations.kt") } abiFile(projectName = "testproject") { - resolve("examples/classes/TopLevelDeclarations.klib.v1.dump") + resolve("/examples/classes/TopLevelDeclarations.klib.v1.dump") } runner { @@ -336,14 +335,14 @@ internal class KLibVerificationTests : BaseKotlinGradleTest() { fun `apiDump for native targets should fail when using invalid signature version`() { val runner = test { settingsGradleKts { - resolve("examples/gradle/settings/settings-name-testproject.gradle.kts") + resolve("/examples/gradle/settings/settings-name-testproject.gradle.kts") } buildGradleKts { - resolve("examples/gradle/base/withNativePlugin.gradle.kts") - resolve("examples/gradle/configuration/signatures/invalid.gradle.kts") + resolve("/examples/gradle/base/withNativePlugin.gradle.kts") + resolve("/examples/gradle/configuration/signatures/invalid.gradle.kts") } kotlin("TopLevelDeclarations.kt", "commonMain") { - resolve("examples/classes/TopLevelDeclarations.kt") + resolve("/examples/classes/TopLevelDeclarations.kt") } runner { @@ -359,21 +358,21 @@ internal class KLibVerificationTests : BaseKotlinGradleTest() { Assume.assumeTrue(HostManager().isEnabled(KonanTarget.MACOS_ARM64)) val runner = test { settingsGradleKts { - resolve("examples/gradle/settings/settings-name-testproject.gradle.kts") + resolve("/examples/gradle/settings/settings-name-testproject.gradle.kts") } buildGradleKts { - resolve("examples/gradle/base/withNativePlugin.gradle.kts") - resolve("examples/gradle/configuration/appleTargets/targets.gradle.kts") + resolve("/examples/gradle/base/withNativePlugin.gradle.kts") + resolve("/examples/gradle/configuration/appleTargets/targets.gradle.kts") } kotlin("TopLevelDeclarations.kt", "commonMain") { - resolve("examples/classes/TopLevelDeclarations.kt") + resolve("/examples/classes/TopLevelDeclarations.kt") } runner { arguments.add(":apiDump") } } - checkKlibDump(runner.build(), "examples/classes/TopLevelDeclarations.klib.all.dump") + checkKlibDump(runner.build(), "/examples/classes/TopLevelDeclarations.klib.all.dump") } @Test @@ -381,17 +380,17 @@ internal class KLibVerificationTests : BaseKotlinGradleTest() { Assume.assumeTrue(HostManager().isEnabled(KonanTarget.MACOS_ARM64)) val runner = test { settingsGradleKts { - resolve("examples/gradle/settings/settings-name-testproject.gradle.kts") + resolve("/examples/gradle/settings/settings-name-testproject.gradle.kts") } buildGradleKts { - resolve("examples/gradle/base/withNativePlugin.gradle.kts") - resolve("examples/gradle/configuration/appleTargets/targets.gradle.kts") + resolve("/examples/gradle/base/withNativePlugin.gradle.kts") + resolve("/examples/gradle/configuration/appleTargets/targets.gradle.kts") } kotlin("TopLevelDeclarations.kt", "commonMain") { - resolve("examples/classes/TopLevelDeclarations.kt") + resolve("/examples/classes/TopLevelDeclarations.kt") } abiFile(projectName = "testproject") { - resolve("examples/classes/TopLevelDeclarations.klib.all.dump") + resolve("/examples/classes/TopLevelDeclarations.klib.all.dump") } runner { arguments.add(":apiCheck") @@ -407,13 +406,13 @@ internal class KLibVerificationTests : BaseKotlinGradleTest() { fun `apiCheck should fail if a target is not supported`() { val runner = test { settingsGradleKts { - resolve("examples/gradle/settings/settings-name-testproject.gradle.kts") + resolve("/examples/gradle/settings/settings-name-testproject.gradle.kts") } buildGradleKts { - resolve("examples/gradle/base/withNativePlugin.gradle.kts") + resolve("/examples/gradle/base/withNativePlugin.gradle.kts") } kotlin("TopLevelDeclarations.kt", "commonMain") { - resolve("examples/classes/TopLevelDeclarations.kt") + resolve("/examples/classes/TopLevelDeclarations.kt") } abiFile(projectName = "testproject", target = KLIB_PHONY_TARGET_NAME) { } @@ -430,17 +429,17 @@ internal class KLibVerificationTests : BaseKotlinGradleTest() { fun `apiCheck should ignore unsupported targets by default`() { val runner = test { settingsGradleKts { - resolve("examples/gradle/settings/settings-name-testproject.gradle.kts") + resolve("/examples/gradle/settings/settings-name-testproject.gradle.kts") } buildGradleKts { - resolve("examples/gradle/base/withNativePlugin.gradle.kts") + resolve("/examples/gradle/base/withNativePlugin.gradle.kts") } kotlin("TopLevelDeclarations.kt", "commonMain") { - resolve("examples/classes/TopLevelDeclarations.kt") + resolve("/examples/classes/TopLevelDeclarations.kt") } abiFile(projectName = "testproject") { // note that the regular dump is used, where linuxArm64 is presented - resolve("examples/classes/TopLevelDeclarations.klib.dump") + resolve("/examples/classes/TopLevelDeclarations.klib.dump") } runner { arguments.add("-P$BANNED_TARGETS_PROPERTY_NAME=linuxArm64") @@ -457,18 +456,18 @@ internal class KLibVerificationTests : BaseKotlinGradleTest() { fun `apiCheck should fail for unsupported targets with strict mode turned on`() { val runner = test { settingsGradleKts { - resolve("examples/gradle/settings/settings-name-testproject.gradle.kts") + resolve("/examples/gradle/settings/settings-name-testproject.gradle.kts") } buildGradleKts { - resolve("examples/gradle/base/withNativePlugin.gradle.kts") - resolve("examples/gradle/configuration/unsupported/enforce.gradle.kts") + resolve("/examples/gradle/base/withNativePlugin.gradle.kts") + resolve("/examples/gradle/configuration/unsupported/enforce.gradle.kts") } kotlin("TopLevelDeclarations.kt", "commonMain") { - resolve("examples/classes/TopLevelDeclarations.kt") + resolve("/examples/classes/TopLevelDeclarations.kt") } abiFile(projectName = "testproject") { // note that the regular dump is used, where linuxArm64 is presented - resolve("examples/classes/TopLevelDeclarations.klib.dump") + resolve("/examples/classes/TopLevelDeclarations.klib.dump") } runner { arguments.add("-P$BANNED_TARGETS_PROPERTY_NAME=linuxArm64") @@ -485,16 +484,16 @@ internal class KLibVerificationTests : BaseKotlinGradleTest() { fun `klibDump should infer a dump for unsupported target from similar enough target`() { val runner = test { settingsGradleKts { - resolve("examples/gradle/settings/settings-name-testproject.gradle.kts") + resolve("/examples/gradle/settings/settings-name-testproject.gradle.kts") } buildGradleKts { - resolve("examples/gradle/base/withNativePlugin.gradle.kts") + resolve("/examples/gradle/base/withNativePlugin.gradle.kts") } kotlin("TopLevelDeclarations.kt", "commonMain") { - resolve("examples/classes/TopLevelDeclarations.kt") + resolve("/examples/classes/TopLevelDeclarations.kt") } kotlin("AnotherBuildConfigLinuxArm64.kt", "linuxArm64Main") { - resolve("examples/classes/AnotherBuildConfigLinuxArm64.kt") + resolve("/examples/classes/AnotherBuildConfigLinuxArm64.kt") } runner { arguments.add("-P$BANNED_TARGETS_PROPERTY_NAME=linuxArm64") @@ -502,7 +501,7 @@ internal class KLibVerificationTests : BaseKotlinGradleTest() { } } - checkKlibDump(runner.build(), "examples/classes/TopLevelDeclarations.klib.with.linux.dump", + checkKlibDump(runner.build(), "/examples/classes/TopLevelDeclarations.klib.with.linux.dump", dumpTask = ":klibApiDump") } @@ -510,16 +509,16 @@ internal class KLibVerificationTests : BaseKotlinGradleTest() { fun `klibDump should fail when the only target in the project is disabled`() { val runner = test { settingsGradleKts { - resolve("examples/gradle/settings/settings-name-testproject.gradle.kts") + resolve("/examples/gradle/settings/settings-name-testproject.gradle.kts") } buildGradleKts { - resolve("examples/gradle/base/withNativePluginAndSingleTarget.gradle.kts") + resolve("/examples/gradle/base/withNativePluginAndSingleTarget.gradle.kts") } kotlin("TopLevelDeclarations.kt", "commonMain") { - resolve("examples/classes/TopLevelDeclarations.kt") + resolve("/examples/classes/TopLevelDeclarations.kt") } kotlin("AnotherBuildConfigLinuxArm64.kt", "linuxArm64Main") { - resolve("examples/classes/AnotherBuildConfigLinuxArm64.kt") + resolve("/examples/classes/AnotherBuildConfigLinuxArm64.kt") } runner { arguments.add("-P$BANNED_TARGETS_PROPERTY_NAME=linuxArm64") @@ -538,13 +537,13 @@ internal class KLibVerificationTests : BaseKotlinGradleTest() { fun `klibDump if all klib-targets are unavailable`() { val runner = test { settingsGradleKts { - resolve("examples/gradle/settings/settings-name-testproject.gradle.kts") + resolve("/examples/gradle/settings/settings-name-testproject.gradle.kts") } buildGradleKts { - resolve("examples/gradle/base/withNativePlugin.gradle.kts") + resolve("/examples/gradle/base/withNativePlugin.gradle.kts") } kotlin("TopLevelDeclarations.kt", "commonMain") { - resolve("examples/classes/TopLevelDeclarations.kt") + resolve("/examples/classes/TopLevelDeclarations.kt") } runner { arguments.add("-P$BANNED_TARGETS_PROPERTY_NAME=linuxArm64,linuxX64,mingwX64," + @@ -564,17 +563,17 @@ internal class KLibVerificationTests : BaseKotlinGradleTest() { fun `klibCheck if all klib-targets are unavailable`() { val runner = test { settingsGradleKts { - resolve("examples/gradle/settings/settings-name-testproject.gradle.kts") + resolve("/examples/gradle/settings/settings-name-testproject.gradle.kts") } buildGradleKts { - resolve("examples/gradle/base/withNativePlugin.gradle.kts") + resolve("/examples/gradle/base/withNativePlugin.gradle.kts") } kotlin("TopLevelDeclarations.kt", "commonMain") { - resolve("examples/classes/TopLevelDeclarations.kt") + resolve("/examples/classes/TopLevelDeclarations.kt") } abiFile(projectName = "testproject") { // note that the regular dump is used, where linuxArm64 is presented - resolve("examples/classes/TopLevelDeclarations.klib.dump") + resolve("/examples/classes/TopLevelDeclarations.klib.dump") } runner { arguments.add("-P$BANNED_TARGETS_PROPERTY_NAME=linuxArm64,linuxX64,mingwX64," + @@ -594,21 +593,21 @@ internal class KLibVerificationTests : BaseKotlinGradleTest() { fun `target name grouping should be disabled on group name clash`() { val runner = test { settingsGradleKts { - resolve("examples/gradle/settings/settings-name-testproject.gradle.kts") + resolve("/examples/gradle/settings/settings-name-testproject.gradle.kts") } buildGradleKts { - resolve("examples/gradle/base/withNativePluginAndNoTargets.gradle.kts") - resolve("examples/gradle/configuration/grouping/clashingTargetNames.gradle.kts") + resolve("/examples/gradle/base/withNativePluginAndNoTargets.gradle.kts") + resolve("/examples/gradle/configuration/grouping/clashingTargetNames.gradle.kts") } kotlin("AnotherBuildConfig.kt", "commonMain") { - resolve("examples/classes/AnotherBuildConfig.kt") + resolve("/examples/classes/AnotherBuildConfig.kt") } runner { arguments.add(":klibApiDump") } } - checkKlibDump(runner.build(), "examples/classes/AnotherBuildConfig.klib.clash.dump", + checkKlibDump(runner.build(), "/examples/classes/AnotherBuildConfig.klib.clash.dump", dumpTask = ":klibApiDump") } @@ -616,21 +615,21 @@ internal class KLibVerificationTests : BaseKotlinGradleTest() { fun `target name grouping with custom target names`() { val runner = test { settingsGradleKts { - resolve("examples/gradle/settings/settings-name-testproject.gradle.kts") + resolve("/examples/gradle/settings/settings-name-testproject.gradle.kts") } buildGradleKts { - resolve("examples/gradle/base/withNativePluginAndNoTargets.gradle.kts") - resolve("examples/gradle/configuration/grouping/customTargetNames.gradle.kts") + resolve("/examples/gradle/base/withNativePluginAndNoTargets.gradle.kts") + resolve("/examples/gradle/configuration/grouping/customTargetNames.gradle.kts") } kotlin("AnotherBuildConfig.kt", "commonMain") { - resolve("examples/classes/AnotherBuildConfig.kt") + resolve("/examples/classes/AnotherBuildConfig.kt") } runner { arguments.add(":klibApiDump") } } - checkKlibDump(runner.build(), "examples/classes/AnotherBuildConfig.klib.custom.dump", + checkKlibDump(runner.build(), "/examples/classes/AnotherBuildConfig.klib.custom.dump", dumpTask = ":klibApiDump") } @@ -638,26 +637,26 @@ internal class KLibVerificationTests : BaseKotlinGradleTest() { fun `target name grouping`() { val runner = test { settingsGradleKts { - resolve("examples/gradle/settings/settings-name-testproject.gradle.kts") + resolve("/examples/gradle/settings/settings-name-testproject.gradle.kts") } buildGradleKts { - resolve("examples/gradle/base/withNativePlugin.gradle.kts") + resolve("/examples/gradle/base/withNativePlugin.gradle.kts") } kotlin("AnotherBuildConfig.kt", "commonMain") { - resolve("examples/classes/AnotherBuildConfig.kt") + resolve("/examples/classes/AnotherBuildConfig.kt") } kotlin("AnotherBuildConfigLinuxArm64.kt", "linuxArm64Main") { - resolve("examples/classes/AnotherBuildConfigLinuxArm64.kt") + resolve("/examples/classes/AnotherBuildConfigLinuxArm64.kt") } kotlin("AnotherBuildConfigLinuxX64.kt", "linuxX64Main") { - resolve("examples/classes/AnotherBuildConfigLinuxArm64.kt") + resolve("/examples/classes/AnotherBuildConfigLinuxArm64.kt") } runner { arguments.add(":klibApiDump") } } - checkKlibDump(runner.build(), "examples/classes/AnotherBuildConfigLinux.klib.grouping.dump", + checkKlibDump(runner.build(), "/examples/classes/AnotherBuildConfigLinux.klib.grouping.dump", dumpTask = ":klibApiDump") } } diff --git a/src/main/kotlin/ApiValidationExtension.kt b/src/main/kotlin/ApiValidationExtension.kt index f1dba2dd..47efbb4a 100644 --- a/src/main/kotlin/ApiValidationExtension.kt +++ b/src/main/kotlin/ApiValidationExtension.kt @@ -82,7 +82,7 @@ public open class ApiValidationExtension { /** * Configure KLib AVI validation settings. */ - public inline fun klib(block: KlibValidationSettings.() -> Unit) { + public fun klib(block: KlibValidationSettings.() -> Unit) { block(this.klib) } } @@ -90,7 +90,7 @@ public open class ApiValidationExtension { /** * Settings affecting KLib ABI validation. */ -open class KlibValidationSettings { +public open class KlibValidationSettings { /** * Enables KLib ABI validation checks. */ diff --git a/src/main/kotlin/BinaryCompatibilityValidatorPlugin.kt b/src/main/kotlin/BinaryCompatibilityValidatorPlugin.kt index 19443f86..c676dd8a 100644 --- a/src/main/kotlin/BinaryCompatibilityValidatorPlugin.kt +++ b/src/main/kotlin/BinaryCompatibilityValidatorPlugin.kt @@ -18,9 +18,8 @@ import org.jetbrains.kotlin.library.abi.LibraryAbiReader import java.io.* import kotlin.text.split -const val API_DIR = "api" -const val KLIB_PHONY_TARGET_NAME = "klib" -const val KLIB_ALL_PHONY_TARGET_NAME = "klib-all" +internal const val KLIB_PHONY_TARGET_NAME = "klib" +internal const val KLIB_ALL_PHONY_TARGET_NAME = "klib-all" public class BinaryCompatibilityValidatorPlugin : Plugin { @@ -256,7 +255,7 @@ internal val Project.apiValidationExtensionOrNull: ApiValidationExtension? private fun apiCheckEnabled(projectName: String, extension: ApiValidationExtension): Boolean = projectName !in extension.ignoredProjects && !extension.validationDisabled -fun klibAbiCheckEnabled(projectName: String, extension: ApiValidationExtension): Boolean = +private fun klibAbiCheckEnabled(projectName: String, extension: ApiValidationExtension): Boolean = projectName !in extension.ignoredProjects && !extension.validationDisabled && extension.klib.enabled private fun Project.configureApiTasks( @@ -332,7 +331,7 @@ private inline fun Project.task( noinline configuration: T.() -> Unit, ): TaskProvider = tasks.register(name, T::class.java, Action(configuration)) -const val BANNED_TARGETS_PROPERTY_NAME = "binary.compatibility.validator.klib.targets.blacklist.for.testing" +internal const val BANNED_TARGETS_PROPERTY_NAME = "binary.compatibility.validator.klib.targets.blacklist.for.testing" private class KlibValidationPipelineBuilder( val dirConfig: Provider?, @@ -344,16 +343,17 @@ private class KlibValidationPipelineBuilder( // In the intermediate phase of Klib dump generation there are always multiple targets, thus we need // target-based directory tree. intermediateFilesConfig = project.provider { DirConfig.TARGET_DIR } - val klibApiDirConfig = dirConfig?.map { TargetConfig(project, KLIB_PHONY_TARGET_NAME, dirConfig) } - val klibDumpConfig = TargetConfig(project, KLIB_PHONY_TARGET_NAME, intermediateFilesConfig) - val klibDumpAllConfig = TargetConfig(project, KLIB_ALL_PHONY_TARGET_NAME, intermediateFilesConfig) + val klibApiDirConfig = dirConfig?.map { TargetConfig(project, extension, KLIB_PHONY_TARGET_NAME, dirConfig) } + val klibDumpConfig = TargetConfig(project, extension, KLIB_PHONY_TARGET_NAME, intermediateFilesConfig) + val klibDumpAllConfig = TargetConfig(project, extension, KLIB_ALL_PHONY_TARGET_NAME, intermediateFilesConfig) val projectDir = project.projectDir val klibApiDir = klibApiDirConfig?.map { projectDir.resolve(it.apiDir.get()) }!! - val klibMergeDir = project.buildDir.resolve(klibDumpConfig.apiDir.get()) - val klibMergeAllDir = project.buildDir.resolve(klibDumpAllConfig.apiDir.get()) + val projectBuildDir = project.layout.buildDirectory.asFile.get() + val klibMergeDir = projectBuildDir.resolve(klibDumpConfig.apiDir.get()) + val klibMergeAllDir = projectBuildDir.resolve(klibDumpAllConfig.apiDir.get()) val klibExtractedFileDir = klibMergeAllDir.resolve("extracted") val klibMerge = project.mergeKlibsUmbrellaTask(klibDumpConfig, klibMergeDir) @@ -470,8 +470,8 @@ private class KlibValidationPipelineBuilder( } val targetName = currentTarget.targetName - val targetConfig = TargetConfig(project, targetName, intermediateFilesConfig) - val apiBuildDir = targetConfig.apiDir.map { project.buildDir.resolve(it) }.get() + val targetConfig = TargetConfig(project, extension, targetName, intermediateFilesConfig) + val apiBuildDir = targetConfig.apiDir.map { project.layout.buildDirectory.asFile.get().resolve(it) }.get() val targetSupported = targetName in supportedTargets.get() if (targetSupported) { diff --git a/src/main/kotlin/BuildTaskBase.kt b/src/main/kotlin/BuildTaskBase.kt index b726a45c..a042f2d8 100644 --- a/src/main/kotlin/BuildTaskBase.kt +++ b/src/main/kotlin/BuildTaskBase.kt @@ -11,45 +11,45 @@ import org.gradle.api.tasks.Internal import org.gradle.api.tasks.OutputFile import java.io.File -abstract class BuildTaskBase : DefaultTask() { +public abstract class BuildTaskBase : DefaultTask() { private val extension = project.apiValidationExtensionOrNull @OutputFile - lateinit var outputApiFile: File + public lateinit var outputApiFile: File private var _ignoredPackages: Set? = null @get:Input - var ignoredPackages : Set + public var ignoredPackages : Set get() = _ignoredPackages ?: extension?.ignoredPackages ?: emptySet() set(value) { _ignoredPackages = value } private var _nonPublicMarkes: Set? = null @get:Input - var nonPublicMarkers : Set + public var nonPublicMarkers : Set get() = _nonPublicMarkes ?: extension?.nonPublicMarkers ?: emptySet() set(value) { _nonPublicMarkes = value } private var _ignoredClasses: Set? = null @get:Input - var ignoredClasses : Set + public var ignoredClasses : Set get() = _ignoredClasses ?: extension?.ignoredClasses ?: emptySet() set(value) { _ignoredClasses = value } private var _publicPackages: Set? = null @get:Input - var publicPackages: Set + public var publicPackages: Set get() = _publicPackages ?: extension?.publicPackages ?: emptySet() set(value) { _publicPackages = value } private var _publicMarkers: Set? = null @get:Input - var publicMarkers: Set + public var publicMarkers: Set get() = _publicMarkers ?: extension?.publicMarkers ?: emptySet() set(value) { _publicMarkers = value} private var _publicClasses: Set? = null @get:Input - var publicClasses: Set + public var publicClasses: Set get() = _publicClasses ?: extension?.publicClasses ?: emptySet() set(value) { _publicClasses = value } diff --git a/src/main/kotlin/KotlinApiCompareTask.kt b/src/main/kotlin/KotlinApiCompareTask.kt index bf7c6d19..643d6d04 100644 --- a/src/main/kotlin/KotlinApiCompareTask.kt +++ b/src/main/kotlin/KotlinApiCompareTask.kt @@ -7,7 +7,6 @@ package kotlinx.validation import com.github.difflib.DiffUtils import com.github.difflib.UnifiedDiffUtils -import difflib.DiffUtils import java.io.* import java.util.TreeMap import javax.inject.Inject diff --git a/src/main/kotlin/KotlinKlibAbiBuildTask.kt b/src/main/kotlin/KotlinKlibAbiBuildTask.kt index 102891d0..36a44ca8 100644 --- a/src/main/kotlin/KotlinKlibAbiBuildTask.kt +++ b/src/main/kotlin/KotlinKlibAbiBuildTask.kt @@ -15,25 +15,25 @@ import org.jetbrains.kotlin.library.abi.* /** * Generates a text file with a ABI dump for a single klib. */ -abstract class KotlinKlibAbiBuildTask : BuildTaskBase() { +public abstract class KotlinKlibAbiBuildTask : BuildTaskBase() { @InputFiles - lateinit var klibFile: FileCollection + public lateinit var klibFile: FileCollection @InputFiles - lateinit var compilationDependencies: FileCollection + public lateinit var compilationDependencies: FileCollection @Optional @get:Input - var signatureVersion: Int? = null + public var signatureVersion: Int? = null @Input - lateinit var target: String + public lateinit var target: String @ExperimentalStdlibApi @ExperimentalLibraryAbiReader @TaskAction - fun generate() { + internal fun generate() { outputApiFile.delete() outputApiFile.parentFile.mkdirs() diff --git a/src/main/kotlin/KotlinKlibExtractSupportedTargetsAbiTask.kt b/src/main/kotlin/KotlinKlibExtractSupportedTargetsAbiTask.kt index 6b661ea1..12401f02 100644 --- a/src/main/kotlin/KotlinKlibExtractSupportedTargetsAbiTask.kt +++ b/src/main/kotlin/KotlinKlibExtractSupportedTargetsAbiTask.kt @@ -16,27 +16,27 @@ import java.io.File /** * Extracts dump for targets supported by the host compiler from a merged API dump stored in a project. */ -abstract class KotlinKlibExtractSupportedTargetsAbiTask : DefaultTask() { +public abstract class KotlinKlibExtractSupportedTargetsAbiTask : DefaultTask() { @get:Internal internal val projectName = project.name @InputFiles - lateinit var inputAbiFile: File + public lateinit var inputAbiFile: File @OutputFile - lateinit var outputAbiFile: File + public lateinit var outputAbiFile: File @get:Input - lateinit var targets: Provider> + public lateinit var targets: Provider> @Input - var strictValidation: Boolean = false + public var strictValidation: Boolean = false @Input - var groupTargetNames: Boolean = true + public var groupTargetNames: Boolean = true @TaskAction - fun generate() { + internal fun generate() { if (inputAbiFile.length() == 0L) { error("Project ABI file $inputAbiFile is empty.") } diff --git a/src/main/kotlin/KotlinKlibInferAbiForUnsupportedTargetTask.kt b/src/main/kotlin/KotlinKlibInferAbiForUnsupportedTargetTask.kt index 543dafee..977bd74f 100644 --- a/src/main/kotlin/KotlinKlibInferAbiForUnsupportedTargetTask.kt +++ b/src/main/kotlin/KotlinKlibInferAbiForUnsupportedTargetTask.kt @@ -24,30 +24,30 @@ import java.io.File * from it and merged into the common ABI extracted previously. * The resulting dump is then used as an inferred dump for the unsupported target. */ -abstract class KotlinKlibInferAbiForUnsupportedTargetTask : DefaultTask() { +public abstract class KotlinKlibInferAbiForUnsupportedTargetTask : DefaultTask() { @get:Internal internal val projectName = project.name @Input - lateinit var unsupportedTarget: String + public lateinit var unsupportedTarget: String @InputFiles - lateinit var outputApiDir: String + public lateinit var outputApiDir: String @Input - lateinit var supportedTargets: Set + public lateinit var supportedTargets: Set @InputFiles - lateinit var inputImageFile: File + public lateinit var inputImageFile: File @Input - lateinit var dumpFileName: String + public lateinit var dumpFileName: String @OutputFile - lateinit var outputFile: File + public lateinit var outputFile: File @TaskAction - fun generate() { + internal fun generate() { // find a set of supported targets that are closer to unsupported target in the hierarchy val matchingTargets = findMatchingTargets() val target2outFile = supportedTargets.keysToMap { diff --git a/src/main/kotlin/KotlinKlibMergeAbiTask.kt b/src/main/kotlin/KotlinKlibMergeAbiTask.kt index dbc48a62..cf616628 100644 --- a/src/main/kotlin/KotlinKlibMergeAbiTask.kt +++ b/src/main/kotlin/KotlinKlibMergeAbiTask.kt @@ -16,31 +16,31 @@ import java.io.File /** * Merges multiple individual KLib ABI dumps into a single merged dump. */ -abstract class KotlinKlibMergeAbiTask : DefaultTask() { +public abstract class KotlinKlibMergeAbiTask : DefaultTask() { private val targetToFile = mutableMapOf() @get:Internal internal val projectName = project.name @get:Input - val targets: Set + public val targets: Set get() = targetToFile.keys @OutputFile - lateinit var mergedFile: File + public lateinit var mergedFile: File @Input - lateinit var dumpFileName: String + public lateinit var dumpFileName: String @Input - var groupTargetNames: Boolean = true + public var groupTargetNames: Boolean = true - fun addInput(target: String, file: File) { + internal fun addInput(target: String, file: File) { targetToFile[target] = file } @TaskAction - fun merge() { + internal fun merge() { val builder = KlibAbiDumpMerger() targets.forEach { targetName -> val target = Target(targetName) From 91903af3aab4782ac59b1ff120860c7a4351a2b2 Mon Sep 17 00:00:00 2001 From: Filipp Zhinkin Date: Thu, 1 Feb 2024 10:06:19 +0100 Subject: [PATCH 07/53] Added a design doc --- docs/design/KLibSupport.md | 184 +++++++++++++++++++++++++++++++++++++ 1 file changed, 184 insertions(+) create mode 100644 docs/design/KLibSupport.md diff --git a/docs/design/KLibSupport.md b/docs/design/KLibSupport.md new file mode 100644 index 00000000..51d571c8 --- /dev/null +++ b/docs/design/KLibSupport.md @@ -0,0 +1,184 @@ +The document described assumptions that led to the current KLib ABI validation implementation. + +### Motivation and assumptions + +Just like JVM class files, Kotlin/Native libraries (a.k.a. KLibs) comes with a binary compatibility guarantees +allowing library developers evolve the library without a fear to break projects compiled against previous versions of +the library (but unlike the JVM, these guarantees are not yet finalized at the time this document was written). + +For the JVM, the Binary compatibility validator allows to check if some binary incompatible changes were made and +review what actually changed. For KLibs, there is no such a tool, and it seems reasonable to extend the BCV with KLib +validation support. + +There are several assumptions based on the experience of supporting +various multiplatform libraries that drive the KLib validation design: +* Multiplatform libraries usually have both JVM and native targets, so instead of introducing some different +unrelated tool/plugin it seems reasonable to extend the existing plugin and provide an experience similar to what +users have now for JVM libraries so that users can verify compatibility for both kinds of targets. +* BCV not only provides a way to verify public ABI changes, but it also allows to check how the public API surface +changed: developers could simply look at the dump file's history in SVC or review the change in a code-review system; +* Projects may have multiple JVM targets, but usually there is only a single target with a single dump file; +At the same time, multiplatform projects have a dozen of different native targets (like `linuxX64`, `iosArm64`, +`macosArm64`, to name a few), so there should be a way to manage dumps for all the targets. +* Usually, even if a project has multiple native targets, the public ABI exposed by corresponding klibs is either +the same or contains only a small number of differences. +* Not all targets could be compiled on every host: currently, the cross-compilation have some limitations (namely, +it's impossible to build Apple-targets on non macOs-host), and it's unlikely that someday it would be possible to build +Apple-specific sources (i.e., not just common sources that need to be compiled to some iosArm64-klib) +on non Apple-hosts. KLib validation requires a klib, so there should be a way to facilitate klib ABI dumping and +validation on something different from macOs (consider a multiplatform project where a developer adds a class +or a function to the common source set, it seems reasonable to expect that it could be done on any host). +* KLibs are emitted not only for native targets, but also for JS and Wasm targets. +* There are scenarios when a klib has to be compiled on a corresponding host platform (i.e., mingw target on Windows +and linux target on Linux), and also there are scenarios when using a Gradle plugin is not an option, +so there should be a way to use BCV for KLibs even in such scenarios. + +### Merged KLib dumps + +Assuming that the library's public ABI does not differ significantly between targets, it seems reasonable to merge +all the dumps into a single file where each declaration is annotated with the targets it belongs to. That will minimize +the number of new files to store in a project and will significantly simplify review of the changes. + +The KLib dump is a text format ([see examples in the Kotlin repo](https://github.com/JetBrains/kotlin/blob/master/compiler/util-klib-abi/ReadMe.md)). +For the merged dumps, we can simply extend it by adding special comments with a list of targets, +like `// Targets: [iosArm64, linuxX64, wasm]`. + +Such targets lists could be long and hard to read, so to simplify a process of reviewing changes in a dump we can +replace explicit target names with group aliases corresponding to groups of targets from the +[default hierarchy template](https://kotlinlang.org/docs/multiplatform-hierarchy.html#see-the-full-hierarchy-template). +Then, a long list of all native targets will become `[native]` and, for example, all Android-native targets will +become simply `[androidNative]`. +Of course, the merged dump file should include the mapping between an alias and actual target names, it could be placed +in a file's header. + +Here's a brief example of such a merged dump file: +``` +// Merged KLib ABI Dump +// Targets: [androidNativeArm32, androidNativeArm64, androidNativeX64, androidNativeX86, linuxArm64, linuxX64, mingwX64] +// Alias: androidNative => [androidNativeArm32, androidNativeArm64, androidNativeX64, androidNativeX86] +// Alias: linux => [linuxArm64, linuxX64] +// Rendering settings: +// - Signature version: 2 +// - Show manifest properties: false +// - Show declarations: true + +// Library unique name: +final class org.different.pack/BuildConfig { // org.different.pack/BuildConfig|null[0] + constructor () // org.different.pack/BuildConfig.|(){}[0] + final fun f1(): kotlin/Int // org.different.pack/BuildConfig.f1|f1(){}[0] + // Targets: [mingwX64] + final val p1 // org.different.pack/BuildConfig.p1|{}p1[0] + final fun (): kotlin/Int // org.different.pack/BuildConfig.p1.|(){}[0] +} +// Targets: [linux] +final fun (org.different.pack/BuildConfig).org.different.pack/linuxSpecific(): kotlin/Int // org.different.pack/linuxSpecific|linuxSpecific@org.different.pack.BuildConfig(){}[0] +``` + +The first line is just a header to check during the parsing. + +The next line (`// Targets: ...`) contains list of all targets for which dumps were merged. +It seems to be excessive to write all the targets each declaration belongs to as the majority +of declarations have the same targets as their parent declarations, and in the case of top level +declarations, until it is mentioned explicitly, the list of targets is the same as the file's target list. + +So, the class `BuildConfig` have the same targets as the whole file, but `linuxSpecific`-function +is presented only on `linux`-targets (`// Targets: [linux]`). + +The next two lines declare target aliases (`// Alias: => []`). There are only two +aliases (`linux` and `androidNative`), because it seems excessive to declare aliases for groups +consisting of only a single target (like `mingw`) as well as declaring aliases for targets +than will never be explicitly presented in a file (like an alias for all the targets, or all native targets). +We also can't add aliases for targets that does not exist, so there are no aliases like `macos` or `apple` +(in this example). +However, all other aliases are generated even if it is not explicitly mentioned (like `androidNative`) to +minimize dump's header changes in the future, when such groups could emerge. + +After that, a regular KLib ABI dump header continues, with an exception to some declarations being +annotated with `// Targets`. + +So, the dump could be interpreted as follows: +- the dump was merged from individual KLib ABI dumps generated for the following targets: +`androidNativeArm32`, `androidNativeArm64`, `androidNativeX64`, `androidNativeX86`, `linuxArm64`, `linuxX64`, +`mingwX64`; +- the class `BuildConfig`, its constructor and `f1`-functions are all exposed by klibs for all targets, +but its property `p1` (and the corresponding getter) is declared only for the `mingwX64`-target; +- an extension function `BuildConfig.linuxSpecific` is declared only for `linuxX64` and `linuxArm64` targets. + +### Working with dumps on hosts that does not support cross-compilation for all the targets + +If a host does not support cross-compilation for all the targets (it's a Linux or Windows host), then +there's no way to both dump and validate klibs for all targets configured for a project. + +When it comes to validation, the simplest approach seems to be ignoring unsupported targets (and printing +a warning about it) and validation the ABI only for targets supported by the host compiler. +To do that, corresponding klibs should be built first, and then their ABI should be dumped and merged. +After that, the merged dump stored in the repository should be filtered so that only the declarations for supported +targets are left. Finally, a newly generated dump could be compared with the filtered "golden" dump the same way +dumps are compared for the JVM. + +The things are a bit more complicated when it comes to updating the "golden" dump as if only dumps for supported targets +are merged, then the resulting dump file will cause validation failure on the host where all targets +are available (the dump won't contain declaration for Apple-targets, it won't even mention these targets, so when +the ABI validation takes a place on macOs-host, it'll fail). + +It seems like there are two ways to handle such a scenario: +- when updating a dump, assume that the ABI for unsupported targets remained the same and update only the ABI +for supported targets; +- try to guess (or infer) the ABI for unsupported targets by looking at the ABI dumped for supported targets. + +Both approaches have some shortcomings: +- with the first one, as long as the ABI changes, an updated dump will always be incorrect as it won't reflect +changes for unsupported targets; +- with the second approach, a "guessed" dump may be incorrect (of course, it depends on how we "guess"). + +By guessing or inferring a dump for unsupported targets, the following procedure is assumed: +- walk up the target hierarchy starting from the unsupported target until a group +consisting of at least one supported target is found; +- assume that declarations shared by all targets in the group found during the previous step are common +to all group members (including unsupported targets); +- generate a dump for unsupported targets by combining this newly generated "common" ABI with all declarations +specific to the unsupported target extracted from an old dump. + +The higher the hierarchy we go, the larger the group of targets should be, so the ABI shared by all these targets +should be "more common" (lowering changes of including some target-specific declarations). On the other hand, +if unsupported targets have some target-specific declarations, then it's likely that the targets closer to them in +the hierarchy are also having these declarations. + +Here's an example of walking up the target hierarchy for `iosArm64` target on a Linux host for a project +having only Apple-targets and WasmJs-target: +- `iosArm64` is unsupported, let's try any `ios` target; +- all `ios` targets are unavailable, let's try any `apple` target; +- all `apple` targets are unavailable, let's try any `native` target; +- all `native` targets are unavailable, let's try any target; +- `wasmJs` target is available, lets use its dump. + +The table below summarizes different scenarios and how the two different approaches handle them. + + +| Scenario | Ignore unsupported target | | Guess ABI for unsupported targets | | +|-----------------------------------------------------------------------------------|------------------------------------------------------------------------------------|------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------| +| | **How it’ll be updated?** | **Valid?** | **How it’ll be updated?** | **Valid?** | +| **KMP-project without A target-specific ABI** | | | | | +| Change common declaration | An Apple-ABI will remain unchanged, but for all other targets the ABI will differ. | No ❌ | The declaration will be declared in all supported targets, we’ll consider it as common and will also add to unsupported (apple) targets without any changes, old apple-ABI will be replaced with the updated one | Yes 👌 | +| Move a declaration from the common sources to every single target-specific source set | An Apple-ABI will remain unchanged, but for all other targets the ABI will differ. | No ❌ | The declaration will be declared in all supported targets, we’ll consider it as common and will also add to unsupported (apple) targets without any changes, old apple-ABI will be replaced with the updated one | Yes 👌 | +| **KMP-project with an Apple-specific ABI** | | | | | +| Change a common declaration | Old version of the declaration will remain in the Apple-ABI dump. |No ❌ | The declaration will be considered common as all supported targets’ dumps will have it, old apple-specific parts of the ABI will remain untouched|Yes 👌| +| Change an Apple-specific declaration | An Apple-ABI dump won’t get any updates | No ❌ | An Apple-specific declarations won’t be updated | No ❌| +| **KMP-project with a linux-spefic ABI** | | | | | +| Change a common declaration | Old version of the declaration will remain in the Apple-ABI dump. | No ❌ | The declaration will be considered common as all supported targets’ dumps will have it, old apple-specific parts of the ABI will remain untouched | Yes 👌| +| Change a Linux-specific declaration | An Apple-ABI will remain unchanged, but for all other targets the ABI will differ. | Yes 👌 | An Aplple-ABI will remain untouched | Yes 👌| +| **KMP-project with a non-Apple specific ABI (i.e. linux+mingw+js+wasm-specific)** | | | | | +| Change a declaration for all non-Apple targets | An Apple ABI will remain untouched | Yes 👌 | Tha changed ABI will be considered as common (as it came from all the supported targets) and we’ll mark it as an ABI available on Apple targets too. | No ❌| +| **Module having only Apple-specific ABI** | | | | | +| Change an ABI | No dumps available, the process will fail (or we can just avoid any updates) | No ❌ | No dumps available, the process will fail (or we can just avoid any updates) | No ❌| + + +The "guessing" approach should succeed it a broader spectrum of scenarios, so we decided to use it. + +### Programmatic API for KLib ABI validation + +To support scenarios when Gradle plugin could not be used (like in the case of Kotlin stdlib), +or when the way users build klibs is different from "compile all the klibs here and now", the programmatic API +should be provided. The API should simply provide an entry point to all the functionality used by the plugin. + +The API is not yet designed and is the subject of further development. From 5a6cf13b1948cd6b1b50ce3857af8ad00b454d87 Mon Sep 17 00:00:00 2001 From: Filipp Zhinkin Date: Thu, 1 Feb 2024 10:39:57 +0100 Subject: [PATCH 08/53] Cleanup --- src/main/kotlin/ApiValidationExtension.kt | 4 +-- src/main/kotlin/klib/KlibAbiDumpFileMerger.kt | 25 +++++++++---------- 2 files changed, 14 insertions(+), 15 deletions(-) diff --git a/src/main/kotlin/ApiValidationExtension.kt b/src/main/kotlin/ApiValidationExtension.kt index 47efbb4a..5462bdfb 100644 --- a/src/main/kotlin/ApiValidationExtension.kt +++ b/src/main/kotlin/ApiValidationExtension.kt @@ -80,7 +80,7 @@ public open class ApiValidationExtension { public val klib: KlibValidationSettings = KlibValidationSettings() /** - * Configure KLib AVI validation settings. + * Configure KLib ABI validation settings. */ public fun klib(block: KlibValidationSettings.() -> Unit) { block(this.klib) @@ -102,7 +102,7 @@ public open class KlibValidationSettings { /** * Fail validation if some build targets are not supported by the host compiler. * By default, ABI dumped only for supported files will be validated. This option makes validation behavior - * more strict and treat having unsupported targets as an error. + * stricter and treats having unsupported targets as an error. */ public var strictValidation: Boolean = false /** diff --git a/src/main/kotlin/klib/KlibAbiDumpFileMerger.kt b/src/main/kotlin/klib/KlibAbiDumpFileMerger.kt index 67321aac..6a6ee82e 100644 --- a/src/main/kotlin/klib/KlibAbiDumpFileMerger.kt +++ b/src/main/kotlin/klib/KlibAbiDumpFileMerger.kt @@ -7,7 +7,6 @@ package kotlinx.validation.klib import java.io.File import java.nio.file.Files -import javax.sound.sampled.Line internal data class Target(val name: String) @@ -122,11 +121,11 @@ internal class KlibAbiDumpMerger { this.targetsMut.addAll(bcvTargets) topLevelDeclaration.targets.addAll(bcvTargets) - // All declarations belonging to the same scope has equal indentation. + // All declarations belonging to the same scope have equal indentation. // Nested declarations have higher indentation. - // By tracking the indentation we can decide if the line should be added into the current container, - // to its parent container (i.e. the line represents sibling declaration) or the current declaration ended, - // and we must pop one or several declaration out of the parsing stack. + // By tracking the indentation, we can decide if the line should be added into the current container, + // to its parent container (i.e., the line represents sibling declaration) or the current declaration ended, + // and we must pop one or several declarations out of the parsing stack. var currentContainer = topLevelDeclaration var depth = -1 @@ -135,12 +134,12 @@ internal class KlibAbiDumpMerger { // TODO: wrap the line and cache the depth inside that wrapper? val lineDepth = line.depth() when { - // The depth is the same as before, we encountered a sibling + // The depth is the same as before; we encountered a sibling depth == lineDepth -> { currentContainer = lines.parseDeclaration(lineDepth, currentContainer.parent!!, bcvTargets, isMergedFile, aliases) } - // The depth is increasing, that means we encountered child declaration + // The depth is increasing; that means we encountered child declaration depth < lineDepth -> { check(lineDepth - depth == 1) { "The line has too big indentation relative to a previous line\nline: $line\n" + @@ -158,7 +157,7 @@ internal class KlibAbiDumpMerger { while (currentContainer.text.depth() > lineDepth) { currentContainer = currentContainer.parent!! } - // If the line is '}' - add it as a terminator to corresponding declaration, it'll simplify + // If the line is '}' - add it as a terminator to the corresponding declaration, it'll simplify // dumping the merged file back to text format. if (line.trim() == CLASS_DECLARATION_TERMINATOR) { currentContainer.delimiter = line @@ -256,7 +255,7 @@ internal class KlibAbiDumpMerger { "and the current file claimed to be a regular dump file:\n$line" } next() // skip prefix - // Target list means that the declaration following it has narrower set of targets then its parent, + // Target list means that the declaration following it has a narrower set of targets then its parent, // so we must use it. val targets = parseBcvTargetsLine(line) val expandedTargets = targets.flatMap { @@ -266,9 +265,9 @@ internal class KlibAbiDumpMerger { } else { // That's an ugly part: // - for a merged file (isMergedFile==true) we need to use parent declaration targets: if we're in this - // branch, no explicit targets were specified and new declaration targets should be the same as targets - // of its parent. We can't use allTargets here, as parent may have more specific set of targets. - // - for a single klib dump file we need to specify exact target associated with this file and allTargets + // branch, no explicit targets were specified, and new declaration targets should be the same as targets + // of its parent. We can't use allTargets here, as parent may have a more specific set of targets. + // - for a single klib dump file, we need to specify the exact target associated with this file and allTargets // must contain exactly one value here. parent.createOrUpdateChildren(next(), if (isMergedFile) parent.targets else allTargets) } @@ -506,7 +505,7 @@ private object DeclarationsComparator : Comparator { } result } else { - // longer the target list, earlier the declaration would appear + // the longer the target list, the earlier the declaration would appear c1.targets.size.compareTo(c0.targets.size) } } From d58121c3b9f2551043edb87ba398171e2f8ac594 Mon Sep 17 00:00:00 2001 From: Filipp Zhinkin Date: Thu, 1 Feb 2024 16:46:14 +0100 Subject: [PATCH 09/53] Mark an experimental API with a dedicated annotation --- api/binary-compatibility-validator.api | 3 +++ build.gradle.kts | 5 +++-- src/main/kotlin/ApiValidationExtension.kt | 3 +++ .../kotlin/BinaryCompatibilityValidatorPlugin.kt | 6 ++++++ src/main/kotlin/ExperimentalBCVApi.kt | 14 ++++++++++++++ src/main/kotlin/klib/KlibAbiDumpFileMerger.kt | 7 +++++++ src/main/kotlin/klib/TargetHierarchy.kt | 3 +++ 7 files changed, 39 insertions(+), 2 deletions(-) create mode 100644 src/main/kotlin/ExperimentalBCVApi.kt diff --git a/api/binary-compatibility-validator.api b/api/binary-compatibility-validator.api index 4dd07406..9ade3185 100644 --- a/api/binary-compatibility-validator.api +++ b/api/binary-compatibility-validator.api @@ -49,6 +49,9 @@ public abstract class kotlinx/validation/BuildTaskBase : org/gradle/api/DefaultT public final fun setPublicPackages (Ljava/util/Set;)V } +public abstract interface annotation class kotlinx/validation/ExperimentalBCVApi : java/lang/annotation/Annotation { +} + public abstract interface annotation class kotlinx/validation/ExternalApi : java/lang/annotation/Annotation { } diff --git a/build.gradle.kts b/build.gradle.kts index 94e6c1f4..372c409d 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -79,7 +79,6 @@ dependencies { tasks.compileKotlin { compilerOptions { - freeCompilerArgs.add("-Xexplicit-api=strict") allWarningsAsErrors.set(true) @Suppress("DEPRECATION") // Compatibility with Gradle 7 requires Kotlin 1.4 languageVersion.set(KotlinVersion.KOTLIN_1_4) @@ -88,7 +87,9 @@ tasks.compileKotlin { // Suppressing "w: Language version 1.4 is deprecated and its support will be removed" message // because LV=1.4 in practice is mandatory as it is a default language version in Gradle 7.0+ for users' kts scripts. freeCompilerArgs.addAll( - "-Xsuppress-version-warnings" + "-Xexplicit-api=strict", + "-Xsuppress-version-warnings", + "-Xopt-in=kotlin.RequiresOptIn" ) } } diff --git a/src/main/kotlin/ApiValidationExtension.kt b/src/main/kotlin/ApiValidationExtension.kt index 5462bdfb..6afe7da2 100644 --- a/src/main/kotlin/ApiValidationExtension.kt +++ b/src/main/kotlin/ApiValidationExtension.kt @@ -77,11 +77,13 @@ public open class ApiValidationExtension { * * @see KlibValidationSettings */ + @ExperimentalBCVApi public val klib: KlibValidationSettings = KlibValidationSettings() /** * Configure KLib ABI validation settings. */ + @ExperimentalBCVApi public fun klib(block: KlibValidationSettings.() -> Unit) { block(this.klib) } @@ -90,6 +92,7 @@ public open class ApiValidationExtension { /** * Settings affecting KLib ABI validation. */ +@ExperimentalBCVApi public open class KlibValidationSettings { /** * Enables KLib ABI validation checks. diff --git a/src/main/kotlin/BinaryCompatibilityValidatorPlugin.kt b/src/main/kotlin/BinaryCompatibilityValidatorPlugin.kt index c676dd8a..81ac1db7 100644 --- a/src/main/kotlin/BinaryCompatibilityValidatorPlugin.kt +++ b/src/main/kotlin/BinaryCompatibilityValidatorPlugin.kt @@ -24,6 +24,7 @@ internal const val KLIB_ALL_PHONY_TARGET_NAME = "klib-all" public class BinaryCompatibilityValidatorPlugin : Plugin { @ExperimentalLibraryAbiReader + @ExperimentalBCVApi override fun apply(target: Project): Unit = with(target) { val extension = extensions.create("apiValidation", ApiValidationExtension::class.java) validateExtension(extension) @@ -33,6 +34,7 @@ public class BinaryCompatibilityValidatorPlugin : Plugin { } @ExperimentalLibraryAbiReader + @ExperimentalBCVApi private fun Project.validateExtension(extension: ApiValidationExtension) { afterEvaluate { val ignored = extension.ignoredProjects @@ -53,6 +55,7 @@ public class BinaryCompatibilityValidatorPlugin : Plugin { } } + @ExperimentalBCVApi private fun configureProject(project: Project, extension: ApiValidationExtension) { configureKotlinPlugin(project, extension) configureAndroidPlugin(project, extension) @@ -69,6 +72,7 @@ public class BinaryCompatibilityValidatorPlugin : Plugin { action.execute(it) } + @ExperimentalBCVApi private fun configureMultiplatformPlugin( project: Project, extension: ApiValidationExtension @@ -255,6 +259,7 @@ internal val Project.apiValidationExtensionOrNull: ApiValidationExtension? private fun apiCheckEnabled(projectName: String, extension: ApiValidationExtension): Boolean = projectName !in extension.ignoredProjects && !extension.validationDisabled +@ExperimentalBCVApi private fun klibAbiCheckEnabled(projectName: String, extension: ApiValidationExtension): Boolean = projectName !in extension.ignoredProjects && !extension.validationDisabled && extension.klib.enabled @@ -333,6 +338,7 @@ private inline fun Project.task( internal const val BANNED_TARGETS_PROPERTY_NAME = "binary.compatibility.validator.klib.targets.blacklist.for.testing" +@ExperimentalBCVApi private class KlibValidationPipelineBuilder( val dirConfig: Provider?, val extension: ApiValidationExtension diff --git a/src/main/kotlin/ExperimentalBCVApi.kt b/src/main/kotlin/ExperimentalBCVApi.kt new file mode 100644 index 00000000..7258943a --- /dev/null +++ b/src/main/kotlin/ExperimentalBCVApi.kt @@ -0,0 +1,14 @@ +/* + * Copyright 2016-2024 JetBrains s.r.o. + * Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file. + */ + +package kotlinx.validation + +/** + * Marks an API that is still experimental in Binary compatibility validator and may change + * in the future. There are also no guarantees on preserving the behavior of the API until its + * stabilization. + */ +@RequiresOptIn(level = RequiresOptIn.Level.WARNING) +public annotation class ExperimentalBCVApi diff --git a/src/main/kotlin/klib/KlibAbiDumpFileMerger.kt b/src/main/kotlin/klib/KlibAbiDumpFileMerger.kt index 6a6ee82e..ea6bb9b9 100644 --- a/src/main/kotlin/klib/KlibAbiDumpFileMerger.kt +++ b/src/main/kotlin/klib/KlibAbiDumpFileMerger.kt @@ -71,6 +71,9 @@ internal data class KlibAbiDumpFormat( val useGroupAliases: Boolean = false ) +/** + * A class representing a textual KLib ABI dump, either a regular one, or a merged. + */ internal class KlibAbiDumpMerger { private val targetsMut: MutableSet = mutableSetOf() private val headerContent: MutableList = mutableListOf() @@ -377,6 +380,10 @@ internal class KlibAbiDumpMerger { } } +/** + * A class representing a single declaration from a KLib API dump along with all its children + * declarations. + */ private class DeclarationContainer(val text: String, val parent: DeclarationContainer? = null) { val targets: MutableSet = mutableSetOf() val children: MutableList = mutableListOf() diff --git a/src/main/kotlin/klib/TargetHierarchy.kt b/src/main/kotlin/klib/TargetHierarchy.kt index e78f73b6..c3c46163 100644 --- a/src/main/kotlin/klib/TargetHierarchy.kt +++ b/src/main/kotlin/klib/TargetHierarchy.kt @@ -5,6 +5,9 @@ package kotlinx.validation.klib +/** + * A hierarchy of KMP targets that should resemble the default hierarchy template. + */ internal object TargetHierarchy { class Node(val name: String, vararg childrenNodes: Node) { var parent: Node? = null From 34313b1ece98b96ac6424fb66596e0863f2a4c3e Mon Sep 17 00:00:00 2001 From: Filipp Zhinkin Date: Thu, 1 Feb 2024 17:33:38 +0100 Subject: [PATCH 10/53] Cleanup --- docs/design/KLibSupport.md | 2 +- .../validation/api/BaseKotlinGradleTest.kt | 10 --- .../validation/test/KLibVerificationTests.kt | 88 +++++++++++-------- .../validation/test/NonPublicMarkersTest.kt | 10 +-- .../BinaryCompatibilityValidatorPlugin.kt | 13 ++- 5 files changed, 61 insertions(+), 62 deletions(-) diff --git a/docs/design/KLibSupport.md b/docs/design/KLibSupport.md index 51d571c8..c1926d58 100644 --- a/docs/design/KLibSupport.md +++ b/docs/design/KLibSupport.md @@ -1,4 +1,4 @@ -The document described assumptions that led to the current KLib ABI validation implementation. +The document describes assumptions that led to the current KLib ABI validation implementation. ### Motivation and assumptions diff --git a/src/functionalTest/kotlin/kotlinx/validation/api/BaseKotlinGradleTest.kt b/src/functionalTest/kotlin/kotlinx/validation/api/BaseKotlinGradleTest.kt index 02db79a0..d60db33d 100644 --- a/src/functionalTest/kotlin/kotlinx/validation/api/BaseKotlinGradleTest.kt +++ b/src/functionalTest/kotlin/kotlinx/validation/api/BaseKotlinGradleTest.kt @@ -9,9 +9,6 @@ import org.junit.Rule import org.junit.rules.TemporaryFolder import java.io.File -internal const val KLIB_PHONY_TARGET_NAME = "klib" -internal const val KLIB_ALL_PHONY_TARGET_NAME = "klib-all" - public open class BaseKotlinGradleTest { @Rule @JvmField @@ -21,14 +18,7 @@ public open class BaseKotlinGradleTest { internal val rootProjectApiDump: File get() = rootProjectDir.resolve("$API_DIR/${rootProjectDir.name}.api") - internal fun rootProjectAbiDump(target: String, project: String = rootProjectDir.name): File { - // TODO: rewrite - val suffix = if (target != KLIB_PHONY_TARGET_NAME) "api" else "klib.api" - return rootProjectDir.resolve("$API_DIR/$target/$project.$suffix") - } - internal fun rootProjectAbiDump(project: String = rootProjectDir.name): File { - // TODO: rewrite return rootProjectDir.resolve("$API_DIR/$project.klib.api") } } diff --git a/src/functionalTest/kotlin/kotlinx/validation/test/KLibVerificationTests.kt b/src/functionalTest/kotlin/kotlinx/validation/test/KLibVerificationTests.kt index 69768986..0baca1fa 100644 --- a/src/functionalTest/kotlin/kotlinx/validation/test/KLibVerificationTests.kt +++ b/src/functionalTest/kotlin/kotlinx/validation/test/KLibVerificationTests.kt @@ -19,17 +19,15 @@ import kotlin.test.assertTrue internal const val BANNED_TARGETS_PROPERTY_NAME = "binary.compatibility.validator.klib.targets.blacklist.for.testing" -private fun KLibVerificationTests.checkKlibDump(buildResult: BuildResult, expectedDumpFileName: String, - projectName: String = "testproject", - dumpTask: String = ":apiDump", - projectWithMultipleDumps: Boolean = false) { +private fun KLibVerificationTests.checkKlibDump( + buildResult: BuildResult, + expectedDumpFileName: String, + projectName: String = "testproject", + dumpTask: String = ":apiDump" +) { buildResult.assertTaskSuccess(dumpTask) - val generatedDump = if (projectWithMultipleDumps) { - rootProjectAbiDump(target = KLIB_PHONY_TARGET_NAME, project = projectName) - } else { - rootProjectAbiDump(projectName) - } + val generatedDump = rootProjectAbiDump(projectName) assertTrue(generatedDump.exists(), "There are no dumps generated for KLibs") val expected = readFileList(expectedDumpFileName) @@ -140,14 +138,10 @@ internal class KLibVerificationTests : BaseKotlinGradleTest() { } runner.build().apply { - assertTaskSuccess(":apiDump") - - // not common, but built from the common source set - val dump = rootProjectAbiDump("testproject") - assertTrue(dump.exists(), "Dump does not exist") - - val expectedDump = readFileList("/examples/classes/AnotherBuildConfigLinuxArm64Extra.klib.dump") - Assertions.assertThat(dump.readText()).isEqualToIgnoringNewLines(expectedDump) + checkKlibDump( + this, + "/examples/classes/AnotherBuildConfigLinuxArm64Extra.klib.dump" + ) } } @@ -170,8 +164,7 @@ internal class KLibVerificationTests : BaseKotlinGradleTest() { } runner.build().apply { - checkKlibDump(this, "/examples/classes/AnotherBuildConfig.klib.dump", - projectWithMultipleDumps = false) + checkKlibDump(this, "/examples/classes/AnotherBuildConfig.klib.dump") val jvmApiDump = rootProjectDir.resolve("$API_DIR/testproject.api") assertTrue(jvmApiDump.exists(), "No API dump for JVM") @@ -403,7 +396,7 @@ internal class KLibVerificationTests : BaseKotlinGradleTest() { } @Test - fun `apiCheck should fail if a target is not supported`() { + fun `apiCheck should not fail if a target is not supported`() { val runner = test { settingsGradleKts { resolve("/examples/gradle/settings/settings-name-testproject.gradle.kts") @@ -414,7 +407,8 @@ internal class KLibVerificationTests : BaseKotlinGradleTest() { kotlin("TopLevelDeclarations.kt", "commonMain") { resolve("/examples/classes/TopLevelDeclarations.kt") } - abiFile(projectName = "testproject", target = KLIB_PHONY_TARGET_NAME) { + abiFile(projectName = "testproject") { + resolve("/examples/classes/TopLevelDeclarations.klib.dump") } runner { arguments.add("-P$BANNED_TARGETS_PROPERTY_NAME=linuxArm64") @@ -422,7 +416,9 @@ internal class KLibVerificationTests : BaseKotlinGradleTest() { } } - runner.buildAndFail() + runner.build().apply { + assertTaskSuccess(":apiCheck") + } } @Test @@ -501,8 +497,10 @@ internal class KLibVerificationTests : BaseKotlinGradleTest() { } } - checkKlibDump(runner.build(), "/examples/classes/TopLevelDeclarations.klib.with.linux.dump", - dumpTask = ":klibApiDump") + checkKlibDump( + runner.build(), "/examples/classes/TopLevelDeclarations.klib.with.linux.dump", + dumpTask = ":klibApiDump" + ) } @Test @@ -528,8 +526,10 @@ internal class KLibVerificationTests : BaseKotlinGradleTest() { runner.buildAndFail().apply { assertTaskFailure(":linuxArm64ApiInferAbiDump") - Assertions.assertThat(output).contains("The target linuxArm64 is not supported by the host compiler " + - "and there are no targets similar to linuxArm64 to infer a dump from it.") + Assertions.assertThat(output).contains( + "The target linuxArm64 is not supported by the host compiler " + + "and there are no targets similar to linuxArm64 to infer a dump from it." + ) } } @@ -546,16 +546,18 @@ internal class KLibVerificationTests : BaseKotlinGradleTest() { resolve("/examples/classes/TopLevelDeclarations.kt") } runner { - arguments.add("-P$BANNED_TARGETS_PROPERTY_NAME=linuxArm64,linuxX64,mingwX64," + - "androidNativeArm32,androidNativeArm64,androidNativeX64,androidNativeX86") + arguments.add( + "-P$BANNED_TARGETS_PROPERTY_NAME=linuxArm64,linuxX64,mingwX64," + + "androidNativeArm32,androidNativeArm64,androidNativeX64,androidNativeX86" + ) arguments.add(":klibApiDump") } } runner.buildAndFail().apply { - Assertions.assertThat(output).contains( - "is not supported by the host compiler and there are no targets similar to" - ) + Assertions.assertThat(output).contains( + "is not supported by the host compiler and there are no targets similar to" + ) } } @@ -576,8 +578,10 @@ internal class KLibVerificationTests : BaseKotlinGradleTest() { resolve("/examples/classes/TopLevelDeclarations.klib.dump") } runner { - arguments.add("-P$BANNED_TARGETS_PROPERTY_NAME=linuxArm64,linuxX64,mingwX64," + - "androidNativeArm32,androidNativeArm64,androidNativeX64,androidNativeX86") + arguments.add( + "-P$BANNED_TARGETS_PROPERTY_NAME=linuxArm64,linuxX64,mingwX64," + + "androidNativeArm32,androidNativeArm64,androidNativeX64,androidNativeX86" + ) arguments.add(":klibApiCheck") } } @@ -607,8 +611,10 @@ internal class KLibVerificationTests : BaseKotlinGradleTest() { } } - checkKlibDump(runner.build(), "/examples/classes/AnotherBuildConfig.klib.clash.dump", - dumpTask = ":klibApiDump") + checkKlibDump( + runner.build(), "/examples/classes/AnotherBuildConfig.klib.clash.dump", + dumpTask = ":klibApiDump" + ) } @Test @@ -629,8 +635,10 @@ internal class KLibVerificationTests : BaseKotlinGradleTest() { } } - checkKlibDump(runner.build(), "/examples/classes/AnotherBuildConfig.klib.custom.dump", - dumpTask = ":klibApiDump") + checkKlibDump( + runner.build(), "/examples/classes/AnotherBuildConfig.klib.custom.dump", + dumpTask = ":klibApiDump" + ) } @Test @@ -656,7 +664,9 @@ internal class KLibVerificationTests : BaseKotlinGradleTest() { } } - checkKlibDump(runner.build(), "/examples/classes/AnotherBuildConfigLinux.klib.grouping.dump", - dumpTask = ":klibApiDump") + checkKlibDump( + runner.build(), "/examples/classes/AnotherBuildConfigLinux.klib.grouping.dump", + dumpTask = ":klibApiDump" + ) } } diff --git a/src/functionalTest/kotlin/kotlinx/validation/test/NonPublicMarkersTest.kt b/src/functionalTest/kotlin/kotlinx/validation/test/NonPublicMarkersTest.kt index ff7ca991..4bb804b7 100644 --- a/src/functionalTest/kotlin/kotlinx/validation/test/NonPublicMarkersTest.kt +++ b/src/functionalTest/kotlin/kotlinx/validation/test/NonPublicMarkersTest.kt @@ -43,21 +43,21 @@ class NonPublicMarkersTest : BaseKotlinGradleTest() { fun testIgnoredMarkersOnPropertiesForNativeTargets() { val runner = test { settingsGradleKts { - resolve("examples/gradle/settings/settings-name-testproject.gradle.kts") + resolve("/examples/gradle/settings/settings-name-testproject.gradle.kts") } buildGradleKts { - resolve("examples/gradle/base/withNativePlugin.gradle.kts") - resolve("examples/gradle/configuration/nonPublicMarkers/markers.gradle.kts") + resolve("/examples/gradle/base/withNativePlugin.gradle.kts") + resolve("/examples/gradle/configuration/nonPublicMarkers/markers.gradle.kts") } kotlin("Properties.kt", sourceSet = "commonMain") { - resolve("examples/classes/Properties.kt") + resolve("/examples/classes/Properties.kt") } commonNativeTargets.forEach { abiFile(projectName = "testproject", target = it) { - resolve("examples/classes/Properties.klib.dump") + resolve("/examples/classes/Properties.klib.dump") } } diff --git a/src/main/kotlin/BinaryCompatibilityValidatorPlugin.kt b/src/main/kotlin/BinaryCompatibilityValidatorPlugin.kt index 81ac1db7..0ed18cd4 100644 --- a/src/main/kotlin/BinaryCompatibilityValidatorPlugin.kt +++ b/src/main/kotlin/BinaryCompatibilityValidatorPlugin.kt @@ -18,9 +18,6 @@ import org.jetbrains.kotlin.library.abi.LibraryAbiReader import java.io.* import kotlin.text.split -internal const val KLIB_PHONY_TARGET_NAME = "klib" -internal const val KLIB_ALL_PHONY_TARGET_NAME = "klib-all" - public class BinaryCompatibilityValidatorPlugin : Plugin { @ExperimentalLibraryAbiReader @@ -336,7 +333,9 @@ private inline fun Project.task( noinline configuration: T.() -> Unit, ): TaskProvider = tasks.register(name, T::class.java, Action(configuration)) -internal const val BANNED_TARGETS_PROPERTY_NAME = "binary.compatibility.validator.klib.targets.blacklist.for.testing" +private const val BANNED_TARGETS_PROPERTY_NAME = "binary.compatibility.validator.klib.targets.blacklist.for.testing" +private const val KLIB_DUMPS_DIRECTORY = "klib" +private const val KLIB_INFERRED_DUMPS_DIRECTORY = "klib-all" @ExperimentalBCVApi private class KlibValidationPipelineBuilder( @@ -349,9 +348,9 @@ private class KlibValidationPipelineBuilder( // In the intermediate phase of Klib dump generation there are always multiple targets, thus we need // target-based directory tree. intermediateFilesConfig = project.provider { DirConfig.TARGET_DIR } - val klibApiDirConfig = dirConfig?.map { TargetConfig(project, extension, KLIB_PHONY_TARGET_NAME, dirConfig) } - val klibDumpConfig = TargetConfig(project, extension, KLIB_PHONY_TARGET_NAME, intermediateFilesConfig) - val klibDumpAllConfig = TargetConfig(project, extension, KLIB_ALL_PHONY_TARGET_NAME, intermediateFilesConfig) + val klibApiDirConfig = dirConfig?.map { TargetConfig(project, extension, KLIB_DUMPS_DIRECTORY, dirConfig) } + val klibDumpConfig = TargetConfig(project, extension, KLIB_DUMPS_DIRECTORY, intermediateFilesConfig) + val klibDumpAllConfig = TargetConfig(project, extension, KLIB_INFERRED_DUMPS_DIRECTORY, intermediateFilesConfig) val projectDir = project.projectDir val klibApiDir = klibApiDirConfig?.map { From 4bc9d2acd8a68501aa95441a142cbbdfc475d58e Mon Sep 17 00:00:00 2001 From: Filipp Zhinkin Date: Thu, 1 Feb 2024 17:50:05 +0100 Subject: [PATCH 11/53] Add some kdoc --- src/main/kotlin/KotlinKlibAbiBuildTask.kt | 14 ++++++++++++- ...otlinKlibExtractSupportedTargetsAbiTask.kt | 15 +++++++++++++ ...linKlibInferAbiForUnsupportedTargetTask.kt | 21 ++++++++++++++++++- src/main/kotlin/KotlinKlibMergeAbiTask.kt | 12 +++++++++++ 4 files changed, 60 insertions(+), 2 deletions(-) diff --git a/src/main/kotlin/KotlinKlibAbiBuildTask.kt b/src/main/kotlin/KotlinKlibAbiBuildTask.kt index 36a44ca8..bc24ceda 100644 --- a/src/main/kotlin/KotlinKlibAbiBuildTask.kt +++ b/src/main/kotlin/KotlinKlibAbiBuildTask.kt @@ -13,20 +13,32 @@ import org.gradle.api.tasks.TaskAction import org.jetbrains.kotlin.library.abi.* /** - * Generates a text file with a ABI dump for a single klib. + * Generates a text file with a KLib ABI dump for a single klib. */ public abstract class KotlinKlibAbiBuildTask : BuildTaskBase() { + /** + * Path to a klib to dump. + */ @InputFiles public lateinit var klibFile: FileCollection + /** + * Bind this task with a klib compilation. + */ @InputFiles public lateinit var compilationDependencies: FileCollection + /** + * Refer to [KlibValidationSettings.signatureVersion] for details. + */ @Optional @get:Input public var signatureVersion: Int? = null + /** + * Name of a target [klibFile] was compiled for. + */ @Input public lateinit var target: String diff --git a/src/main/kotlin/KotlinKlibExtractSupportedTargetsAbiTask.kt b/src/main/kotlin/KotlinKlibExtractSupportedTargetsAbiTask.kt index 12401f02..12c03708 100644 --- a/src/main/kotlin/KotlinKlibExtractSupportedTargetsAbiTask.kt +++ b/src/main/kotlin/KotlinKlibExtractSupportedTargetsAbiTask.kt @@ -20,18 +20,33 @@ public abstract class KotlinKlibExtractSupportedTargetsAbiTask : DefaultTask() { @get:Internal internal val projectName = project.name + /** + * Merged KLib dump that should be filtered by this task. + */ @InputFiles public lateinit var inputAbiFile: File + /** + * A path to the resulting dump file. + */ @OutputFile public lateinit var outputAbiFile: File + /** + * Provider returning targets supported by the host compiler. + */ @get:Input public lateinit var targets: Provider> + /** + * Refer to [KlibValidationSettings.strictValidation] for details. + */ @Input public var strictValidation: Boolean = false + /** + * Refer to [KlibValidationSettings.useTargetGroupAliases] for details. + */ @Input public var groupTargetNames: Boolean = true diff --git a/src/main/kotlin/KotlinKlibInferAbiForUnsupportedTargetTask.kt b/src/main/kotlin/KotlinKlibInferAbiForUnsupportedTargetTask.kt index 977bd74f..66e5ccf3 100644 --- a/src/main/kotlin/KotlinKlibInferAbiForUnsupportedTargetTask.kt +++ b/src/main/kotlin/KotlinKlibInferAbiForUnsupportedTargetTask.kt @@ -16,7 +16,7 @@ import java.io.File /** * Task infers a possible KLib ABI dump for an unsupported target. - * To infer a dump, tasks walks up the default targets hierarchy tree starting from the unsupported + * To infer a dump, tasks walk up the default targets hierarchy tree starting from the unsupported * target until it finds a node corresponding to a group of targets having at least one supported target. * After that, dumps generated for such supported targets are merged and declarations that are common to all * of them are considered as a common ABI that most likely will be shared by the unsupported target. @@ -28,21 +28,40 @@ public abstract class KotlinKlibInferAbiForUnsupportedTargetTask : DefaultTask() @get:Internal internal val projectName = project.name + /** + * The name of a target to infer a dump for. + */ @Input public lateinit var unsupportedTarget: String + /** + * A root directory containing dumps successfully generated for each supported target. + * It is assumed that this directory contains subdirectories named after targets. + */ @InputFiles public lateinit var outputApiDir: String + /** + * Set of all supported targets. + */ @Input public lateinit var supportedTargets: Set + /** + * Previously generated merged ABI dump file, the golden image every dump should be verified against. + */ @InputFiles public lateinit var inputImageFile: File + /** + * The name of a dump file. + */ @Input public lateinit var dumpFileName: String + /** + * A path to an inferred dump file. + */ @OutputFile public lateinit var outputFile: File diff --git a/src/main/kotlin/KotlinKlibMergeAbiTask.kt b/src/main/kotlin/KotlinKlibMergeAbiTask.kt index cf616628..81a13dca 100644 --- a/src/main/kotlin/KotlinKlibMergeAbiTask.kt +++ b/src/main/kotlin/KotlinKlibMergeAbiTask.kt @@ -22,16 +22,28 @@ public abstract class KotlinKlibMergeAbiTask : DefaultTask() { @get:Internal internal val projectName = project.name + /** + * Set of targets whose dumps should be merged. + */ @get:Input public val targets: Set get() = targetToFile.keys + /** + * A path to a resulting merged dump. + */ @OutputFile public lateinit var mergedFile: File + /** + * The name of a dump file. + */ @Input public lateinit var dumpFileName: String + /** + * Refer to [KlibValidationSettings.useTargetGroupAliases] for details. + */ @Input public var groupTargetNames: Boolean = true From a987909bea4c9d99baf7b445ec45f28ab879441a Mon Sep 17 00:00:00 2001 From: Filipp Zhinkin Date: Mon, 5 Feb 2024 10:50:37 +0100 Subject: [PATCH 12/53] Improved test coverage --- .../validation/test/KLibVerificationTests.kt | 51 ++++++++++++++++++- .../classes/AnotherBuildConfig.klib.web.dump | 17 +++++++ .../nonNativeKlibTargets/targets.gradle.kts | 10 ++++ .../signatures/invalid.gradle.kts | 4 +- 4 files changed, 80 insertions(+), 2 deletions(-) create mode 100644 src/functionalTest/resources/examples/classes/AnotherBuildConfig.klib.web.dump create mode 100644 src/functionalTest/resources/examples/gradle/configuration/nonNativeKlibTargets/targets.gradle.kts diff --git a/src/functionalTest/kotlin/kotlinx/validation/test/KLibVerificationTests.kt b/src/functionalTest/kotlin/kotlinx/validation/test/KLibVerificationTests.kt index 0baca1fa..7e8d2ec6 100644 --- a/src/functionalTest/kotlin/kotlinx/validation/test/KLibVerificationTests.kt +++ b/src/functionalTest/kotlin/kotlinx/validation/test/KLibVerificationTests.kt @@ -343,7 +343,9 @@ internal class KLibVerificationTests : BaseKotlinGradleTest() { } } - runner.buildAndFail() + runner.buildAndFail().apply { + Assertions.assertThat(output).contains("Unsupported KLib signature version '100500'") + } } @Test @@ -669,4 +671,51 @@ internal class KLibVerificationTests : BaseKotlinGradleTest() { dumpTask = ":klibApiDump" ) } + + @Test + fun `apiDump should work with web targets`() { + val runner = test { + settingsGradleKts { + resolve("/examples/gradle/settings/settings-name-testproject.gradle.kts") + } + buildGradleKts { + resolve("/examples/gradle/base/withNativePlugin.gradle.kts") + resolve("/examples/gradle/configuration/nonNativeKlibTargets/targets.gradle.kts") + } + kotlin("AnotherBuildConfig.kt", "commonMain") { + resolve("/examples/classes/AnotherBuildConfig.kt") + } + runner { + arguments.add(":apiDump") + } + } + + checkKlibDump(runner.build(), "/examples/classes/AnotherBuildConfig.klib.web.dump") + } + + @Test + fun `apiCheck should work with web targets`() { + val runner = test { + settingsGradleKts { + resolve("/examples/gradle/settings/settings-name-testproject.gradle.kts") + } + buildGradleKts { + resolve("/examples/gradle/base/withNativePlugin.gradle.kts") + resolve("/examples/gradle/configuration/nonNativeKlibTargets/targets.gradle.kts") + } + kotlin("AnotherBuildConfig.kt", "commonMain") { + resolve("/examples/classes/AnotherBuildConfig.kt") + } + abiFile(projectName = "testproject") { + resolve("/examples/classes/AnotherBuildConfig.klib.web.dump") + } + runner { + arguments.add(":apiCheck") + } + } + + runner.build().apply { + assertTaskSuccess(":apiCheck") + } + } } diff --git a/src/functionalTest/resources/examples/classes/AnotherBuildConfig.klib.web.dump b/src/functionalTest/resources/examples/classes/AnotherBuildConfig.klib.web.dump new file mode 100644 index 00000000..f00be63e --- /dev/null +++ b/src/functionalTest/resources/examples/classes/AnotherBuildConfig.klib.web.dump @@ -0,0 +1,17 @@ +// Merged KLib ABI Dump +// Targets: [androidNativeArm32, androidNativeArm64, androidNativeX64, androidNativeX86, js, linuxArm64, linuxX64, mingwX64, wasmJs, wasmWasi] +// Alias: native => [androidNativeArm32, androidNativeArm64, androidNativeX64, androidNativeX86, linuxArm64, linuxX64, mingwX64] +// Alias: androidNative => [androidNativeArm32, androidNativeArm64, androidNativeX64, androidNativeX86] +// Alias: linux => [linuxArm64, linuxX64] +// Rendering settings: +// - Signature version: 2 +// - Show manifest properties: false +// - Show declarations: true + +// Library unique name: +final class org.different.pack/BuildConfig { // org.different.pack/BuildConfig|null[0] + constructor () // org.different.pack/BuildConfig.|(){}[0] + final fun f1(): kotlin/Int // org.different.pack/BuildConfig.f1|f1(){}[0] + final val p1 // org.different.pack/BuildConfig.p1|{}p1[0] + final fun (): kotlin/Int // org.different.pack/BuildConfig.p1.|(){}[0] +} diff --git a/src/functionalTest/resources/examples/gradle/configuration/nonNativeKlibTargets/targets.gradle.kts b/src/functionalTest/resources/examples/gradle/configuration/nonNativeKlibTargets/targets.gradle.kts new file mode 100644 index 00000000..f6030484 --- /dev/null +++ b/src/functionalTest/resources/examples/gradle/configuration/nonNativeKlibTargets/targets.gradle.kts @@ -0,0 +1,10 @@ +/* + * Copyright 2016-2024 JetBrains s.r.o. + * Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file. + */ + +kotlin { + wasmWasi() + wasmJs() + js() +} diff --git a/src/functionalTest/resources/examples/gradle/configuration/signatures/invalid.gradle.kts b/src/functionalTest/resources/examples/gradle/configuration/signatures/invalid.gradle.kts index 1576719d..eafc6ccc 100644 --- a/src/functionalTest/resources/examples/gradle/configuration/signatures/invalid.gradle.kts +++ b/src/functionalTest/resources/examples/gradle/configuration/signatures/invalid.gradle.kts @@ -4,5 +4,7 @@ */ configure { - klibSignatureVersion = 100500 + klib { + signatureVersion = 100500 + } } From 5ba0dbe6e7337d6ae0b0af2887634dca434a2af6 Mon Sep 17 00:00:00 2001 From: Filipp Zhinkin Date: Mon, 5 Feb 2024 11:50:17 +0100 Subject: [PATCH 13/53] Described a klib validation pipeline, fixed task names, improved error messages --- .../validation/test/KLibVerificationTests.kt | 6 +- .../BinaryCompatibilityValidatorPlugin.kt | 106 ++++++++++++------ src/main/kotlin/KotlinApiBuildTask.kt | 2 +- src/main/kotlin/KotlinApiCompareTask.kt | 2 +- src/main/kotlin/KotlinKlibAbiBuildTask.kt | 6 +- ...otlinKlibExtractSupportedTargetsAbiTask.kt | 2 +- ...linKlibInferAbiForUnsupportedTargetTask.kt | 10 +- src/main/kotlin/KotlinKlibMergeAbiTask.kt | 2 +- 8 files changed, 87 insertions(+), 49 deletions(-) diff --git a/src/functionalTest/kotlin/kotlinx/validation/test/KLibVerificationTests.kt b/src/functionalTest/kotlin/kotlinx/validation/test/KLibVerificationTests.kt index 7e8d2ec6..7ef6f6f0 100644 --- a/src/functionalTest/kotlin/kotlinx/validation/test/KLibVerificationTests.kt +++ b/src/functionalTest/kotlin/kotlinx/validation/test/KLibVerificationTests.kt @@ -474,7 +474,7 @@ internal class KLibVerificationTests : BaseKotlinGradleTest() { } runner.buildAndFail().apply { - assertTaskFailure(":klibApiPrepareAbiForValidation") + assertTaskFailure(":klibApiExtractForValidation") } } @@ -527,7 +527,7 @@ internal class KLibVerificationTests : BaseKotlinGradleTest() { } runner.buildAndFail().apply { - assertTaskFailure(":linuxArm64ApiInferAbiDump") + assertTaskFailure(":linuxArm64ApiInfer") Assertions.assertThat(output).contains( "The target linuxArm64 is not supported by the host compiler " + "and there are no targets similar to linuxArm64 to infer a dump from it." @@ -590,7 +590,7 @@ internal class KLibVerificationTests : BaseKotlinGradleTest() { runner.buildAndFail().apply { Assertions.assertThat(output).contains( - "KLib ABI dump/validation requires at least enabled klib target, but none were found." + "KLib ABI dump/validation requires at least one enabled klib target, but none were found." ) } } diff --git a/src/main/kotlin/BinaryCompatibilityValidatorPlugin.kt b/src/main/kotlin/BinaryCompatibilityValidatorPlugin.kt index 0ed18cd4..7358a1e9 100644 --- a/src/main/kotlin/BinaryCompatibilityValidatorPlugin.kt +++ b/src/main/kotlin/BinaryCompatibilityValidatorPlugin.kt @@ -1,5 +1,5 @@ /* - * Copyright 2016-2023 JetBrains s.r.o. + * Copyright 2016-2024 JetBrains s.r.o. * Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file. */ @@ -45,7 +45,8 @@ public class BinaryCompatibilityValidatorPlugin : Plugin { } catch (e: NoClassDefFoundError) { throw IllegalStateException( "KLib validation is not available. " + - "Make sure the project use at least Kotlin 1.9.20.", e + "Make sure the project use at least Kotlin 1.9.20 or disable KLib validation " + + "by setting apiValidation.klib.enabled to false", e ) } } @@ -337,6 +338,34 @@ private const val BANNED_TARGETS_PROPERTY_NAME = "binary.compatibility.validator private const val KLIB_DUMPS_DIRECTORY = "klib" private const val KLIB_INFERRED_DUMPS_DIRECTORY = "klib-all" +/** + * KLib ABI dump validation and dump extraction consists of multiple steps that extracts and transforms dumps for klibs. + * The main entry point for validation is the `klibApiCheck` task, which is a dependency for `apiCheck` task, and the + * main entry point for dump extraction is the `klibApiDump` task, which is a dependency for `apiDump` task. + * + * Both `klibApiCheck` and `klibApiDump` depends on multiple other tasks that extracts dumps for compiled klibs, + * generate (in case of dumping) dumps for targets that are not supported by the host compiler and don't have compiled + * klibs, and, finally, merges individual dumps into a single merged KLib ABI dump file that is then either stored + * inside a project's api dir (in case of dumping), or compared against a golden value (in case of validation). + * + * Here's how different tasks depend on each other: + * - `klibApiCheck` ([KotlinApiCompareTask]) depends on `klibApiMerge` and `klibApiExtractForValidation` tasks; + * this task itself does not perform anything except comparing the result of a merge, with a preprocessed golden value; + * - `klibApiDump` ([CopyFile]) depends on `klibApiMergeInferred` and simply moves the merged ABI dump into a configured + * api directory within a project; + * - `klibApiMerge` and `klibApiMergeInferred` are both [KotlinKlibMergeAbiTask] instances merging multiple individual + * KLib ABI dumps into a single merged dump file; these tasks differs only by their dependencies and input dump files + * to merge: `klibApiMerge` uses only dump files extracted from compiled klibs, these dumps are extracted using + * multiple `ApiBuild` tasks ([KotlinKlibAbiBuildTask]); `klibApiMergeInferred` depends on the same tasks + * as `klibApiMerge`, but also have additional dependencies responsible for inferring KLib ABI dumps for targets not + * supported by the host compiler (`ApiInfer` tasks + * instantiating [KotlinKlibInferAbiForUnsupportedTargetTask]); + * - `klibApiExtractForValidation` ([KotlinKlibExtractSupportedTargetsAbiTask]) is responsible for filtering out all + * currently unsupported targets from the golden image, so that it could be compared with a merged dump; + * - each `ApiInfer` task depends on all regular `ApiBuild` tasks; it searches for targets + * that are suitable to ABI dump inference, merges them and then mixes in all declarations specific to the unsupported + * target that were extracted from the golden image. + */ @ExperimentalBCVApi private class KlibValidationPipelineBuilder( val dirConfig: Provider?, @@ -345,12 +374,13 @@ private class KlibValidationPipelineBuilder( lateinit var intermediateFilesConfig: Provider fun configureTasks(project: Project, commonApiDump: TaskProvider, commonApiCheck: TaskProvider) { - // In the intermediate phase of Klib dump generation there are always multiple targets, thus we need - // target-based directory tree. + // In the intermediate phase of KLib dump generation, there are always multiple targets; thus we need + // a target-based directory tree. intermediateFilesConfig = project.provider { DirConfig.TARGET_DIR } val klibApiDirConfig = dirConfig?.map { TargetConfig(project, extension, KLIB_DUMPS_DIRECTORY, dirConfig) } val klibDumpConfig = TargetConfig(project, extension, KLIB_DUMPS_DIRECTORY, intermediateFilesConfig) - val klibDumpAllConfig = TargetConfig(project, extension, KLIB_INFERRED_DUMPS_DIRECTORY, intermediateFilesConfig) + val klibInferDumpConfig = + TargetConfig(project, extension, KLIB_INFERRED_DUMPS_DIRECTORY, intermediateFilesConfig) val projectDir = project.projectDir val klibApiDir = klibApiDirConfig?.map { @@ -358,25 +388,25 @@ private class KlibValidationPipelineBuilder( }!! val projectBuildDir = project.layout.buildDirectory.asFile.get() val klibMergeDir = projectBuildDir.resolve(klibDumpConfig.apiDir.get()) - val klibMergeAllDir = projectBuildDir.resolve(klibDumpAllConfig.apiDir.get()) - val klibExtractedFileDir = klibMergeAllDir.resolve("extracted") + val klibMergeInferredDir = projectBuildDir.resolve(klibInferDumpConfig.apiDir.get()) + val klibExtractedFileDir = klibMergeInferredDir.resolve("extracted") val klibMerge = project.mergeKlibsUmbrellaTask(klibDumpConfig, klibMergeDir) - val klibMergeAll = project.mergeAllKlibsUmbrellaTask(klibDumpConfig, klibMergeAllDir) - val klibDump = project.dumpKlibsTask(klibDumpConfig, klibApiDir, klibMergeAllDir) + val klibMergeInferred = project.mergeInferredKlibsUmbrellaTask(klibDumpConfig, klibMergeInferredDir) + val klibDump = project.dumpKlibsTask(klibDumpConfig, klibApiDir, klibMergeInferredDir) val klibExtractAbiForSupportedTargets = project.extractAbi(klibDumpConfig, klibApiDir, klibExtractedFileDir) val klibCheck = project.checkKlibsTask(klibDumpConfig, project.provider { klibExtractedFileDir }, klibMergeDir) commonApiDump.configure { it.dependsOn(klibDump) } commonApiCheck.configure { it.dependsOn(klibCheck) } - klibDump.configure { it.dependsOn(klibMergeAll) } + klibDump.configure { it.dependsOn(klibMergeInferred) } klibCheck.configure { it.dependsOn(klibExtractAbiForSupportedTargets) it.dependsOn(klibMerge) } - project.configureTargets(klibApiDir, klibMerge, klibMergeAll) + project.configureTargets(klibApiDir, klibMerge, klibMergeInferred) } private fun Project.checkKlibsTask( @@ -386,7 +416,7 @@ private class KlibValidationPipelineBuilder( ) = project.task(klibDumpConfig.apiTaskName("Check")) { isEnabled = klibAbiCheckEnabled(project.name, extension) group = "verification" - description = "Checks signatures of public klib ABI against the golden value in ABI folder for " + + description = "Checks signatures of a public KLib ABI against the golden value in ABI folder for " + project.name projectApiFile = klibApiDir.get().resolve(klibDumpFileName) generatedApiFile = klibMergeDir.resolve(klibDumpFileName) @@ -398,7 +428,7 @@ private class KlibValidationPipelineBuilder( klibMergeDir: File ) = project.task(klibDumpConfig.apiTaskName("Dump")) { isEnabled = klibAbiCheckEnabled(project.name, extension) - description = "Syncs klib ABI dump from build dir to ${klibDumpConfig.apiDir} dir for ${project.name}" + description = "Syncs a KLib ABI dump from a build dir to the ${klibDumpConfig.apiDir} dir for ${project.name}" group = "other" from = klibMergeDir.resolve(klibDumpFileName) to = klibApiDir.get().resolve(klibDumpFileName) @@ -408,9 +438,13 @@ private class KlibValidationPipelineBuilder( klibDumpConfig: TargetConfig, klibApiDir: Provider, klibOutputDir: File - ) = project.task(klibDumpConfig.apiTaskName("PrepareAbiForValidation")) { + ) = project.task( + klibDumpConfig.apiTaskName("ExtractForValidation") + ) + { isEnabled = klibAbiCheckEnabled(project.name, extension) - description = "Prepare a reference ABI file by removing all unsupported targets from it" + description = "Prepare a reference KLib ABI file by removing all unsupported targets from " + + "the golden file stored in the project" group = "other" strictValidation = extension.klib.strictValidation groupTargetNames = extension.klib.useTargetGroupAliases @@ -419,17 +453,17 @@ private class KlibValidationPipelineBuilder( outputAbiFile = klibOutputDir.resolve(klibDumpFileName) } - private fun Project.mergeAllKlibsUmbrellaTask( + private fun Project.mergeInferredKlibsUmbrellaTask( klibDumpConfig: TargetConfig, klibMergeDir: File, ) = project.task( - klibDumpConfig.apiTaskName("MergeAll") + klibDumpConfig.apiTaskName("MergeInferred") ) { isEnabled = klibAbiCheckEnabled(project.name, extension) - description = "Merges multiple klib ABI dump files generated for " + - "different targets (including files substituting dumps for unsupported target) " + - "into a single multi-target dump" + description = "Merges multiple KLib ABI dump files generated for " + + "different targets (including inferred dumps for unsupported targets) " + + "into a single merged KLib ABI dump" dumpFileName = klibDumpFileName mergedFile = klibMergeDir.resolve(klibDumpFileName) groupTargetNames = extension.klib.useTargetGroupAliases @@ -440,8 +474,8 @@ private class KlibValidationPipelineBuilder( klibMergeDir: File ) = project.task(klibDumpConfig.apiTaskName("Merge")) { isEnabled = klibAbiCheckEnabled(project.name, extension) - description = "Merges multiple klib ABI dump files generated for " + - "different targets into a single multi-target dump" + description = "Merges multiple KLib ABI dump files generated for " + + "different targets into a single merged KLib ABI dump" dumpFileName = klibDumpFileName mergedFile = klibMergeDir.resolve(klibDumpFileName) groupTargetNames = extension.klib.useTargetGroupAliases @@ -463,7 +497,7 @@ private class KlibValidationPipelineBuilder( fun Project.configureTargets( klibApiDir: Provider, mergeTask: TaskProvider, - mergeFakeTask: TaskProvider + mergeInferredTask: TaskProvider ) { val kotlin = project.kotlinMultiplatform @@ -479,6 +513,7 @@ private class KlibValidationPipelineBuilder( val apiBuildDir = targetConfig.apiDir.map { project.layout.buildDirectory.asFile.get().resolve(it) }.get() val targetSupported = targetName in supportedTargets.get() + // If a target is supported, the workflow is simple: create a dump, then merge it along with other dumps. if (targetSupported) { mainCompilations.all { val buildTargetAbi = configureKlibCompilation( @@ -489,20 +524,23 @@ private class KlibValidationPipelineBuilder( it.addInput(targetName, apiBuildDir) it.dependsOn(buildTargetAbi) } - mergeFakeTask.configure { + mergeInferredTask.configure { it.addInput(targetName, apiBuildDir) it.dependsOn(buildTargetAbi) } } return@configureEach } - + // If the target is unsupported, the regular merge task will only depend on a task complaining about + // the target being unsupported. val unsupportedTargetStub = mergeDependencyForUnsupportedTarget(targetConfig) mergeTask.configure { it.dependsOn(unsupportedTargetStub) } + // The actual merge will happen here, where we'll try to infer a dump for the unsupported target and merge + // it with other supported target dumps. val proxy = unsupportedTargetDumpProxy(klibApiDir, targetConfig, apiBuildDir, supportedTargets.get()) - mergeFakeTask.configure { + mergeInferredTask.configure { it.addInput(targetName, apiBuildDir) it.dependsOn(proxy) } @@ -511,7 +549,7 @@ private class KlibValidationPipelineBuilder( it.doFirst { if (supportedTargets.get().isEmpty()) { throw IllegalStateException( - "KLib ABI dump/validation requires at least enabled klib target, but none were found." + "KLib ABI dump/validation requires at least one enabled klib target, but none were found." ) } } @@ -519,7 +557,7 @@ private class KlibValidationPipelineBuilder( } private fun Project.supportedTargets(): Provider> { - val banned = bannedTargets() + val banned = bannedTargets() // for testing only return project.provider { val hm = HostManager() project.kotlinMultiplatform.targets.matching { it.emitsKlib } @@ -554,7 +592,7 @@ private class KlibValidationPipelineBuilder( extension ) && compilation.allKotlinSourceSets.any { it.kotlin.srcDirs.any { it.exists() } } // 'group' is not specified deliberately, so it will be hidden from ./gradlew tasks - description = "Builds Kotlin KLib ABI for 'main' compilations of $projectName. " + + description = "Builds Kotlin KLib ABI dump for 'main' compilations of $projectName. " + "Complementary task and shouldn't be called manually" klibFile = project.files(project.provider { compilation.output.classesDirs }) compilationDependencies = project.files(project.provider { compilation.compileDependencyFiles }) @@ -570,8 +608,8 @@ private class KlibValidationPipelineBuilder( doLast { logger.warn( - "Target ${targetConfig.targetName} is not supported by the host compiler and the " + - "KLib ABI dump could not be generated for it." + "Target ${targetConfig.targetName} is not supported by the host compiler and a " + + "KLib ABI dump could not be directly generated for it." ) } } @@ -583,10 +621,10 @@ private class KlibValidationPipelineBuilder( supportedTargets: Set ): TaskProvider { val targetName = targetConfig.targetName!! - return project.task(targetConfig.apiTaskName("InferAbiDump")) { + return project.task(targetConfig.apiTaskName("Infer")) { isEnabled = klibAbiCheckEnabled(project.name, extension) - description = "Try to replace the dump for unsupported target $targetName with the dump " + - "generated for one of the supported targets." + description = "Try to infer the dump for unsupported target $targetName using dumps " + + "generated for supported targets." group = "other" this.supportedTargets = supportedTargets inputImageFile = klibApiDir.get().resolve(klibDumpFileName) diff --git a/src/main/kotlin/KotlinApiBuildTask.kt b/src/main/kotlin/KotlinApiBuildTask.kt index 04f8deb8..21f22ce5 100644 --- a/src/main/kotlin/KotlinApiBuildTask.kt +++ b/src/main/kotlin/KotlinApiBuildTask.kt @@ -1,5 +1,5 @@ /* - * Copyright 2016-2020 JetBrains s.r.o. + * Copyright 2016-2024 JetBrains s.r.o. * Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file. */ diff --git a/src/main/kotlin/KotlinApiCompareTask.kt b/src/main/kotlin/KotlinApiCompareTask.kt index 643d6d04..88e693ef 100644 --- a/src/main/kotlin/KotlinApiCompareTask.kt +++ b/src/main/kotlin/KotlinApiCompareTask.kt @@ -1,5 +1,5 @@ /* - * Copyright 2016-2020 JetBrains s.r.o. + * Copyright 2016-2024 JetBrains s.r.o. * Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file. */ diff --git a/src/main/kotlin/KotlinKlibAbiBuildTask.kt b/src/main/kotlin/KotlinKlibAbiBuildTask.kt index bc24ceda..c9959d0d 100644 --- a/src/main/kotlin/KotlinKlibAbiBuildTask.kt +++ b/src/main/kotlin/KotlinKlibAbiBuildTask.kt @@ -1,5 +1,5 @@ /* - * Copyright 2016-2023 JetBrains s.r.o. + * Copyright 2016-2024 JetBrains s.r.o. * Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file. */ @@ -68,7 +68,7 @@ public abstract class KotlinKlibAbiBuildTask : BuildTaskBase() { val parsedAbi = try { LibraryAbiReader.readAbiInfo(klibFile.singleFile, filters) } catch (e: Exception) { - throw IllegalStateException("Can't read a KLib: ${klibFile.singleFile}", e) + throw IllegalStateException("Can't read a klib: ${klibFile.singleFile}", e) } val supportedVersions = parsedAbi.signatureVersions.asSequence() @@ -83,7 +83,7 @@ public abstract class KotlinKlibAbiBuildTask : BuildTaskBase() { AbiSignatureVersion.resolveByVersionNumber(signatureVersion!!) } else { supportedVersions.maxByOrNull(AbiSignatureVersion::versionNumber) - ?: throw IllegalStateException("Can't choose abiSignatureVersion") + ?: throw IllegalStateException("Can't choose signatureVersion") } outputApiFile.bufferedWriter().use { diff --git a/src/main/kotlin/KotlinKlibExtractSupportedTargetsAbiTask.kt b/src/main/kotlin/KotlinKlibExtractSupportedTargetsAbiTask.kt index 12c03708..822f0ed4 100644 --- a/src/main/kotlin/KotlinKlibExtractSupportedTargetsAbiTask.kt +++ b/src/main/kotlin/KotlinKlibExtractSupportedTargetsAbiTask.kt @@ -61,7 +61,7 @@ public abstract class KotlinKlibExtractSupportedTargetsAbiTask : DefaultTask() { if (targetsToRemove.isNotEmpty() && strictValidation) { throw IllegalStateException( "Validation could not be performed as some targets are not available " + - "and the strictValidation mode was enabled" + "and the strictValidation mode was enabled." ) } for (target in targetsToRemove) { diff --git a/src/main/kotlin/KotlinKlibInferAbiForUnsupportedTargetTask.kt b/src/main/kotlin/KotlinKlibInferAbiForUnsupportedTargetTask.kt index 66e5ccf3..37983897 100644 --- a/src/main/kotlin/KotlinKlibInferAbiForUnsupportedTargetTask.kt +++ b/src/main/kotlin/KotlinKlibInferAbiForUnsupportedTargetTask.kt @@ -1,5 +1,5 @@ /* - * Copyright 2016-2023 JetBrains s.r.o. + * Copyright 2016-2024 JetBrains s.r.o. * Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file. */ @@ -105,10 +105,10 @@ public abstract class KotlinKlibInferAbiForUnsupportedTargetTask : DefaultTask() } logger.warn( - "An ABI dump for target $unsupportedTarget was inferred from the ABI generated for target " + - "[${matchingTargets.joinToString(",")}] " + - "as the former target is not supported by the host compiler. " + - "Inferred dump may not reflect actual ABI for the target $unsupportedTarget. " + + "An ABI dump for target $unsupportedTarget was inferred from the ABI generated for the following targets " + + "as the former target is not supported by the host compiler: " + + "[${matchingTargets.joinToString(",")}]. " + + "Inferred dump may not reflect an actual ABI for the target $unsupportedTarget. " + "It is recommended to regenerate the dump on the host supporting all required compilation target." ) } diff --git a/src/main/kotlin/KotlinKlibMergeAbiTask.kt b/src/main/kotlin/KotlinKlibMergeAbiTask.kt index 81a13dca..ff3fd4b8 100644 --- a/src/main/kotlin/KotlinKlibMergeAbiTask.kt +++ b/src/main/kotlin/KotlinKlibMergeAbiTask.kt @@ -1,5 +1,5 @@ /* - * Copyright 2016-2023 JetBrains s.r.o. + * Copyright 2016-2024 JetBrains s.r.o. * Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file. */ From 9d9f1e741ac8648509f09309749c1f47390391d2 Mon Sep 17 00:00:00 2001 From: Filipp Zhinkin Date: Fri, 9 Feb 2024 11:54:01 +0100 Subject: [PATCH 14/53] Fixed klib merge task's dependencies Input klib dump files were not tracked as merger dependencies, so the merged dump was not updated when apiDump was re-executed after sources update --- .../validation/test/KLibVerificationTests.kt | 100 ++++++++++++++++++ .../AnotherBuildConfigModified.klib.dump | 17 +++ .../classes/AnotherBuildConfigModified.kt | 14 +++ src/main/kotlin/KotlinKlibMergeAbiTask.kt | 6 ++ 4 files changed, 137 insertions(+) create mode 100644 src/functionalTest/resources/examples/classes/AnotherBuildConfigModified.klib.dump create mode 100644 src/functionalTest/resources/examples/classes/AnotherBuildConfigModified.kt diff --git a/src/functionalTest/kotlin/kotlinx/validation/test/KLibVerificationTests.kt b/src/functionalTest/kotlin/kotlinx/validation/test/KLibVerificationTests.kt index 7ef6f6f0..65d85088 100644 --- a/src/functionalTest/kotlin/kotlinx/validation/test/KLibVerificationTests.kt +++ b/src/functionalTest/kotlin/kotlinx/validation/test/KLibVerificationTests.kt @@ -15,6 +15,8 @@ import org.jetbrains.kotlin.konan.target.HostManager import org.jetbrains.kotlin.konan.target.KonanTarget import org.junit.Assume import org.junit.Test +import java.io.File +import java.nio.file.Files import kotlin.test.assertTrue internal const val BANNED_TARGETS_PROPERTY_NAME = "binary.compatibility.validator.klib.targets.blacklist.for.testing" @@ -718,4 +720,102 @@ internal class KLibVerificationTests : BaseKotlinGradleTest() { assertTaskSuccess(":apiCheck") } } + + @Test + fun `check dump is updated on added declaration`() { + val runner = test { + settingsGradleKts { + resolve("/examples/gradle/settings/settings-name-testproject.gradle.kts") + } + buildGradleKts { + resolve("/examples/gradle/base/withNativePlugin.gradle.kts") + } + kotlin("AnotherBuildConfig.kt", "commonMain") { + resolve("/examples/classes/AnotherBuildConfig.kt") + } + runner { + arguments.add(":apiDump") + } + } + checkKlibDump(runner.build(), "/examples/classes/AnotherBuildConfig.klib.dump") + + // Update the source file by adding a declaration + val updatedSourceFile = File(this::class.java.getResource( + "/examples/classes/AnotherBuildConfigModified.kt")!!.toURI() + ) + val existingSource = runner.projectDir.resolve( + "src/commonMain/kotlin/AnotherBuildConfig.kt" + ) + Files.write(existingSource.toPath(), updatedSourceFile.readBytes()) + + checkKlibDump(runner.build(), "/examples/classes/AnotherBuildConfigModified.klib.dump") + } + + @Test + fun `check dump is updated on a declaration added to some source sets`() { + val runner = test { + settingsGradleKts { + resolve("/examples/gradle/settings/settings-name-testproject.gradle.kts") + } + buildGradleKts { + resolve("/examples/gradle/base/withNativePlugin.gradle.kts") + } + kotlin("AnotherBuildConfig.kt", "commonMain") { + resolve("/examples/classes/AnotherBuildConfig.kt") + } + runner { + arguments.add(":apiDump") + } + } + checkKlibDump(runner.build(), "/examples/classes/AnotherBuildConfig.klib.dump") + + // Update the source file by adding a declaration + val updatedSourceFile = File(this::class.java.getResource( + "/examples/classes/AnotherBuildConfigLinuxArm64.kt")!!.toURI() + ) + val existingSource = runner.projectDir.resolve( + "src/linuxArm64Main/kotlin/AnotherBuildConfigLinuxArm64.kt" + ) + existingSource.parentFile.mkdirs() + Files.write(existingSource.toPath(), updatedSourceFile.readBytes()) + + checkKlibDump(runner.build(), "/examples/classes/AnotherBuildConfigLinuxArm64Extra.klib.dump") + } + + @Test + fun `re-validate dump after sources updated`() { + val runner = test { + settingsGradleKts { + resolve("/examples/gradle/settings/settings-name-testproject.gradle.kts") + } + buildGradleKts { + resolve("/examples/gradle/base/withNativePlugin.gradle.kts") + } + kotlin("AnotherBuildConfig.kt", "commonMain") { + resolve("/examples/classes/AnotherBuildConfig.kt") + } + abiFile(projectName = "testproject") { + resolve("/examples/classes/AnotherBuildConfig.klib.dump") + } + runner { + arguments.add(":apiCheck") + } + } + runner.build().apply { + assertTaskSuccess(":apiCheck") + } + + // Update the source file by adding a declaration + val updatedSourceFile = File(this::class.java.getResource( + "/examples/classes/AnotherBuildConfigModified.kt")!!.toURI() + ) + val existingSource = runner.projectDir.resolve( + "src/commonMain/kotlin/AnotherBuildConfig.kt" + ) + Files.write(existingSource.toPath(), updatedSourceFile.readBytes()) + + runner.buildAndFail().apply { + assertTaskFailure(":klibApiCheck") + } + } } diff --git a/src/functionalTest/resources/examples/classes/AnotherBuildConfigModified.klib.dump b/src/functionalTest/resources/examples/classes/AnotherBuildConfigModified.klib.dump new file mode 100644 index 00000000..383fb7e0 --- /dev/null +++ b/src/functionalTest/resources/examples/classes/AnotherBuildConfigModified.klib.dump @@ -0,0 +1,17 @@ +// Merged KLib ABI Dump +// Targets: [androidNativeArm32, androidNativeArm64, androidNativeX64, androidNativeX86, linuxArm64, linuxX64, mingwX64] +// Alias: androidNative => [androidNativeArm32, androidNativeArm64, androidNativeX64, androidNativeX86] +// Alias: linux => [linuxArm64, linuxX64] +// Rendering settings: +// - Signature version: 2 +// - Show manifest properties: false +// - Show declarations: true + +// Library unique name: +final class org.different.pack/BuildConfig { // org.different.pack/BuildConfig|null[0] + constructor () // org.different.pack/BuildConfig.|(){}[0] + final fun f1(): kotlin/Int // org.different.pack/BuildConfig.f1|f1(){}[0] + final fun f2(): kotlin/Int // org.different.pack/BuildConfig.f2|f2(){}[0] + final val p1 // org.different.pack/BuildConfig.p1|{}p1[0] + final fun (): kotlin/Int // org.different.pack/BuildConfig.p1.|(){}[0] +} diff --git a/src/functionalTest/resources/examples/classes/AnotherBuildConfigModified.kt b/src/functionalTest/resources/examples/classes/AnotherBuildConfigModified.kt new file mode 100644 index 00000000..8165117b --- /dev/null +++ b/src/functionalTest/resources/examples/classes/AnotherBuildConfigModified.kt @@ -0,0 +1,14 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. + * Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file. + */ + +package org.different.pack + +public class BuildConfig { + public val p1 = 1 + + public fun f1() = p1 + + public fun f2() = p1 +} diff --git a/src/main/kotlin/KotlinKlibMergeAbiTask.kt b/src/main/kotlin/KotlinKlibMergeAbiTask.kt index ff3fd4b8..b8c766af 100644 --- a/src/main/kotlin/KotlinKlibMergeAbiTask.kt +++ b/src/main/kotlin/KotlinKlibMergeAbiTask.kt @@ -29,6 +29,12 @@ public abstract class KotlinKlibMergeAbiTask : DefaultTask() { public val targets: Set get() = targetToFile.keys + // Required to enforce task rerun on klibs update + @Suppress("UNUSED") + @get:InputFiles + internal val inputDumps: Collection + get() = targetToFile.values + /** * A path to a resulting merged dump. */ From cf65ddf56fe4ae8de34ce231d3b35aba062087ef Mon Sep 17 00:00:00 2001 From: Filipp Zhinkin Date: Fri, 9 Feb 2024 12:29:16 +0100 Subject: [PATCH 15/53] Added shortcuts to reduce the amount of boilerplate code in the klib test --- .../validation/test/KLibVerificationTests.kt | 505 +++++------------- 1 file changed, 137 insertions(+), 368 deletions(-) diff --git a/src/functionalTest/kotlin/kotlinx/validation/test/KLibVerificationTests.kt b/src/functionalTest/kotlin/kotlinx/validation/test/KLibVerificationTests.kt index 65d85088..6c67c656 100644 --- a/src/functionalTest/kotlin/kotlinx/validation/test/KLibVerificationTests.kt +++ b/src/functionalTest/kotlin/kotlinx/validation/test/KLibVerificationTests.kt @@ -17,6 +17,7 @@ import org.junit.Assume import org.junit.Test import java.io.File import java.nio.file.Files +import java.nio.file.Paths import kotlin.test.assertTrue internal const val BANNED_TARGETS_PROPERTY_NAME = "binary.compatibility.validator.klib.targets.blacklist.for.testing" @@ -38,21 +39,45 @@ private fun KLibVerificationTests.checkKlibDump( } internal class KLibVerificationTests : BaseKotlinGradleTest() { + private fun BaseKotlinScope.baseProjectSetting() { + settingsGradleKts { + resolve("/examples/gradle/settings/settings-name-testproject.gradle.kts") + } + buildGradleKts { + resolve("/examples/gradle/base/withNativePlugin.gradle.kts") + } + } + private fun BaseKotlinScope.additionalBuildConfig(config: String) { + buildGradleKts { + resolve(config) + } + } + private fun BaseKotlinScope.addToSrcSet(pathTestFile: String, sourceSet: String = "commonMain") { + val fileName = Paths.get(pathTestFile).fileName.toString() + kotlin(fileName, sourceSet) { + resolve(pathTestFile) + } + } + private fun BaseKotlinScope.runApiCheck() { + runner { + arguments.add(":apiCheck") + } + } + private fun BaseKotlinScope.runApiDump() { + runner { + arguments.add(":apiDump") + } + } + private fun assertApiCheckPassed(buildResult: BuildResult) { + buildResult.assertTaskSuccess(":apiCheck") + } + @Test fun `apiDump for native targets`() { val runner = test { - settingsGradleKts { - resolve("/examples/gradle/settings/settings-name-testproject.gradle.kts") - } - buildGradleKts { - resolve("/examples/gradle/base/withNativePlugin.gradle.kts") - } - kotlin("TopLevelDeclarations.kt", "commonMain") { - resolve("/examples/classes/TopLevelDeclarations.kt") - } - runner { - arguments.add(":apiDump") - } + baseProjectSetting() + addToSrcSet("/examples/classes/TopLevelDeclarations.kt") + runApiDump() } checkKlibDump(runner.build(), "/examples/classes/TopLevelDeclarations.klib.with.linux.dump") @@ -61,54 +86,32 @@ internal class KLibVerificationTests : BaseKotlinGradleTest() { @Test fun `apiCheck for native targets`() { val runner = test { - settingsGradleKts { - resolve("/examples/gradle/settings/settings-name-testproject.gradle.kts") - } - buildGradleKts { - resolve("/examples/gradle/base/withNativePlugin.gradle.kts") - } - kotlin("TopLevelDeclarations.kt", "commonMain") { - resolve("/examples/classes/TopLevelDeclarations.kt") - } + baseProjectSetting() + + addToSrcSet("/examples/classes/TopLevelDeclarations.kt") abiFile(projectName = "testproject") { resolve("/examples/classes/TopLevelDeclarations.klib.dump") } - runner { - arguments.add(":apiCheck") - } + runApiCheck() } - runner.build().apply { - assertTaskSuccess(":apiCheck") - } + assertApiCheckPassed(runner.build()) } @Test fun `apiCheck for native targets should fail when a class is not in a dump`() { val runner = test { - settingsGradleKts { - resolve("/examples/gradle/settings/settings-name-testproject.gradle.kts") - } - buildGradleKts { - resolve("/examples/gradle/base/withNativePlugin.gradle.kts") - } - kotlin("BuildConfig.kt", "commonMain") { - resolve("/examples/classes/BuildConfig.kt") - } - + baseProjectSetting() + addToSrcSet("/examples/classes/BuildConfig.kt") abiFile(projectName = "testproject") { resolve("/examples/classes/Empty.klib.dump") } - - runner { - arguments.add(":apiCheck") - } + runApiCheck() } runner.buildAndFail().apply { - Assertions.assertThat(output) .contains("+final class com.company/BuildConfig { // com.company/BuildConfig|null[0]") tasks.filter { it.path.endsWith("ApiCheck") } @@ -121,22 +124,10 @@ internal class KLibVerificationTests : BaseKotlinGradleTest() { @Test fun `apiDump should include target-specific sources`() { val runner = test { - settingsGradleKts { - resolve("/examples/gradle/settings/settings-name-testproject.gradle.kts") - } - buildGradleKts { - resolve("/examples/gradle/base/withNativePlugin.gradle.kts") - } - kotlin("AnotherBuildConfig.kt", "commonMain") { - resolve("/examples/classes/AnotherBuildConfig.kt") - } - kotlin("AnotherBuildConfigLinuxArm64.kt", "linuxArm64Main") { - resolve("/examples/classes/AnotherBuildConfigLinuxArm64.kt") - } - - runner { - arguments.add(":apiDump") - } + baseProjectSetting() + addToSrcSet("/examples/classes/AnotherBuildConfig.kt") + addToSrcSet("/examples/classes/AnotherBuildConfigLinuxArm64.kt", "linuxArm64Main") + runApiDump() } runner.build().apply { @@ -150,19 +141,10 @@ internal class KLibVerificationTests : BaseKotlinGradleTest() { @Test fun `apiDump with native targets along with JVM target`() { val runner = test { - settingsGradleKts { - resolve("/examples/gradle/settings/settings-name-testproject.gradle.kts") - } - buildGradleKts { - resolve("/examples/gradle/base/withNativePlugin.gradle.kts") - resolve("/examples/gradle/base/enableJvmInWithNativePlugin.gradle.kts") - } - kotlin("AnotherBuildConfig.kt", "commonMain") { - resolve("/examples/classes/AnotherBuildConfig.kt") - } - runner { - arguments.add(":apiDump") - } + baseProjectSetting() + additionalBuildConfig("/examples/gradle/base/enableJvmInWithNativePlugin.gradle.kts") + addToSrcSet("/examples/classes/AnotherBuildConfig.kt") + runApiDump() } runner.build().apply { @@ -179,23 +161,11 @@ internal class KLibVerificationTests : BaseKotlinGradleTest() { @Test fun `apiDump should ignore a class listed in ignoredClasses`() { val runner = test { - settingsGradleKts { - resolve("/examples/gradle/settings/settings-name-testproject.gradle.kts") - } - buildGradleKts { - resolve("/examples/gradle/base/withNativePlugin.gradle.kts") - resolve("/examples/gradle/configuration/ignoredClasses/oneValidFullyQualifiedClass.gradle.kts") - } - kotlin("BuildConfig.kt", "commonMain") { - resolve("/examples/classes/BuildConfig.kt") - } - kotlin("AnotherBuildConfig.kt", "commonMain") { - resolve("/examples/classes/AnotherBuildConfig.kt") - } - - runner { - arguments.add(":apiDump") - } + baseProjectSetting() + additionalBuildConfig("/examples/gradle/configuration/ignoredClasses/oneValidFullyQualifiedClass.gradle.kts") + addToSrcSet("/examples/classes/BuildConfig.kt") + addToSrcSet("/examples/classes/AnotherBuildConfig.kt") + runApiDump() } checkKlibDump(runner.build(), "/examples/classes/AnotherBuildConfig.klib.dump") @@ -204,20 +174,10 @@ internal class KLibVerificationTests : BaseKotlinGradleTest() { @Test fun `apiDump should succeed if a class listed in ignoredClasses is not found`() { val runner = test { - settingsGradleKts { - resolve("/examples/gradle/settings/settings-name-testproject.gradle.kts") - } - buildGradleKts { - resolve("/examples/gradle/base/withNativePlugin.gradle.kts") - resolve("/examples/gradle/configuration/ignoredClasses/oneValidFullyQualifiedClass.gradle.kts") - } - kotlin("AnotherBuildConfig.kt", "commonMain") { - resolve("/examples/classes/AnotherBuildConfig.kt") - } - - runner { - arguments.add(":apiDump") - } + baseProjectSetting() + additionalBuildConfig("/examples/gradle/configuration/ignoredClasses/oneValidFullyQualifiedClass.gradle.kts") + addToSrcSet("/examples/classes/AnotherBuildConfig.kt") + runApiDump() } checkKlibDump(runner.build(), "/examples/classes/AnotherBuildConfig.klib.dump") @@ -226,26 +186,12 @@ internal class KLibVerificationTests : BaseKotlinGradleTest() { @Test fun `apiDump should ignore all entities from a package listed in ingoredPackages`() { val runner = test { - settingsGradleKts { - resolve("/examples/gradle/settings/settings-name-testproject.gradle.kts") - } - buildGradleKts { - resolve("/examples/gradle/base/withNativePlugin.gradle.kts") - resolve("/examples/gradle/configuration/ignoredPackages/oneValidPackage.gradle.kts") - } - kotlin("BuildConfig.kt", "commonMain") { - resolve("/examples/classes/BuildConfig.kt") - } - kotlin("AnotherBuildConfig.kt", "commonMain") { - resolve("/examples/classes/AnotherBuildConfig.kt") - } - kotlin("SubPackage.kt", "commonMain") { - resolve("/examples/classes/SubPackage.kt") - } - - runner { - arguments.add(":apiDump") - } + baseProjectSetting() + additionalBuildConfig("/examples/gradle/configuration/ignoredPackages/oneValidPackage.gradle.kts") + addToSrcSet("/examples/classes/BuildConfig.kt") + addToSrcSet("/examples/classes/AnotherBuildConfig.kt") + addToSrcSet("/examples/classes/SubPackage.kt") + runApiDump() } checkKlibDump(runner.build(), "/examples/classes/AnotherBuildConfig.klib.dump") @@ -254,23 +200,11 @@ internal class KLibVerificationTests : BaseKotlinGradleTest() { @Test fun `apiDump should ignore all entities annotated with non-public markers`() { val runner = test { - settingsGradleKts { - resolve("/examples/gradle/settings/settings-name-testproject.gradle.kts") - } - buildGradleKts { - resolve("/examples/gradle/base/withNativePlugin.gradle.kts") - resolve("/examples/gradle/configuration/nonPublicMarkers/klib.gradle.kts") - } - kotlin("HiddenDeclarations.kt", "commonMain") { - resolve("/examples/classes/HiddenDeclarations.kt") - } - kotlin("NonPublicMarkers.kt", "commonMain") { - resolve("/examples/classes/NonPublicMarkers.kt") - } - - runner { - arguments.add(":apiDump") - } + baseProjectSetting() + additionalBuildConfig("/examples/gradle/configuration/nonPublicMarkers/klib.gradle.kts") + addToSrcSet("/examples/classes/HiddenDeclarations.kt") + addToSrcSet("/examples/classes/NonPublicMarkers.kt") + runApiDump() } checkKlibDump(runner.build(), "/examples/classes/HiddenDeclarations.klib.dump") @@ -279,20 +213,10 @@ internal class KLibVerificationTests : BaseKotlinGradleTest() { @Test fun `apiDump should not dump subclasses excluded via ignoredClasses`() { val runner = test { - settingsGradleKts { - resolve("/examples/gradle/settings/settings-name-testproject.gradle.kts") - } - buildGradleKts { - resolve("/examples/gradle/base/withNativePlugin.gradle.kts") - resolve("/examples/gradle/configuration/ignoreSubclasses/ignore.gradle.kts") - } - kotlin("Subclasses.kt", "commonMain") { - resolve("/examples/classes/Subclasses.kt") - } - - runner { - arguments.add(":apiDump") - } + baseProjectSetting() + additionalBuildConfig("/examples/gradle/configuration/ignoreSubclasses/ignore.gradle.kts") + addToSrcSet("/examples/classes/Subclasses.kt") + runApiDump() } checkKlibDump(runner.build(), "/examples/classes/Subclasses.klib.dump") @@ -301,48 +225,27 @@ internal class KLibVerificationTests : BaseKotlinGradleTest() { @Test fun `apiCheck for native targets using v1 signatures`() { val runner = test { - settingsGradleKts { - resolve("/examples/gradle/settings/settings-name-testproject.gradle.kts") - } - buildGradleKts { - resolve("/examples/gradle/base/withNativePlugin.gradle.kts") - resolve("/examples/gradle/configuration/signatures/v1.gradle.kts") - } - kotlin("TopLevelDeclarations.kt", "commonMain") { - resolve("/examples/classes/TopLevelDeclarations.kt") - } + baseProjectSetting() + additionalBuildConfig("/examples/gradle/configuration/signatures/v1.gradle.kts") + addToSrcSet("/examples/classes/TopLevelDeclarations.kt") abiFile(projectName = "testproject") { resolve("/examples/classes/TopLevelDeclarations.klib.v1.dump") } - runner { - arguments.add(":apiCheck") - } + runApiCheck() } - runner.build().apply { - assertTaskSuccess(":apiCheck") - } + assertApiCheckPassed(runner.build()) } @Test fun `apiDump for native targets should fail when using invalid signature version`() { val runner = test { - settingsGradleKts { - resolve("/examples/gradle/settings/settings-name-testproject.gradle.kts") - } - buildGradleKts { - resolve("/examples/gradle/base/withNativePlugin.gradle.kts") - resolve("/examples/gradle/configuration/signatures/invalid.gradle.kts") - } - kotlin("TopLevelDeclarations.kt", "commonMain") { - resolve("/examples/classes/TopLevelDeclarations.kt") - } - - runner { - arguments.add(":apiDump") - } + baseProjectSetting() + additionalBuildConfig("/examples/gradle/configuration/signatures/invalid.gradle.kts") + addToSrcSet("/examples/classes/TopLevelDeclarations.kt") + runApiDump() } runner.buildAndFail().apply { @@ -354,19 +257,10 @@ internal class KLibVerificationTests : BaseKotlinGradleTest() { fun `apiDump should work for Apple-targets`() { Assume.assumeTrue(HostManager().isEnabled(KonanTarget.MACOS_ARM64)) val runner = test { - settingsGradleKts { - resolve("/examples/gradle/settings/settings-name-testproject.gradle.kts") - } - buildGradleKts { - resolve("/examples/gradle/base/withNativePlugin.gradle.kts") - resolve("/examples/gradle/configuration/appleTargets/targets.gradle.kts") - } - kotlin("TopLevelDeclarations.kt", "commonMain") { - resolve("/examples/classes/TopLevelDeclarations.kt") - } - runner { - arguments.add(":apiDump") - } + baseProjectSetting() + additionalBuildConfig("/examples/gradle/configuration/appleTargets/targets.gradle.kts") + addToSrcSet("/examples/classes/TopLevelDeclarations.kt") + runApiDump() } checkKlibDump(runner.build(), "/examples/classes/TopLevelDeclarations.klib.all.dump") @@ -376,41 +270,23 @@ internal class KLibVerificationTests : BaseKotlinGradleTest() { fun `apiCheck should work for Apple-targets`() { Assume.assumeTrue(HostManager().isEnabled(KonanTarget.MACOS_ARM64)) val runner = test { - settingsGradleKts { - resolve("/examples/gradle/settings/settings-name-testproject.gradle.kts") - } - buildGradleKts { - resolve("/examples/gradle/base/withNativePlugin.gradle.kts") - resolve("/examples/gradle/configuration/appleTargets/targets.gradle.kts") - } - kotlin("TopLevelDeclarations.kt", "commonMain") { - resolve("/examples/classes/TopLevelDeclarations.kt") - } + baseProjectSetting() + additionalBuildConfig("/examples/gradle/configuration/appleTargets/targets.gradle.kts") + addToSrcSet("/examples/classes/TopLevelDeclarations.kt") abiFile(projectName = "testproject") { resolve("/examples/classes/TopLevelDeclarations.klib.all.dump") } - runner { - arguments.add(":apiCheck") - } + runApiCheck() } - runner.build().apply { - assertTaskSuccess(":apiCheck") - } + assertApiCheckPassed(runner.build()) } @Test fun `apiCheck should not fail if a target is not supported`() { val runner = test { - settingsGradleKts { - resolve("/examples/gradle/settings/settings-name-testproject.gradle.kts") - } - buildGradleKts { - resolve("/examples/gradle/base/withNativePlugin.gradle.kts") - } - kotlin("TopLevelDeclarations.kt", "commonMain") { - resolve("/examples/classes/TopLevelDeclarations.kt") - } + baseProjectSetting() + addToSrcSet("/examples/classes/TopLevelDeclarations.kt") abiFile(projectName = "testproject") { resolve("/examples/classes/TopLevelDeclarations.klib.dump") } @@ -420,23 +296,14 @@ internal class KLibVerificationTests : BaseKotlinGradleTest() { } } - runner.build().apply { - assertTaskSuccess(":apiCheck") - } + assertApiCheckPassed(runner.build()) } @Test fun `apiCheck should ignore unsupported targets by default`() { val runner = test { - settingsGradleKts { - resolve("/examples/gradle/settings/settings-name-testproject.gradle.kts") - } - buildGradleKts { - resolve("/examples/gradle/base/withNativePlugin.gradle.kts") - } - kotlin("TopLevelDeclarations.kt", "commonMain") { - resolve("/examples/classes/TopLevelDeclarations.kt") - } + baseProjectSetting() + addToSrcSet("/examples/classes/TopLevelDeclarations.kt") abiFile(projectName = "testproject") { // note that the regular dump is used, where linuxArm64 is presented resolve("/examples/classes/TopLevelDeclarations.klib.dump") @@ -447,24 +314,15 @@ internal class KLibVerificationTests : BaseKotlinGradleTest() { } } - runner.build().apply { - assertTaskSuccess(":apiCheck") - } + assertApiCheckPassed(runner.build()) } @Test fun `apiCheck should fail for unsupported targets with strict mode turned on`() { val runner = test { - settingsGradleKts { - resolve("/examples/gradle/settings/settings-name-testproject.gradle.kts") - } - buildGradleKts { - resolve("/examples/gradle/base/withNativePlugin.gradle.kts") - resolve("/examples/gradle/configuration/unsupported/enforce.gradle.kts") - } - kotlin("TopLevelDeclarations.kt", "commonMain") { - resolve("/examples/classes/TopLevelDeclarations.kt") - } + baseProjectSetting() + additionalBuildConfig("/examples/gradle/configuration/unsupported/enforce.gradle.kts") + addToSrcSet("/examples/classes/TopLevelDeclarations.kt") abiFile(projectName = "testproject") { // note that the regular dump is used, where linuxArm64 is presented resolve("/examples/classes/TopLevelDeclarations.klib.dump") @@ -483,18 +341,9 @@ internal class KLibVerificationTests : BaseKotlinGradleTest() { @Test fun `klibDump should infer a dump for unsupported target from similar enough target`() { val runner = test { - settingsGradleKts { - resolve("/examples/gradle/settings/settings-name-testproject.gradle.kts") - } - buildGradleKts { - resolve("/examples/gradle/base/withNativePlugin.gradle.kts") - } - kotlin("TopLevelDeclarations.kt", "commonMain") { - resolve("/examples/classes/TopLevelDeclarations.kt") - } - kotlin("AnotherBuildConfigLinuxArm64.kt", "linuxArm64Main") { - resolve("/examples/classes/AnotherBuildConfigLinuxArm64.kt") - } + baseProjectSetting() + addToSrcSet("/examples/classes/TopLevelDeclarations.kt") + addToSrcSet("/examples/classes/AnotherBuildConfigLinuxArm64.kt", "linuxArm64Main") runner { arguments.add("-P$BANNED_TARGETS_PROPERTY_NAME=linuxArm64") arguments.add(":klibApiDump") @@ -516,12 +365,8 @@ internal class KLibVerificationTests : BaseKotlinGradleTest() { buildGradleKts { resolve("/examples/gradle/base/withNativePluginAndSingleTarget.gradle.kts") } - kotlin("TopLevelDeclarations.kt", "commonMain") { - resolve("/examples/classes/TopLevelDeclarations.kt") - } - kotlin("AnotherBuildConfigLinuxArm64.kt", "linuxArm64Main") { - resolve("/examples/classes/AnotherBuildConfigLinuxArm64.kt") - } + addToSrcSet("/examples/classes/TopLevelDeclarations.kt") + addToSrcSet("/examples/classes/AnotherBuildConfigLinuxArm64.kt", "linuxArm64Main") runner { arguments.add("-P$BANNED_TARGETS_PROPERTY_NAME=linuxArm64") arguments.add(":klibApiDump") @@ -540,15 +385,8 @@ internal class KLibVerificationTests : BaseKotlinGradleTest() { @Test fun `klibDump if all klib-targets are unavailable`() { val runner = test { - settingsGradleKts { - resolve("/examples/gradle/settings/settings-name-testproject.gradle.kts") - } - buildGradleKts { - resolve("/examples/gradle/base/withNativePlugin.gradle.kts") - } - kotlin("TopLevelDeclarations.kt", "commonMain") { - resolve("/examples/classes/TopLevelDeclarations.kt") - } + baseProjectSetting() + addToSrcSet("/examples/classes/TopLevelDeclarations.kt") runner { arguments.add( "-P$BANNED_TARGETS_PROPERTY_NAME=linuxArm64,linuxX64,mingwX64," + @@ -568,15 +406,8 @@ internal class KLibVerificationTests : BaseKotlinGradleTest() { @Test fun `klibCheck if all klib-targets are unavailable`() { val runner = test { - settingsGradleKts { - resolve("/examples/gradle/settings/settings-name-testproject.gradle.kts") - } - buildGradleKts { - resolve("/examples/gradle/base/withNativePlugin.gradle.kts") - } - kotlin("TopLevelDeclarations.kt", "commonMain") { - resolve("/examples/classes/TopLevelDeclarations.kt") - } + baseProjectSetting() + addToSrcSet("/examples/classes/TopLevelDeclarations.kt") abiFile(projectName = "testproject") { // note that the regular dump is used, where linuxArm64 is presented resolve("/examples/classes/TopLevelDeclarations.klib.dump") @@ -607,9 +438,7 @@ internal class KLibVerificationTests : BaseKotlinGradleTest() { resolve("/examples/gradle/base/withNativePluginAndNoTargets.gradle.kts") resolve("/examples/gradle/configuration/grouping/clashingTargetNames.gradle.kts") } - kotlin("AnotherBuildConfig.kt", "commonMain") { - resolve("/examples/classes/AnotherBuildConfig.kt") - } + addToSrcSet("/examples/classes/AnotherBuildConfig.kt") runner { arguments.add(":klibApiDump") } @@ -631,9 +460,7 @@ internal class KLibVerificationTests : BaseKotlinGradleTest() { resolve("/examples/gradle/base/withNativePluginAndNoTargets.gradle.kts") resolve("/examples/gradle/configuration/grouping/customTargetNames.gradle.kts") } - kotlin("AnotherBuildConfig.kt", "commonMain") { - resolve("/examples/classes/AnotherBuildConfig.kt") - } + addToSrcSet("/examples/classes/AnotherBuildConfig.kt") runner { arguments.add(":klibApiDump") } @@ -648,18 +475,9 @@ internal class KLibVerificationTests : BaseKotlinGradleTest() { @Test fun `target name grouping`() { val runner = test { - settingsGradleKts { - resolve("/examples/gradle/settings/settings-name-testproject.gradle.kts") - } - buildGradleKts { - resolve("/examples/gradle/base/withNativePlugin.gradle.kts") - } - kotlin("AnotherBuildConfig.kt", "commonMain") { - resolve("/examples/classes/AnotherBuildConfig.kt") - } - kotlin("AnotherBuildConfigLinuxArm64.kt", "linuxArm64Main") { - resolve("/examples/classes/AnotherBuildConfigLinuxArm64.kt") - } + baseProjectSetting() + addToSrcSet("/examples/classes/AnotherBuildConfig.kt") + addToSrcSet("/examples/classes/AnotherBuildConfigLinuxArm64.kt", "linuxArm64Main") kotlin("AnotherBuildConfigLinuxX64.kt", "linuxX64Main") { resolve("/examples/classes/AnotherBuildConfigLinuxArm64.kt") } @@ -677,19 +495,10 @@ internal class KLibVerificationTests : BaseKotlinGradleTest() { @Test fun `apiDump should work with web targets`() { val runner = test { - settingsGradleKts { - resolve("/examples/gradle/settings/settings-name-testproject.gradle.kts") - } - buildGradleKts { - resolve("/examples/gradle/base/withNativePlugin.gradle.kts") - resolve("/examples/gradle/configuration/nonNativeKlibTargets/targets.gradle.kts") - } - kotlin("AnotherBuildConfig.kt", "commonMain") { - resolve("/examples/classes/AnotherBuildConfig.kt") - } - runner { - arguments.add(":apiDump") - } + baseProjectSetting() + additionalBuildConfig("/examples/gradle/configuration/nonNativeKlibTargets/targets.gradle.kts") + addToSrcSet("/examples/classes/AnotherBuildConfig.kt") + runApiDump() } checkKlibDump(runner.build(), "/examples/classes/AnotherBuildConfig.klib.web.dump") @@ -698,44 +507,24 @@ internal class KLibVerificationTests : BaseKotlinGradleTest() { @Test fun `apiCheck should work with web targets`() { val runner = test { - settingsGradleKts { - resolve("/examples/gradle/settings/settings-name-testproject.gradle.kts") - } - buildGradleKts { - resolve("/examples/gradle/base/withNativePlugin.gradle.kts") - resolve("/examples/gradle/configuration/nonNativeKlibTargets/targets.gradle.kts") - } - kotlin("AnotherBuildConfig.kt", "commonMain") { - resolve("/examples/classes/AnotherBuildConfig.kt") - } + baseProjectSetting() + additionalBuildConfig("/examples/gradle/configuration/nonNativeKlibTargets/targets.gradle.kts") + addToSrcSet("/examples/classes/AnotherBuildConfig.kt") abiFile(projectName = "testproject") { resolve("/examples/classes/AnotherBuildConfig.klib.web.dump") } - runner { - arguments.add(":apiCheck") - } + runApiCheck() } - runner.build().apply { - assertTaskSuccess(":apiCheck") - } + assertApiCheckPassed(runner.build()) } @Test fun `check dump is updated on added declaration`() { val runner = test { - settingsGradleKts { - resolve("/examples/gradle/settings/settings-name-testproject.gradle.kts") - } - buildGradleKts { - resolve("/examples/gradle/base/withNativePlugin.gradle.kts") - } - kotlin("AnotherBuildConfig.kt", "commonMain") { - resolve("/examples/classes/AnotherBuildConfig.kt") - } - runner { - arguments.add(":apiDump") - } + baseProjectSetting() + addToSrcSet("/examples/classes/AnotherBuildConfig.kt") + runApiDump() } checkKlibDump(runner.build(), "/examples/classes/AnotherBuildConfig.klib.dump") @@ -754,18 +543,9 @@ internal class KLibVerificationTests : BaseKotlinGradleTest() { @Test fun `check dump is updated on a declaration added to some source sets`() { val runner = test { - settingsGradleKts { - resolve("/examples/gradle/settings/settings-name-testproject.gradle.kts") - } - buildGradleKts { - resolve("/examples/gradle/base/withNativePlugin.gradle.kts") - } - kotlin("AnotherBuildConfig.kt", "commonMain") { - resolve("/examples/classes/AnotherBuildConfig.kt") - } - runner { - arguments.add(":apiDump") - } + baseProjectSetting() + addToSrcSet("/examples/classes/AnotherBuildConfig.kt") + runApiDump() } checkKlibDump(runner.build(), "/examples/classes/AnotherBuildConfig.klib.dump") @@ -785,25 +565,14 @@ internal class KLibVerificationTests : BaseKotlinGradleTest() { @Test fun `re-validate dump after sources updated`() { val runner = test { - settingsGradleKts { - resolve("/examples/gradle/settings/settings-name-testproject.gradle.kts") - } - buildGradleKts { - resolve("/examples/gradle/base/withNativePlugin.gradle.kts") - } - kotlin("AnotherBuildConfig.kt", "commonMain") { - resolve("/examples/classes/AnotherBuildConfig.kt") - } + baseProjectSetting() + addToSrcSet("/examples/classes/AnotherBuildConfig.kt") abiFile(projectName = "testproject") { resolve("/examples/classes/AnotherBuildConfig.klib.dump") } - runner { - arguments.add(":apiCheck") - } - } - runner.build().apply { - assertTaskSuccess(":apiCheck") + runApiCheck() } + assertApiCheckPassed(runner.build()) // Update the source file by adding a declaration val updatedSourceFile = File(this::class.java.getResource( From 3f93275921c23020bd62bc56da0263c774b7f3cf Mon Sep 17 00:00:00 2001 From: Filipp Zhinkin Date: Mon, 19 Feb 2024 15:06:17 +0100 Subject: [PATCH 16/53] Fixed a test --- src/test/kotlin/tests/KlibAbiMergingTest.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/test/kotlin/tests/KlibAbiMergingTest.kt b/src/test/kotlin/tests/KlibAbiMergingTest.kt index 26d32929..a156284b 100644 --- a/src/test/kotlin/tests/KlibAbiMergingTest.kt +++ b/src/test/kotlin/tests/KlibAbiMergingTest.kt @@ -83,10 +83,10 @@ class KlibAbiMergingTest { @Test fun divergingDumpFiles() { - val klib = KlibAbiDumpMerger() + val targets = mutableListOf("androidNativeArm64", "linuxArm64", "linuxX64", "tvOsX64") val random = Random(42) for (i in 0 until 10) { - val targets = mutableListOf("androidNativeArm64", "linuxArm64", "linuxX64", "tvOsX64") + val klib = KlibAbiDumpMerger() targets.shuffle(random) targets.forEach { klib.addIndividualDump(Target(it), file("/merge/diverging/$it.api")) From 6b1708741141a70b68c926058084d47155977422 Mon Sep 17 00:00:00 2001 From: Filipp Zhinkin Date: Wed, 21 Feb 2024 15:33:56 +0100 Subject: [PATCH 17/53] Store children decls as a map, get rid of a separate cache --- src/main/kotlin/klib/KlibAbiDumpFileMerger.kt | 55 +++++++++---------- 1 file changed, 25 insertions(+), 30 deletions(-) diff --git a/src/main/kotlin/klib/KlibAbiDumpFileMerger.kt b/src/main/kotlin/klib/KlibAbiDumpFileMerger.kt index ea6bb9b9..c2e70776 100644 --- a/src/main/kotlin/klib/KlibAbiDumpFileMerger.kt +++ b/src/main/kotlin/klib/KlibAbiDumpFileMerger.kt @@ -289,7 +289,7 @@ internal class KlibAbiDumpMerger { headerContent.forEach { appendable.append(it).append('\n') } - topLevelDeclaration.children.sortedWith(DeclarationsComparator).forEach { + topLevelDeclaration.children.values.sortedWith(DeclarationsComparator).forEach { it.dump(appendable, targetsMut, dumpFormat, formatter) } } @@ -386,14 +386,12 @@ internal class KlibAbiDumpMerger { */ private class DeclarationContainer(val text: String, val parent: DeclarationContainer? = null) { val targets: MutableSet = mutableSetOf() - val children: MutableList = mutableListOf() + val children: MutableMap = mutableMapOf() var delimiter: String? = null - private val childrenCache: MutableMap = mutableMapOf() fun createOrUpdateChildren(text: String, targets: Set): DeclarationContainer { - val child = childrenCache.computeIfAbsent(text) { + val child = children.computeIfAbsent(text) { val newChild = DeclarationContainer(it, this) - children.add(newChild) newChild } child.targets.addAll(targets) @@ -411,7 +409,7 @@ private class DeclarationContainer(val text: String, val parent: DeclarationCont .append('\n') } appendable.append(text).append('\n') - children.sortedWith(DeclarationsComparator).forEach { + children.values.sortedWith(DeclarationsComparator).forEach { it.dump(appendable, this.targets, dumpFormat, formatter) } if (delimiter != null) { @@ -423,16 +421,8 @@ private class DeclarationContainer(val text: String, val parent: DeclarationCont if (parent != null && !targets.contains(target)) { return } - targets.remove(target) - children.removeIf { - val shouldRemove = it.targets.contains(target) && it.targets.size == 1 - if (shouldRemove) { - childrenCache.remove(it.text) - } - shouldRemove - } - children.forEach { it.remove(target) } + mutateChildrenAndRemoveTargetless { it.remove(target) } } fun retainSpecific(target: Target, allTargets: Set) { @@ -442,8 +432,8 @@ private class DeclarationContainer(val text: String, val parent: DeclarationCont return } - children.forEach { it.retainSpecific(target, allTargets) } - children.removeIf { it.targets.isEmpty() } + mutateChildrenAndRemoveTargetless { it.retainSpecific(target, allTargets) } + if (targets == allTargets) { if (children.isEmpty()) { targets.clear() @@ -461,39 +451,44 @@ private class DeclarationContainer(val text: String, val parent: DeclarationCont targets.clear() return } - children.forEach { it.retainCommon(commonTargets) } - children.removeIf { it.targets.isEmpty() } + mutateChildrenAndRemoveTargetless { it.retainCommon(commonTargets) } } fun mergeTargetSpecific(other: DeclarationContainer) { targets.addAll(other.targets) - val newChildren = mutableListOf() other.children.forEach { otherChild -> - val child = children.find { it.text == otherChild.text } - if (child != null) { - child.mergeTargetSpecific(otherChild) - } else { - newChildren.add(otherChild) + when (val child = children[otherChild.key]) { + null -> children[otherChild.key] = otherChild.value + else -> child.mergeTargetSpecific(otherChild.value) } } children.forEach { - if (other.targets.first() !in it.targets) { - it.addTargetRecursively(other.targets.first()) + if (other.targets.first() !in it.value.targets) { + it.value.addTargetRecursively(other.targets.first()) } } - children.addAll(newChildren) } private fun addTargetRecursively(first: Target) { targets.add(first) - children.forEach { it.addTargetRecursively(first) } + children.forEach { it.value.addTargetRecursively(first) } } fun overrideTargets(targets: Set) { this.targets.clear() this.targets.addAll(targets) + children.forEach { it.value.overrideTargets(targets) } + } - children.forEach { it.overrideTargets(targets) } + private inline fun mutateChildrenAndRemoveTargetless(blockAction: (DeclarationContainer) -> Unit) { + val iterator = children.iterator() + while (iterator.hasNext()) { + val (_, child) = iterator.next() + blockAction(child) + if (child.targets.isEmpty()) { + iterator.remove() + } + } } } From f5fdacf4d2e9548144f522aa2924742ac4e473f5 Mon Sep 17 00:00:00 2001 From: Filipp Zhinkin Date: Thu, 22 Feb 2024 14:55:26 +0100 Subject: [PATCH 18/53] Fixed typos Co-authored-by: Leonid Startsev --- src/functionalTest/kotlin/kotlinx/validation/api/TestDsl.kt | 2 +- src/main/kotlin/BinaryCompatibilityValidatorPlugin.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/functionalTest/kotlin/kotlinx/validation/api/TestDsl.kt b/src/functionalTest/kotlin/kotlinx/validation/api/TestDsl.kt index 38aecb9f..42f3c914 100644 --- a/src/functionalTest/kotlin/kotlinx/validation/api/TestDsl.kt +++ b/src/functionalTest/kotlin/kotlinx/validation/api/TestDsl.kt @@ -124,7 +124,7 @@ internal fun FileContainer.apiFile(projectName: String, fn: AppendableScope.() - } /** - * Shortcut for creating a `api//.api` descriptor using [file][FileContainer.file] + * Shortcut for creating a `api//.klib.api` descriptor using [file][FileContainer.file] */ internal fun FileContainer.abiFile(projectName: String, target: String, fn: AppendableScope.() -> Unit) { dir(API_DIR) { diff --git a/src/main/kotlin/BinaryCompatibilityValidatorPlugin.kt b/src/main/kotlin/BinaryCompatibilityValidatorPlugin.kt index 7358a1e9..d8af55da 100644 --- a/src/main/kotlin/BinaryCompatibilityValidatorPlugin.kt +++ b/src/main/kotlin/BinaryCompatibilityValidatorPlugin.kt @@ -45,7 +45,7 @@ public class BinaryCompatibilityValidatorPlugin : Plugin { } catch (e: NoClassDefFoundError) { throw IllegalStateException( "KLib validation is not available. " + - "Make sure the project use at least Kotlin 1.9.20 or disable KLib validation " + + "Make sure the project uses at least Kotlin 1.9.20 or disable KLib validation " + "by setting apiValidation.klib.enabled to false", e ) } From b274e64930c39c7d76f358b76d1e3bfa7b06e7a0 Mon Sep 17 00:00:00 2001 From: Filipp Zhinkin Date: Thu, 22 Feb 2024 17:57:58 +0100 Subject: [PATCH 19/53] Opt-in experimental annotations instead of propagating it further --- src/main/kotlin/BinaryCompatibilityValidatorPlugin.kt | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src/main/kotlin/BinaryCompatibilityValidatorPlugin.kt b/src/main/kotlin/BinaryCompatibilityValidatorPlugin.kt index d8af55da..43ee5c5c 100644 --- a/src/main/kotlin/BinaryCompatibilityValidatorPlugin.kt +++ b/src/main/kotlin/BinaryCompatibilityValidatorPlugin.kt @@ -18,10 +18,9 @@ import org.jetbrains.kotlin.library.abi.LibraryAbiReader import java.io.* import kotlin.text.split +@OptIn(ExperimentalBCVApi::class, ExperimentalLibraryAbiReader::class) public class BinaryCompatibilityValidatorPlugin : Plugin { - @ExperimentalLibraryAbiReader - @ExperimentalBCVApi override fun apply(target: Project): Unit = with(target) { val extension = extensions.create("apiValidation", ApiValidationExtension::class.java) validateExtension(extension) @@ -30,8 +29,6 @@ public class BinaryCompatibilityValidatorPlugin : Plugin { } } - @ExperimentalLibraryAbiReader - @ExperimentalBCVApi private fun Project.validateExtension(extension: ApiValidationExtension) { afterEvaluate { val ignored = extension.ignoredProjects @@ -53,7 +50,7 @@ public class BinaryCompatibilityValidatorPlugin : Plugin { } } - @ExperimentalBCVApi + @OptIn(ExperimentalBCVApi::class) private fun configureProject(project: Project, extension: ApiValidationExtension) { configureKotlinPlugin(project, extension) configureAndroidPlugin(project, extension) @@ -70,7 +67,6 @@ public class BinaryCompatibilityValidatorPlugin : Plugin { action.execute(it) } - @ExperimentalBCVApi private fun configureMultiplatformPlugin( project: Project, extension: ApiValidationExtension @@ -257,7 +253,7 @@ internal val Project.apiValidationExtensionOrNull: ApiValidationExtension? private fun apiCheckEnabled(projectName: String, extension: ApiValidationExtension): Boolean = projectName !in extension.ignoredProjects && !extension.validationDisabled -@ExperimentalBCVApi +@OptIn(ExperimentalBCVApi::class) private fun klibAbiCheckEnabled(projectName: String, extension: ApiValidationExtension): Boolean = projectName !in extension.ignoredProjects && !extension.validationDisabled && extension.klib.enabled From 369d79193fe63a4bd474dc76c6bdaf041d04f0c2 Mon Sep 17 00:00:00 2001 From: Filipp Zhinkin Date: Wed, 28 Feb 2024 10:08:32 +0100 Subject: [PATCH 20/53] Made signature version optional, improved docs, fixed typos --- api/binary-compatibility-validator.api | 4 ++-- .../validation/test/KLibVerificationTests.kt | 2 +- src/main/kotlin/ApiValidationExtension.kt | 19 +++++++++++++++---- .../BinaryCompatibilityValidatorPlugin.kt | 3 +-- src/main/kotlin/KotlinKlibAbiBuildTask.kt | 2 +- 5 files changed, 20 insertions(+), 10 deletions(-) diff --git a/api/binary-compatibility-validator.api b/api/binary-compatibility-validator.api index 9ade3185..8784c80d 100644 --- a/api/binary-compatibility-validator.api +++ b/api/binary-compatibility-validator.api @@ -58,11 +58,11 @@ public abstract interface annotation class kotlinx/validation/ExternalApi : java public class kotlinx/validation/KlibValidationSettings { public fun ()V public final fun getEnabled ()Z - public final fun getSignatureVersion ()I + public final fun getSignatureVersion ()Ljava/lang/Integer; public final fun getStrictValidation ()Z public final fun getUseTargetGroupAliases ()Z public final fun setEnabled (Z)V - public final fun setSignatureVersion (I)V + public final fun setSignatureVersion (Ljava/lang/Integer;)V public final fun setStrictValidation (Z)V public final fun setUseTargetGroupAliases (Z)V } diff --git a/src/functionalTest/kotlin/kotlinx/validation/test/KLibVerificationTests.kt b/src/functionalTest/kotlin/kotlinx/validation/test/KLibVerificationTests.kt index 6c67c656..ae3a6681 100644 --- a/src/functionalTest/kotlin/kotlinx/validation/test/KLibVerificationTests.kt +++ b/src/functionalTest/kotlin/kotlinx/validation/test/KLibVerificationTests.kt @@ -20,7 +20,7 @@ import java.nio.file.Files import java.nio.file.Paths import kotlin.test.assertTrue -internal const val BANNED_TARGETS_PROPERTY_NAME = "binary.compatibility.validator.klib.targets.blacklist.for.testing" +internal const val BANNED_TARGETS_PROPERTY_NAME = "binary.compatibility.validator.klib.targets.disabled.for.testing" private fun KLibVerificationTests.checkKlibDump( buildResult: BuildResult, diff --git a/src/main/kotlin/ApiValidationExtension.kt b/src/main/kotlin/ApiValidationExtension.kt index 6afe7da2..94a64c32 100644 --- a/src/main/kotlin/ApiValidationExtension.kt +++ b/src/main/kotlin/ApiValidationExtension.kt @@ -99,9 +99,20 @@ public open class KlibValidationSettings { */ public var enabled: Boolean = false /** - * Specify which version of signature KLib ABI dump should contain. - */ - public var signatureVersion: Int = 2 + * Specifies which version of signature KLib ABI dump should contain. + * By default, or when explicitly set to null, the latest supported version will be used. + * + * This option covers some advanced scenarios and does not require any configuration by default. + * + * A linker uses signatures to look up symbols, thus signature changes brake binary compatibility and + * should be tracked. Signature format itself is not stabilized yet and may change in the future. In that case, + * a new version of a signature will be introduced. Change of a signature version will be reflected in a dump + * causing a validation failure even if declarations itself remained unchanged. + * However, if a klib supports multiple signature versions simultaneously, one my explicitly specify the version + * that will be dumped to prevent changes in a dump file. + */ + public var signatureVersion: Int? = null + /** * Fail validation if some build targets are not supported by the host compiler. * By default, ABI dumped only for supported files will be validated. This option makes validation behavior @@ -109,7 +120,7 @@ public open class KlibValidationSettings { */ public var strictValidation: Boolean = false /** - * For declarations available only on some targets, replace a comment listing all targets + * For declarations available only on some targets, replace a comment listing these targets * with a short alias. * * Group aliases are based on [the default hierarchy template](https://kotlinlang.org/docs/multiplatform-hierarchy.html#see-the-full-hierarchy-template) diff --git a/src/main/kotlin/BinaryCompatibilityValidatorPlugin.kt b/src/main/kotlin/BinaryCompatibilityValidatorPlugin.kt index 43ee5c5c..f10f163d 100644 --- a/src/main/kotlin/BinaryCompatibilityValidatorPlugin.kt +++ b/src/main/kotlin/BinaryCompatibilityValidatorPlugin.kt @@ -16,7 +16,6 @@ import org.jetbrains.kotlin.konan.target.HostManager import org.jetbrains.kotlin.library.abi.ExperimentalLibraryAbiReader import org.jetbrains.kotlin.library.abi.LibraryAbiReader import java.io.* -import kotlin.text.split @OptIn(ExperimentalBCVApi::class, ExperimentalLibraryAbiReader::class) public class BinaryCompatibilityValidatorPlugin : Plugin { @@ -330,7 +329,7 @@ private inline fun Project.task( noinline configuration: T.() -> Unit, ): TaskProvider = tasks.register(name, T::class.java, Action(configuration)) -private const val BANNED_TARGETS_PROPERTY_NAME = "binary.compatibility.validator.klib.targets.blacklist.for.testing" +private const val BANNED_TARGETS_PROPERTY_NAME = "binary.compatibility.validator.klib.targets.disabled.for.testing" private const val KLIB_DUMPS_DIRECTORY = "klib" private const val KLIB_INFERRED_DUMPS_DIRECTORY = "klib-all" diff --git a/src/main/kotlin/KotlinKlibAbiBuildTask.kt b/src/main/kotlin/KotlinKlibAbiBuildTask.kt index c9959d0d..c7827904 100644 --- a/src/main/kotlin/KotlinKlibAbiBuildTask.kt +++ b/src/main/kotlin/KotlinKlibAbiBuildTask.kt @@ -71,7 +71,7 @@ public abstract class KotlinKlibAbiBuildTask : BuildTaskBase() { throw IllegalStateException("Can't read a klib: ${klibFile.singleFile}", e) } - val supportedVersions = parsedAbi.signatureVersions.asSequence() + val supportedVersions = parsedAbi.signatureVersions.asSequence().filter { it.isSupportedByAbiReader } val sigVersion = if (signatureVersion != null) { val versionNumbers = supportedVersions.map { it.versionNumber }.toSortedSet() if (signatureVersion !in versionNumbers) { From 9006c1a843468aa6e7f4ed9ed7a1d3295d86d888 Mon Sep 17 00:00:00 2001 From: Filipp Zhinkin Date: Wed, 28 Feb 2024 17:29:52 +0100 Subject: [PATCH 21/53] Simplify rules for target group aliases inclusion --- docs/design/KLibSupport.md | 16 ++++----- .../classes/AnotherBuildConfig.klib.dump | 2 -- .../classes/AnotherBuildConfig.klib.web.dump | 3 -- ...AnotherBuildConfigLinux.klib.grouping.dump | 1 - ...notherBuildConfigLinuxArm64Extra.klib.dump | 2 -- .../AnotherBuildConfigModified.klib.dump | 2 -- .../classes/HiddenDeclarations.klib.dump | 2 -- .../examples/classes/Subclasses.klib.dump | 2 -- .../TopLevelDeclarations.klib.all.dump | 7 ---- .../TopLevelDeclarations.klib.with.linux.dump | 2 -- src/main/kotlin/klib/KlibAbiDumpFileMerger.kt | 33 +++++++++++++++---- .../merge/diverging/merged_with_aliases.abi | 1 - 12 files changed, 33 insertions(+), 40 deletions(-) diff --git a/docs/design/KLibSupport.md b/docs/design/KLibSupport.md index c1926d58..ecefa3d9 100644 --- a/docs/design/KLibSupport.md +++ b/docs/design/KLibSupport.md @@ -55,7 +55,6 @@ Here's a brief example of such a merged dump file: ``` // Merged KLib ABI Dump // Targets: [androidNativeArm32, androidNativeArm64, androidNativeX64, androidNativeX86, linuxArm64, linuxX64, mingwX64] -// Alias: androidNative => [androidNativeArm32, androidNativeArm64, androidNativeX64, androidNativeX86] // Alias: linux => [linuxArm64, linuxX64] // Rendering settings: // - Signature version: 2 @@ -84,15 +83,12 @@ declarations, until it is mentioned explicitly, the list of targets is the same So, the class `BuildConfig` have the same targets as the whole file, but `linuxSpecific`-function is presented only on `linux`-targets (`// Targets: [linux]`). -The next two lines declare target aliases (`// Alias: => []`). There are only two -aliases (`linux` and `androidNative`), because it seems excessive to declare aliases for groups -consisting of only a single target (like `mingw`) as well as declaring aliases for targets -than will never be explicitly presented in a file (like an alias for all the targets, or all native targets). -We also can't add aliases for targets that does not exist, so there are no aliases like `macos` or `apple` -(in this example). -However, all other aliases are generated even if it is not explicitly mentioned (like `androidNative`) to -minimize dump's header changes in the future, when such groups could emerge. - +The next line declares a target alias (`// Alias: => []`). There are only one alias (`linux`). +Aliases are generated for target groups corresponding to groups from a default hierarchy, that could appear in a file +and consist of more than a single target. +For instance, there's no `andoidNative` group alias as there are no declarations having only +corresponding android-native targets and there is no group for `mingwX64` +as there would no other targets in such a group. After that, a regular KLib ABI dump header continues, with an exception to some declarations being annotated with `// Targets`. diff --git a/src/functionalTest/resources/examples/classes/AnotherBuildConfig.klib.dump b/src/functionalTest/resources/examples/classes/AnotherBuildConfig.klib.dump index 7a1b5aa2..7e52930c 100644 --- a/src/functionalTest/resources/examples/classes/AnotherBuildConfig.klib.dump +++ b/src/functionalTest/resources/examples/classes/AnotherBuildConfig.klib.dump @@ -1,7 +1,5 @@ // Merged KLib ABI Dump // Targets: [androidNativeArm32, androidNativeArm64, androidNativeX64, androidNativeX86, linuxArm64, linuxX64, mingwX64] -// Alias: androidNative => [androidNativeArm32, androidNativeArm64, androidNativeX64, androidNativeX86] -// Alias: linux => [linuxArm64, linuxX64] // Rendering settings: // - Signature version: 2 // - Show manifest properties: false diff --git a/src/functionalTest/resources/examples/classes/AnotherBuildConfig.klib.web.dump b/src/functionalTest/resources/examples/classes/AnotherBuildConfig.klib.web.dump index f00be63e..dfd569ed 100644 --- a/src/functionalTest/resources/examples/classes/AnotherBuildConfig.klib.web.dump +++ b/src/functionalTest/resources/examples/classes/AnotherBuildConfig.klib.web.dump @@ -1,8 +1,5 @@ // Merged KLib ABI Dump // Targets: [androidNativeArm32, androidNativeArm64, androidNativeX64, androidNativeX86, js, linuxArm64, linuxX64, mingwX64, wasmJs, wasmWasi] -// Alias: native => [androidNativeArm32, androidNativeArm64, androidNativeX64, androidNativeX86, linuxArm64, linuxX64, mingwX64] -// Alias: androidNative => [androidNativeArm32, androidNativeArm64, androidNativeX64, androidNativeX86] -// Alias: linux => [linuxArm64, linuxX64] // Rendering settings: // - Signature version: 2 // - Show manifest properties: false diff --git a/src/functionalTest/resources/examples/classes/AnotherBuildConfigLinux.klib.grouping.dump b/src/functionalTest/resources/examples/classes/AnotherBuildConfigLinux.klib.grouping.dump index 98d3c553..0bec2833 100644 --- a/src/functionalTest/resources/examples/classes/AnotherBuildConfigLinux.klib.grouping.dump +++ b/src/functionalTest/resources/examples/classes/AnotherBuildConfigLinux.klib.grouping.dump @@ -1,6 +1,5 @@ // Merged KLib ABI Dump // Targets: [androidNativeArm32, androidNativeArm64, androidNativeX64, androidNativeX86, linuxArm64, linuxX64, mingwX64] -// Alias: androidNative => [androidNativeArm32, androidNativeArm64, androidNativeX64, androidNativeX86] // Alias: linux => [linuxArm64, linuxX64] // Rendering settings: // - Signature version: 2 diff --git a/src/functionalTest/resources/examples/classes/AnotherBuildConfigLinuxArm64Extra.klib.dump b/src/functionalTest/resources/examples/classes/AnotherBuildConfigLinuxArm64Extra.klib.dump index 70842122..fad83d19 100644 --- a/src/functionalTest/resources/examples/classes/AnotherBuildConfigLinuxArm64Extra.klib.dump +++ b/src/functionalTest/resources/examples/classes/AnotherBuildConfigLinuxArm64Extra.klib.dump @@ -1,7 +1,5 @@ // Merged KLib ABI Dump // Targets: [androidNativeArm32, androidNativeArm64, androidNativeX64, androidNativeX86, linuxArm64, linuxX64, mingwX64] -// Alias: androidNative => [androidNativeArm32, androidNativeArm64, androidNativeX64, androidNativeX86] -// Alias: linux => [linuxArm64, linuxX64] // Rendering settings: // - Signature version: 2 // - Show manifest properties: false diff --git a/src/functionalTest/resources/examples/classes/AnotherBuildConfigModified.klib.dump b/src/functionalTest/resources/examples/classes/AnotherBuildConfigModified.klib.dump index 383fb7e0..1841ba6a 100644 --- a/src/functionalTest/resources/examples/classes/AnotherBuildConfigModified.klib.dump +++ b/src/functionalTest/resources/examples/classes/AnotherBuildConfigModified.klib.dump @@ -1,7 +1,5 @@ // Merged KLib ABI Dump // Targets: [androidNativeArm32, androidNativeArm64, androidNativeX64, androidNativeX86, linuxArm64, linuxX64, mingwX64] -// Alias: androidNative => [androidNativeArm32, androidNativeArm64, androidNativeX64, androidNativeX86] -// Alias: linux => [linuxArm64, linuxX64] // Rendering settings: // - Signature version: 2 // - Show manifest properties: false diff --git a/src/functionalTest/resources/examples/classes/HiddenDeclarations.klib.dump b/src/functionalTest/resources/examples/classes/HiddenDeclarations.klib.dump index 32f3d71f..dc86c2b6 100644 --- a/src/functionalTest/resources/examples/classes/HiddenDeclarations.klib.dump +++ b/src/functionalTest/resources/examples/classes/HiddenDeclarations.klib.dump @@ -1,7 +1,5 @@ // Merged KLib ABI Dump // Targets: [androidNativeArm32, androidNativeArm64, androidNativeX64, androidNativeX86, linuxArm64, linuxX64, mingwX64] -// Alias: androidNative => [androidNativeArm32, androidNativeArm64, androidNativeX64, androidNativeX86] -// Alias: linux => [linuxArm64, linuxX64] // Rendering settings: // - Signature version: 2 // - Show manifest properties: false diff --git a/src/functionalTest/resources/examples/classes/Subclasses.klib.dump b/src/functionalTest/resources/examples/classes/Subclasses.klib.dump index 1a9cd04c..86eda8f0 100644 --- a/src/functionalTest/resources/examples/classes/Subclasses.klib.dump +++ b/src/functionalTest/resources/examples/classes/Subclasses.klib.dump @@ -1,7 +1,5 @@ // Merged KLib ABI Dump // Targets: [androidNativeArm32, androidNativeArm64, androidNativeX64, androidNativeX86, linuxArm64, linuxX64, mingwX64] -// Alias: androidNative => [androidNativeArm32, androidNativeArm64, androidNativeX64, androidNativeX86] -// Alias: linux => [linuxArm64, linuxX64] // Rendering settings: // - Signature version: 2 // - Show manifest properties: false diff --git a/src/functionalTest/resources/examples/classes/TopLevelDeclarations.klib.all.dump b/src/functionalTest/resources/examples/classes/TopLevelDeclarations.klib.all.dump index 1b6b628e..02622b09 100644 --- a/src/functionalTest/resources/examples/classes/TopLevelDeclarations.klib.all.dump +++ b/src/functionalTest/resources/examples/classes/TopLevelDeclarations.klib.all.dump @@ -1,12 +1,5 @@ // Merged KLib ABI Dump // Targets: [androidNativeArm32, androidNativeArm64, androidNativeX64, androidNativeX86, iosArm64, iosSimulatorArm64, iosX64, linuxArm64, linuxX64, macosArm64, macosX64, mingwX64, tvosArm64, tvosSimulatorArm64, tvosX64, watchosArm32, watchosArm64, watchosDeviceArm64, watchosSimulatorArm64, watchosX64] -// Alias: apple => [iosArm64, iosSimulatorArm64, iosX64, macosArm64, macosX64, tvosArm64, tvosSimulatorArm64, tvosX64, watchosArm32, watchosArm64, watchosDeviceArm64, watchosSimulatorArm64, watchosX64] -// Alias: watchos => [watchosArm32, watchosArm64, watchosDeviceArm64, watchosSimulatorArm64, watchosX64] -// Alias: ios => [iosArm64, iosSimulatorArm64, iosX64] -// Alias: androidNative => [androidNativeArm32, androidNativeArm64, androidNativeX64, androidNativeX86] -// Alias: tvos => [tvosArm64, tvosSimulatorArm64, tvosX64] -// Alias: linux => [linuxArm64, linuxX64] -// Alias: macos => [macosArm64, macosX64] // Rendering settings: // - Signature version: 2 // - Show manifest properties: false diff --git a/src/functionalTest/resources/examples/classes/TopLevelDeclarations.klib.with.linux.dump b/src/functionalTest/resources/examples/classes/TopLevelDeclarations.klib.with.linux.dump index 6c1f6dbb..735798b4 100644 --- a/src/functionalTest/resources/examples/classes/TopLevelDeclarations.klib.with.linux.dump +++ b/src/functionalTest/resources/examples/classes/TopLevelDeclarations.klib.with.linux.dump @@ -1,7 +1,5 @@ // Merged KLib ABI Dump // Targets: [androidNativeArm32, androidNativeArm64, androidNativeX64, androidNativeX86, linuxArm64, linuxX64, mingwX64] -// Alias: androidNative => [androidNativeArm32, androidNativeArm64, androidNativeX64, androidNativeX86] -// Alias: linux => [linuxArm64, linuxX64] // Rendering settings: // - Signature version: 2 // - Show manifest properties: false diff --git a/src/main/kotlin/klib/KlibAbiDumpFileMerger.kt b/src/main/kotlin/klib/KlibAbiDumpFileMerger.kt index c2e70776..09847114 100644 --- a/src/main/kotlin/klib/KlibAbiDumpFileMerger.kt +++ b/src/main/kotlin/klib/KlibAbiDumpFileMerger.kt @@ -305,7 +305,7 @@ internal class KlibAbiDumpMerger { ) } } - return GroupingFormatter(targets) + return GroupingFormatter(this) } else { DefaultFormatter } @@ -378,13 +378,19 @@ internal class KlibAbiDumpMerger { topLevelDeclaration.overrideTargets(targets) } + + internal fun visit(action: (DeclarationContainer) -> Unit) { + topLevelDeclaration.children.forEach { + action(it.value) + } + } } /** * A class representing a single declaration from a KLib API dump along with all its children * declarations. */ -private class DeclarationContainer(val text: String, val parent: DeclarationContainer? = null) { +internal class DeclarationContainer(val text: String, val parent: DeclarationContainer? = null) { val targets: MutableSet = mutableSetOf() val children: MutableMap = mutableMapOf() var delimiter: String? = null @@ -490,6 +496,12 @@ private class DeclarationContainer(val text: String, val parent: DeclarationCont } } } + + internal fun visit(action: (DeclarationContainer) -> Unit) { + children.forEach { + action(it.value) + } + } } // TODO: optimize @@ -514,7 +526,7 @@ private object DeclarationsComparator : Comparator { } } -private interface KLibsTargetsFormatter { +internal interface KLibsTargetsFormatter { fun formatHeader(targets: Set): String fun formatDeclarationTargets(targets: Set): String @@ -531,15 +543,16 @@ private object DefaultFormatter : KLibsTargetsFormatter { } } -private class GroupingFormatter(allTargets: Set) : KLibsTargetsFormatter { +private class GroupingFormatter(klibDump: KlibAbiDumpMerger) : KLibsTargetsFormatter { private data class Alias(val name: String, val targets: Set) private val aliases: List init { + val allTargets = klibDump.targets val aliasesBuilder = mutableListOf() TargetHierarchy.hierarchyIndex.asSequence() - // place smaller groups (more specific groups) closer to beginning of the list + // place smaller groups (more specific groups) closer to the beginning of the list .sortedWith(compareBy({ it.value.allLeafs.size }, { it.key })) .forEach { // intersect with all targets to use only enabled targets in aliases @@ -554,6 +567,14 @@ private class GroupingFormatter(allTargets: Set) : KLibsTargetsFormatter // filter out all groups consisting of less than one member aliasesBuilder.removeIf { it.targets.size < 2 } aliasesBuilder.removeIf { it.targets == allTargets } + // collect all actually used target groups and remove all unused aliases + val usedAliases = mutableSetOf>() + fun visitor(decl: DeclarationContainer) { + usedAliases.add(decl.targets) + decl.visit(::visitor) + } + klibDump.visit(::visitor) + aliasesBuilder.removeIf { !usedAliases.contains(it.targets) } // Remove all duplicating groups. At this point, aliases are sorted so // that more specific groups are before more common groups, so we'll remove // more common groups here. @@ -569,7 +590,7 @@ private class GroupingFormatter(allTargets: Set) : KLibsTargetsFormatter toRemove.forEach { aliasesBuilder.removeAt(it) } - // reverse the order to place common group first + // reverse the order to place a common group first aliases = aliasesBuilder.reversed() } diff --git a/src/test/resources/merge/diverging/merged_with_aliases.abi b/src/test/resources/merge/diverging/merged_with_aliases.abi index 868e8f88..9e57d3c7 100644 --- a/src/test/resources/merge/diverging/merged_with_aliases.abi +++ b/src/test/resources/merge/diverging/merged_with_aliases.abi @@ -1,6 +1,5 @@ // Merged KLib ABI Dump // Targets: [androidNativeArm64, linuxArm64, linuxX64, tvOsX64] -// Alias: native => [androidNativeArm64, linuxArm64, linuxX64] // Alias: linux => [linuxArm64, linuxX64] // Rendering settings: // - Signature version: 2 From 05c424d264e2213c2c432fa027b92ce7392647ee Mon Sep 17 00:00:00 2001 From: Filipp Zhinkin Date: Thu, 29 Feb 2024 17:12:08 +0100 Subject: [PATCH 22/53] Support compound target names --- api/binary-compatibility-validator.api | 9 +- .../validation/test/KLibVerificationTests.kt | 30 +++- .../AnotherBuildConfig.klib.clash.dump | 7 +- .../AnotherBuildConfig.klib.custom.dump | 4 +- .../classes/AnotherBuildConfig.klib.dump | 2 +- .../classes/AnotherBuildConfig.klib.web.dump | 2 +- ...AnotherBuildConfigLinux.klib.grouping.dump | 2 +- ...notherBuildConfigLinuxArm64Extra.klib.dump | 2 +- .../AnotherBuildConfigModified.klib.dump | 2 +- .../classes/ClassWithPublicMarkers.klib.dump | 2 +- .../examples/classes/Empty.klib.dump | 2 +- .../classes/HiddenDeclarations.klib.dump | 2 +- .../examples/classes/Properties.klib.dump | 2 +- .../examples/classes/Subclasses.klib.dump | 2 +- .../TopLevelDeclarations.klib.all.dump | 2 +- .../classes/TopLevelDeclarations.klib.dump | 2 +- .../TopLevelDeclarations.klib.unsup.dump | 2 +- .../classes/TopLevelDeclarations.klib.v1.dump | 2 +- .../TopLevelDeclarations.klib.with.linux.dump | 2 +- .../grouping/clashingTargetNames.gradle.kts | 7 +- .../BinaryCompatibilityValidatorPlugin.kt | 21 ++- src/main/kotlin/KotlinKlibAbiBuildTask.kt | 2 +- ...otlinKlibExtractSupportedTargetsAbiTask.kt | 4 +- ...linKlibInferAbiForUnsupportedTargetTask.kt | 21 ++- src/main/kotlin/KotlinKlibMergeAbiTask.kt | 7 +- src/main/kotlin/klib/KlibAbiDumpFileMerger.kt | 168 +++++++++++++----- src/main/kotlin/klib/TargetHierarchy.kt | 43 +++-- src/test/kotlin/tests/KlibAbiMergingTest.kt | 110 ++++++++++-- src/test/kotlin/tests/TargetHierarchyTest.kt | 17 ++ .../merge/diverging/androidNativeArm64.api | 4 + .../resources/merge/diverging/linuxArm64.api | 4 + .../resources/merge/diverging/linuxX64.api | 4 + src/test/resources/merge/diverging/merged.abi | 6 +- .../merge/diverging/merged_with_aliases.abi | 6 +- .../merged_with_aliases_and_custom_names.abi | 32 ++++ .../resources/merge/diverging/tvOsX64.api | 4 + .../merge/identical/dump_linux_x64.abi | 17 ++ .../merge/identical/dump_macos_arm64.abi | 17 ++ src/test/resources/merge/webTargets/js.abi | 16 ++ .../dump.abi => webTargets/merged.abi} | 6 +- .../resources/merge/webTargets/wasmJs.abi | 16 ++ .../resources/merge/webTargets/wasmWasi.abi | 16 ++ 42 files changed, 509 insertions(+), 119 deletions(-) create mode 100644 src/test/resources/merge/diverging/merged_with_aliases_and_custom_names.abi create mode 100644 src/test/resources/merge/identical/dump_linux_x64.abi create mode 100644 src/test/resources/merge/identical/dump_macos_arm64.abi create mode 100644 src/test/resources/merge/webTargets/js.abi rename src/test/resources/merge/{identical/dump.abi => webTargets/merged.abi} (93%) create mode 100644 src/test/resources/merge/webTargets/wasmJs.abi create mode 100644 src/test/resources/merge/webTargets/wasmWasi.abi diff --git a/api/binary-compatibility-validator.api b/api/binary-compatibility-validator.api index 8784c80d..75c7bbcd 100644 --- a/api/binary-compatibility-validator.api +++ b/api/binary-compatibility-validator.api @@ -125,20 +125,23 @@ public abstract class kotlinx/validation/KotlinKlibInferAbiForUnsupportedTargetT public field outputApiDir Ljava/lang/String; public field outputFile Ljava/io/File; public field supportedTargets Ljava/util/Set; - public field unsupportedTarget Ljava/lang/String; + public field unsupportedTargetName Ljava/lang/String; + public field unsupportedUnderlyingTarget Ljava/lang/String; public fun ()V public final fun getDumpFileName ()Ljava/lang/String; public final fun getInputImageFile ()Ljava/io/File; public final fun getOutputApiDir ()Ljava/lang/String; public final fun getOutputFile ()Ljava/io/File; public final fun getSupportedTargets ()Ljava/util/Set; - public final fun getUnsupportedTarget ()Ljava/lang/String; + public final fun getUnsupportedTargetName ()Ljava/lang/String; + public final fun getUnsupportedUnderlyingTarget ()Ljava/lang/String; public final fun setDumpFileName (Ljava/lang/String;)V public final fun setInputImageFile (Ljava/io/File;)V public final fun setOutputApiDir (Ljava/lang/String;)V public final fun setOutputFile (Ljava/io/File;)V public final fun setSupportedTargets (Ljava/util/Set;)V - public final fun setUnsupportedTarget (Ljava/lang/String;)V + public final fun setUnsupportedTargetName (Ljava/lang/String;)V + public final fun setUnsupportedUnderlyingTarget (Ljava/lang/String;)V } public abstract class kotlinx/validation/KotlinKlibMergeAbiTask : org/gradle/api/DefaultTask { diff --git a/src/functionalTest/kotlin/kotlinx/validation/test/KLibVerificationTests.kt b/src/functionalTest/kotlin/kotlinx/validation/test/KLibVerificationTests.kt index ae3a6681..54d62409 100644 --- a/src/functionalTest/kotlin/kotlinx/validation/test/KLibVerificationTests.kt +++ b/src/functionalTest/kotlin/kotlinx/validation/test/KLibVerificationTests.kt @@ -356,6 +356,30 @@ internal class KLibVerificationTests : BaseKotlinGradleTest() { ) } + @Test + fun `infer a dump for a target with custom name`() { + val runner = test { + settingsGradleKts { + resolve("/examples/gradle/settings/settings-name-testproject.gradle.kts") + } + buildGradleKts { + resolve("/examples/gradle/base/withNativePluginAndNoTargets.gradle.kts") + } + additionalBuildConfig("/examples/gradle/configuration/grouping/clashingTargetNames.gradle.kts") + addToSrcSet("/examples/classes/TopLevelDeclarations.kt") + addToSrcSet("/examples/classes/AnotherBuildConfigLinuxArm64.kt", "linuxMain") + runner { + arguments.add("-P$BANNED_TARGETS_PROPERTY_NAME=linuxx") + arguments.add(":klibApiDump") + } + } + + checkKlibDump( + runner.build(), "/examples/classes/TopLevelDeclarations.klib.with.linux.dump", + dumpTask = ":klibApiDump" + ) + } + @Test fun `klibDump should fail when the only target in the project is disabled`() { val runner = test { @@ -429,7 +453,7 @@ internal class KLibVerificationTests : BaseKotlinGradleTest() { } @Test - fun `target name grouping should be disabled on group name clash`() { + fun `target name clashing with a group name`() { val runner = test { settingsGradleKts { resolve("/examples/gradle/settings/settings-name-testproject.gradle.kts") @@ -439,6 +463,10 @@ internal class KLibVerificationTests : BaseKotlinGradleTest() { resolve("/examples/gradle/configuration/grouping/clashingTargetNames.gradle.kts") } addToSrcSet("/examples/classes/AnotherBuildConfig.kt") + addToSrcSet("/examples/classes/AnotherBuildConfigLinuxArm64.kt", "linuxArm64Main") + kotlin("AnotherBuildConfigLinuxX64.kt", "linuxMain") { + resolve("/examples/classes/AnotherBuildConfigLinuxArm64.kt") + } runner { arguments.add(":klibApiDump") } diff --git a/src/functionalTest/resources/examples/classes/AnotherBuildConfig.klib.clash.dump b/src/functionalTest/resources/examples/classes/AnotherBuildConfig.klib.clash.dump index 10da55c9..c5cdbe7c 100644 --- a/src/functionalTest/resources/examples/classes/AnotherBuildConfig.klib.clash.dump +++ b/src/functionalTest/resources/examples/classes/AnotherBuildConfig.klib.clash.dump @@ -1,8 +1,9 @@ // Merged KLib ABI Dump -// Targets: [linux, linuxArm64] +// Targets: [androidNativeArm32, androidNativeArm64, androidNativeX64, androidNativeX86, linux.linuxX64, linuxArm64, mingwX64] +// Alias: linux => [linux.linuxX64, linuxArm64] // Rendering settings: // - Signature version: 2 -// - Show manifest properties: false +// - Show manifest properties: true // - Show declarations: true // Library unique name: @@ -12,3 +13,5 @@ final class org.different.pack/BuildConfig { // org.different.pack/BuildConfig|n final val p1 // org.different.pack/BuildConfig.p1|{}p1[0] final fun (): kotlin/Int // org.different.pack/BuildConfig.p1.|(){}[0] } +// Targets: [linux] +final fun (org.different.pack/BuildConfig).org.different.pack/linuxArm64Specific(): kotlin/Int // org.different.pack/linuxArm64Specific|linuxArm64Specific@org.different.pack.BuildConfig(){}[0] diff --git a/src/functionalTest/resources/examples/classes/AnotherBuildConfig.klib.custom.dump b/src/functionalTest/resources/examples/classes/AnotherBuildConfig.klib.custom.dump index 2d6ba9d3..4de2ecfc 100644 --- a/src/functionalTest/resources/examples/classes/AnotherBuildConfig.klib.custom.dump +++ b/src/functionalTest/resources/examples/classes/AnotherBuildConfig.klib.custom.dump @@ -1,8 +1,8 @@ // Merged KLib ABI Dump -// Targets: [linuxA, linuxB] +// Targets: [linuxA.linuxX64, linuxB.linuxX64] // Rendering settings: // - Signature version: 2 -// - Show manifest properties: false +// - Show manifest properties: true // - Show declarations: true // Library unique name: diff --git a/src/functionalTest/resources/examples/classes/AnotherBuildConfig.klib.dump b/src/functionalTest/resources/examples/classes/AnotherBuildConfig.klib.dump index 7e52930c..29d43e40 100644 --- a/src/functionalTest/resources/examples/classes/AnotherBuildConfig.klib.dump +++ b/src/functionalTest/resources/examples/classes/AnotherBuildConfig.klib.dump @@ -2,7 +2,7 @@ // Targets: [androidNativeArm32, androidNativeArm64, androidNativeX64, androidNativeX86, linuxArm64, linuxX64, mingwX64] // Rendering settings: // - Signature version: 2 -// - Show manifest properties: false +// - Show manifest properties: true // - Show declarations: true // Library unique name: diff --git a/src/functionalTest/resources/examples/classes/AnotherBuildConfig.klib.web.dump b/src/functionalTest/resources/examples/classes/AnotherBuildConfig.klib.web.dump index dfd569ed..226addf8 100644 --- a/src/functionalTest/resources/examples/classes/AnotherBuildConfig.klib.web.dump +++ b/src/functionalTest/resources/examples/classes/AnotherBuildConfig.klib.web.dump @@ -2,7 +2,7 @@ // Targets: [androidNativeArm32, androidNativeArm64, androidNativeX64, androidNativeX86, js, linuxArm64, linuxX64, mingwX64, wasmJs, wasmWasi] // Rendering settings: // - Signature version: 2 -// - Show manifest properties: false +// - Show manifest properties: true // - Show declarations: true // Library unique name: diff --git a/src/functionalTest/resources/examples/classes/AnotherBuildConfigLinux.klib.grouping.dump b/src/functionalTest/resources/examples/classes/AnotherBuildConfigLinux.klib.grouping.dump index 0bec2833..32aeceb3 100644 --- a/src/functionalTest/resources/examples/classes/AnotherBuildConfigLinux.klib.grouping.dump +++ b/src/functionalTest/resources/examples/classes/AnotherBuildConfigLinux.klib.grouping.dump @@ -3,7 +3,7 @@ // Alias: linux => [linuxArm64, linuxX64] // Rendering settings: // - Signature version: 2 -// - Show manifest properties: false +// - Show manifest properties: true // - Show declarations: true // Library unique name: diff --git a/src/functionalTest/resources/examples/classes/AnotherBuildConfigLinuxArm64Extra.klib.dump b/src/functionalTest/resources/examples/classes/AnotherBuildConfigLinuxArm64Extra.klib.dump index fad83d19..5386f860 100644 --- a/src/functionalTest/resources/examples/classes/AnotherBuildConfigLinuxArm64Extra.klib.dump +++ b/src/functionalTest/resources/examples/classes/AnotherBuildConfigLinuxArm64Extra.klib.dump @@ -2,7 +2,7 @@ // Targets: [androidNativeArm32, androidNativeArm64, androidNativeX64, androidNativeX86, linuxArm64, linuxX64, mingwX64] // Rendering settings: // - Signature version: 2 -// - Show manifest properties: false +// - Show manifest properties: true // - Show declarations: true // Library unique name: diff --git a/src/functionalTest/resources/examples/classes/AnotherBuildConfigModified.klib.dump b/src/functionalTest/resources/examples/classes/AnotherBuildConfigModified.klib.dump index 1841ba6a..d498c0c1 100644 --- a/src/functionalTest/resources/examples/classes/AnotherBuildConfigModified.klib.dump +++ b/src/functionalTest/resources/examples/classes/AnotherBuildConfigModified.klib.dump @@ -2,7 +2,7 @@ // Targets: [androidNativeArm32, androidNativeArm64, androidNativeX64, androidNativeX86, linuxArm64, linuxX64, mingwX64] // Rendering settings: // - Signature version: 2 -// - Show manifest properties: false +// - Show manifest properties: true // - Show declarations: true // Library unique name: diff --git a/src/functionalTest/resources/examples/classes/ClassWithPublicMarkers.klib.dump b/src/functionalTest/resources/examples/classes/ClassWithPublicMarkers.klib.dump index 94cccd63..4884ae1f 100644 --- a/src/functionalTest/resources/examples/classes/ClassWithPublicMarkers.klib.dump +++ b/src/functionalTest/resources/examples/classes/ClassWithPublicMarkers.klib.dump @@ -4,7 +4,7 @@ // Alias: linux => [linuxArm64, linuxX64] // Rendering settings: // - Signature version: 2 -// - Show manifest properties: false +// - Show manifest properties: true // - Show declarations: true // Library unique name: diff --git a/src/functionalTest/resources/examples/classes/Empty.klib.dump b/src/functionalTest/resources/examples/classes/Empty.klib.dump index f3ba7066..cc4d1356 100644 --- a/src/functionalTest/resources/examples/classes/Empty.klib.dump +++ b/src/functionalTest/resources/examples/classes/Empty.klib.dump @@ -2,5 +2,5 @@ // Targets: [mingwX64] // Rendering settings: // - Signature version: 2 -// - Show manifest properties: false +// - Show manifest properties: true // - Show declarations: true diff --git a/src/functionalTest/resources/examples/classes/HiddenDeclarations.klib.dump b/src/functionalTest/resources/examples/classes/HiddenDeclarations.klib.dump index dc86c2b6..030dc49d 100644 --- a/src/functionalTest/resources/examples/classes/HiddenDeclarations.klib.dump +++ b/src/functionalTest/resources/examples/classes/HiddenDeclarations.klib.dump @@ -2,7 +2,7 @@ // Targets: [androidNativeArm32, androidNativeArm64, androidNativeX64, androidNativeX86, linuxArm64, linuxX64, mingwX64] // Rendering settings: // - Signature version: 2 -// - Show manifest properties: false +// - Show manifest properties: true // - Show declarations: true // Library unique name: diff --git a/src/functionalTest/resources/examples/classes/Properties.klib.dump b/src/functionalTest/resources/examples/classes/Properties.klib.dump index 70189bb8..f69dad8d 100644 --- a/src/functionalTest/resources/examples/classes/Properties.klib.dump +++ b/src/functionalTest/resources/examples/classes/Properties.klib.dump @@ -2,7 +2,7 @@ // Targets: [androidNativeArm32, androidNativeArm64, androidNativeX64, androidNativeX86, linuxArm64, linuxX64, mingwX64] // Rendering settings: // - Signature version: 2 -// - Show manifest properties: false +// - Show manifest properties: true // - Show declarations: true // Library unique name: diff --git a/src/functionalTest/resources/examples/classes/Subclasses.klib.dump b/src/functionalTest/resources/examples/classes/Subclasses.klib.dump index 86eda8f0..854fac55 100644 --- a/src/functionalTest/resources/examples/classes/Subclasses.klib.dump +++ b/src/functionalTest/resources/examples/classes/Subclasses.klib.dump @@ -2,7 +2,7 @@ // Targets: [androidNativeArm32, androidNativeArm64, androidNativeX64, androidNativeX86, linuxArm64, linuxX64, mingwX64] // Rendering settings: // - Signature version: 2 -// - Show manifest properties: false +// - Show manifest properties: true // - Show declarations: true // Library unique name: diff --git a/src/functionalTest/resources/examples/classes/TopLevelDeclarations.klib.all.dump b/src/functionalTest/resources/examples/classes/TopLevelDeclarations.klib.all.dump index 02622b09..a10615fd 100644 --- a/src/functionalTest/resources/examples/classes/TopLevelDeclarations.klib.all.dump +++ b/src/functionalTest/resources/examples/classes/TopLevelDeclarations.klib.all.dump @@ -2,7 +2,7 @@ // Targets: [androidNativeArm32, androidNativeArm64, androidNativeX64, androidNativeX86, iosArm64, iosSimulatorArm64, iosX64, linuxArm64, linuxX64, macosArm64, macosX64, mingwX64, tvosArm64, tvosSimulatorArm64, tvosX64, watchosArm32, watchosArm64, watchosDeviceArm64, watchosSimulatorArm64, watchosX64] // Rendering settings: // - Signature version: 2 -// - Show manifest properties: false +// - Show manifest properties: true // - Show declarations: true // Library unique name: diff --git a/src/functionalTest/resources/examples/classes/TopLevelDeclarations.klib.dump b/src/functionalTest/resources/examples/classes/TopLevelDeclarations.klib.dump index 247b5e2c..91d444fe 100644 --- a/src/functionalTest/resources/examples/classes/TopLevelDeclarations.klib.dump +++ b/src/functionalTest/resources/examples/classes/TopLevelDeclarations.klib.dump @@ -3,7 +3,7 @@ // Alias: androidNative => [androidNativeArm32, androidNativeArm64, androidNativeX64, androidNativeX86] // Rendering settings: // - Signature version: 2 -// - Show manifest properties: false +// - Show manifest properties: true // - Show declarations: true // Library unique name: diff --git a/src/functionalTest/resources/examples/classes/TopLevelDeclarations.klib.unsup.dump b/src/functionalTest/resources/examples/classes/TopLevelDeclarations.klib.unsup.dump index 31a322a8..d9a4ac78 100644 --- a/src/functionalTest/resources/examples/classes/TopLevelDeclarations.klib.unsup.dump +++ b/src/functionalTest/resources/examples/classes/TopLevelDeclarations.klib.unsup.dump @@ -2,7 +2,7 @@ // Targets: [androidNativeArm32, androidNativeArm64, androidNativeX64, androidNativeX86, linuxX64, mingwX64] // Rendering settings: // - Signature version: 2 -// - Show manifest properties: false +// - Show manifest properties: true // - Show declarations: true // Library unique name: diff --git a/src/functionalTest/resources/examples/classes/TopLevelDeclarations.klib.v1.dump b/src/functionalTest/resources/examples/classes/TopLevelDeclarations.klib.v1.dump index 1c85eb12..7e40214b 100644 --- a/src/functionalTest/resources/examples/classes/TopLevelDeclarations.klib.v1.dump +++ b/src/functionalTest/resources/examples/classes/TopLevelDeclarations.klib.v1.dump @@ -2,7 +2,7 @@ // Targets: [androidNativeArm32, androidNativeArm64, androidNativeX64, androidNativeX86, linuxArm64, linuxX64, mingwX64] // Rendering settings: // - Signature version: 1 -// - Show manifest properties: false +// - Show manifest properties: true // - Show declarations: true // Library unique name: diff --git a/src/functionalTest/resources/examples/classes/TopLevelDeclarations.klib.with.linux.dump b/src/functionalTest/resources/examples/classes/TopLevelDeclarations.klib.with.linux.dump index 735798b4..27642861 100644 --- a/src/functionalTest/resources/examples/classes/TopLevelDeclarations.klib.with.linux.dump +++ b/src/functionalTest/resources/examples/classes/TopLevelDeclarations.klib.with.linux.dump @@ -2,7 +2,7 @@ // Targets: [androidNativeArm32, androidNativeArm64, androidNativeX64, androidNativeX86, linuxArm64, linuxX64, mingwX64] // Rendering settings: // - Signature version: 2 -// - Show manifest properties: false +// - Show manifest properties: true // - Show declarations: true // Library unique name: diff --git a/src/functionalTest/resources/examples/gradle/configuration/grouping/clashingTargetNames.gradle.kts b/src/functionalTest/resources/examples/gradle/configuration/grouping/clashingTargetNames.gradle.kts index 5c85af92..dbce3862 100644 --- a/src/functionalTest/resources/examples/gradle/configuration/grouping/clashingTargetNames.gradle.kts +++ b/src/functionalTest/resources/examples/gradle/configuration/grouping/clashingTargetNames.gradle.kts @@ -4,6 +4,11 @@ */ kotlin { - linuxX64("linux") + linuxX64("linuxx") linuxArm64() + mingwX64() + androidNativeArm32() + androidNativeArm64() + androidNativeX64() + androidNativeX86() } diff --git a/src/main/kotlin/BinaryCompatibilityValidatorPlugin.kt b/src/main/kotlin/BinaryCompatibilityValidatorPlugin.kt index f10f163d..a3887e55 100644 --- a/src/main/kotlin/BinaryCompatibilityValidatorPlugin.kt +++ b/src/main/kotlin/BinaryCompatibilityValidatorPlugin.kt @@ -5,6 +5,8 @@ package kotlinx.validation +import kotlinx.validation.klib.Target +import kotlinx.validation.klib.konanTargetNameMapping import org.gradle.api.* import org.gradle.api.plugins.* import org.gradle.api.provider.* @@ -12,6 +14,7 @@ import org.gradle.api.tasks.* import org.jetbrains.kotlin.gradle.dsl.* import org.jetbrains.kotlin.gradle.plugin.* import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget +import org.jetbrains.kotlin.gradle.targets.js.KotlinJsTarget import org.jetbrains.kotlin.konan.target.HostManager import org.jetbrains.kotlin.library.abi.ExperimentalLibraryAbiReader import org.jetbrains.kotlin.library.abi.LibraryAbiReader @@ -534,7 +537,9 @@ private class KlibValidationPipelineBuilder( } // The actual merge will happen here, where we'll try to infer a dump for the unsupported target and merge // it with other supported target dumps. - val proxy = unsupportedTargetDumpProxy(klibApiDir, targetConfig, apiBuildDir, supportedTargets.get()) + val proxy = unsupportedTargetDumpProxy(klibApiDir, targetConfig, + extractUnderlyingTarget(currentTarget), + apiBuildDir, supportedTargets.get()) mergeInferredTask.configure { it.addInput(targetName, apiBuildDir) it.dependsOn(proxy) @@ -612,7 +617,9 @@ private class KlibValidationPipelineBuilder( private fun Project.unsupportedTargetDumpProxy( klibApiDir: Provider, - targetConfig: TargetConfig, apiBuildDir: File, + targetConfig: TargetConfig, + underlyingTarget: String, + apiBuildDir: File, supportedTargets: Set ): TaskProvider { val targetName = targetConfig.targetName!! @@ -625,7 +632,8 @@ private class KlibValidationPipelineBuilder( inputImageFile = klibApiDir.get().resolve(klibDumpFileName) outputApiDir = apiBuildDir.toString() outputFile = apiBuildDir.resolve(klibDumpFileName) - unsupportedTarget = targetConfig.targetName + unsupportedTargetName = targetConfig.targetName + unsupportedUnderlyingTarget = underlyingTarget dumpFileName = klibDumpFileName dependsOn(project.tasks.withType(KotlinKlibAbiBuildTask::class.java)) } @@ -646,6 +654,13 @@ private val KotlinTarget.jvmBased: Boolean return platformType == KotlinPlatformType.jvm || platformType == KotlinPlatformType.androidJvm } +private fun extractUnderlyingTarget(target: KotlinTarget): String { + return when (target) { + is KotlinNativeTarget -> konanTargetNameMapping[target.konanTarget.name]!! + else -> target.name + } +} + private val Project.kotlinMultiplatform get() = extensions.getByName("kotlin") as KotlinMultiplatformExtension diff --git a/src/main/kotlin/KotlinKlibAbiBuildTask.kt b/src/main/kotlin/KotlinKlibAbiBuildTask.kt index c7827904..31a70a78 100644 --- a/src/main/kotlin/KotlinKlibAbiBuildTask.kt +++ b/src/main/kotlin/KotlinKlibAbiBuildTask.kt @@ -87,7 +87,7 @@ public abstract class KotlinKlibAbiBuildTask : BuildTaskBase() { } outputApiFile.bufferedWriter().use { - LibraryAbiRenderer.render(parsedAbi, it, AbiRenderingSettings(sigVersion)) + LibraryAbiRenderer.render(parsedAbi, it, AbiRenderingSettings(sigVersion, renderManifest = true)) } } } diff --git a/src/main/kotlin/KotlinKlibExtractSupportedTargetsAbiTask.kt b/src/main/kotlin/KotlinKlibExtractSupportedTargetsAbiTask.kt index 822f0ed4..c74fe4d6 100644 --- a/src/main/kotlin/KotlinKlibExtractSupportedTargetsAbiTask.kt +++ b/src/main/kotlin/KotlinKlibExtractSupportedTargetsAbiTask.kt @@ -71,8 +71,6 @@ public abstract class KotlinKlibExtractSupportedTargetsAbiTask : DefaultTask() { } private fun canUseGroupAliases(): Boolean { - if (!groupTargetNames) return false - val clashingTargets = targets.get().intersect(TargetHierarchy.nonLeafTargets()) - return clashingTargets.isEmpty() + return groupTargetNames } } diff --git a/src/main/kotlin/KotlinKlibInferAbiForUnsupportedTargetTask.kt b/src/main/kotlin/KotlinKlibInferAbiForUnsupportedTargetTask.kt index 37983897..093d3443 100644 --- a/src/main/kotlin/KotlinKlibInferAbiForUnsupportedTargetTask.kt +++ b/src/main/kotlin/KotlinKlibInferAbiForUnsupportedTargetTask.kt @@ -32,7 +32,13 @@ public abstract class KotlinKlibInferAbiForUnsupportedTargetTask : DefaultTask() * The name of a target to infer a dump for. */ @Input - public lateinit var unsupportedTarget: String + public lateinit var unsupportedTargetName: String + + /** + * The name of a target to infer a dump for. + */ + @Input + public lateinit var unsupportedUnderlyingTarget: String /** * A root directory containing dumps successfully generated for each supported target. @@ -67,8 +73,9 @@ public abstract class KotlinKlibInferAbiForUnsupportedTargetTask : DefaultTask() @TaskAction internal fun generate() { + val unsupportedTarget = Target(unsupportedTargetName, unsupportedUnderlyingTarget) // find a set of supported targets that are closer to unsupported target in the hierarchy - val matchingTargets = findMatchingTargets() + val matchingTargets = findMatchingTargets(unsupportedTarget) val target2outFile = supportedTargets.keysToMap { File(outputApiDir).parentFile.resolve(it).resolve(dumpFileName) } @@ -77,7 +84,7 @@ public abstract class KotlinKlibInferAbiForUnsupportedTargetTask : DefaultTask() // a common ABI that should be shared by the unsupported target as well val commonDump = KlibAbiDumpMerger() for (target in matchingTargets) { - commonDump.addIndividualDump(Target(target), target2outFile[target]!!) + commonDump.addIndividualDump(target, target2outFile[target]!!) } commonDump.retainCommonAbi() @@ -87,7 +94,7 @@ public abstract class KotlinKlibInferAbiForUnsupportedTargetTask : DefaultTask() if (inputImageFile.exists()) { if (inputImageFile.length() > 0L) { image.loadMergedDump(inputImageFile) - image.retainTargetSpecificAbi(Target(unsupportedTarget)) + image.retainTargetSpecificAbi(unsupportedTarget) // merge common ABI with target-specific ABI commonDump.mergeTargetSpecific(image) } else { @@ -98,7 +105,7 @@ public abstract class KotlinKlibInferAbiForUnsupportedTargetTask : DefaultTask() ) } } - commonDump.overrideTargets(setOf(Target(unsupportedTarget))) + commonDump.overrideTargets(setOf(unsupportedTarget)) outputFile.bufferedWriter().use { commonDump.dump(it, KlibAbiDumpFormat(includeTargets = false)) @@ -113,8 +120,8 @@ public abstract class KotlinKlibInferAbiForUnsupportedTargetTask : DefaultTask() ) } - private fun findMatchingTargets(): Set { - var currentGroup: String? = unsupportedTarget + private fun findMatchingTargets(unsupportedTarget: Target): Set { + var currentGroup: String? = unsupportedTarget.underlyingTarget while (currentGroup != null) { // If a current group has some supported targets, use them. val groupTargets = TargetHierarchy.targets(currentGroup).intersect(supportedTargets) diff --git a/src/main/kotlin/KotlinKlibMergeAbiTask.kt b/src/main/kotlin/KotlinKlibMergeAbiTask.kt index b8c766af..c836f254 100644 --- a/src/main/kotlin/KotlinKlibMergeAbiTask.kt +++ b/src/main/kotlin/KotlinKlibMergeAbiTask.kt @@ -61,15 +61,12 @@ public abstract class KotlinKlibMergeAbiTask : DefaultTask() { internal fun merge() { val builder = KlibAbiDumpMerger() targets.forEach { targetName -> - val target = Target(targetName) - builder.addIndividualDump(target, targetToFile[targetName]!!.resolve(dumpFileName)) + builder.addIndividualDump(targetName, targetToFile[targetName]!!.resolve(dumpFileName)) } mergedFile.bufferedWriter().use { builder.dump(it, KlibAbiDumpFormat(useGroupAliases = canUseGroupAliases())) } } private fun canUseGroupAliases(): Boolean { - if (!groupTargetNames) return false - val clashingTargets = targets.intersect(TargetHierarchy.nonLeafTargets()) - return clashingTargets.isEmpty() + return groupTargetNames } } diff --git a/src/main/kotlin/klib/KlibAbiDumpFileMerger.kt b/src/main/kotlin/klib/KlibAbiDumpFileMerger.kt index 09847114..2d626bda 100644 --- a/src/main/kotlin/klib/KlibAbiDumpFileMerger.kt +++ b/src/main/kotlin/klib/KlibAbiDumpFileMerger.kt @@ -8,7 +8,35 @@ package kotlinx.validation.klib import java.io.File import java.nio.file.Files -internal data class Target(val name: String) +/** + * Target name consisting of two parts: a [name] that could be configured by a user, and an [underlyingTarget] + * that names a target platform and could not be configured by a user. + * + * When serialized, the target represented as a tuple `.`, like `ios.iosArm64`. + * If both names are the same (they are by default, unless a user decides to use a custom name), the serialized + * from is shortened to a single term. For example, `macosArm64.macosArm64` and `macosArm64` are a long and a short + * serialized forms of the same target. + */ +internal data class Target(val name: String, val underlyingTarget: String) { + companion object { + fun parse(line: String): Target { + if (!line.contains('.')) { + return Target(line) + } + val parts = line.split('.') + if (parts.size != 2 || parts.any { it.isBlank() }) { + throw IllegalStateException( + "Target has illegal name format: \"$line\", expected: ." + ) + } + return Target(parts[0], parts[1]) + } + } + + override fun toString(): String = if (name == underlyingTarget) name else "$name.$underlyingTarget" +} + +internal fun Target(name: String) = Target(name, name) internal class LinesProvider(private val lines: Iterator) : Iterator { private var nextLine: String? = null @@ -46,6 +74,10 @@ private const val TARGETS_LIST_SUFFIX = "]" private const val TARGETS_DELIMITER = ", " private const val CLASS_DECLARATION_TERMINATOR = "}" private const val INDENT_WIDTH = 4 +private const val ALIAS_PREFIX = "// Alias: " +private const val PLATFORM_PREFIX = "// Platform: " +private const val NATIVE_TARGETS_PREFIX = "// Native targets: " +private const val LIBRARY_NAME_PREFIX = "// Library unique name:" private fun String.depth(): Int { val indentation = this.takeWhile { it == ' ' }.count() @@ -62,7 +94,7 @@ private fun parseBcvTargetsLine(line: String): Set { } return trimmedLine.substring(TARGETS_LIST_PREFIX.length, trimmedLine.length - 1) .split(TARGETS_DELIMITER) - .map { Target(it) } + .map { Target.parse(it) } .toSet() } @@ -71,6 +103,18 @@ internal data class KlibAbiDumpFormat( val useGroupAliases: Boolean = false ) +private class KlibAbiDumpHeader( + val content: List, + val underlyingTarget: String? +) { + fun extractTarget(targetName: String): Target { + if (underlyingTarget == "wasm") { + return Target(targetName) + } + return Target(targetName, underlyingTarget ?: targetName) + } +} + /** * A class representing a textual KLib ABI dump, either a regular one, or a merged. */ @@ -87,38 +131,48 @@ internal class KlibAbiDumpMerger { public fun loadMergedDump(file: File) { require(file.exists()) { "File does not exist: $file" } Files.lines(file.toPath()).use { - mergeFile(emptySet(), LinesProvider(it.iterator())) + mergeFile(true, null, LinesProvider(it.iterator())) } } - public fun addIndividualDump(target: Target, file: File) { + public fun addIndividualDump(customTargetName: String, file: File) { require(file.exists()) { "File does not exist: $file" } Files.lines(file.toPath()).use { - mergeFile(setOf(target), LinesProvider(it.iterator())) + mergeFile(false, customTargetName, LinesProvider(it.iterator())) } } - private fun mergeFile(targets: Set, lines: LinesProvider) { - val isMergedFile = targets.isEmpty() - if (isMergedFile) check(this.targetsMut.isEmpty()) { "Merged dump could only be loaded once." } + public fun addIndividualDump(file: File) { + require(file.exists()) { "File does not exist: $file" } + Files.lines(file.toPath()).use { + mergeFile(false, null, LinesProvider(it.iterator())) + } + } + private fun mergeFile(isMergedFile: Boolean, targetName: String?, lines: LinesProvider) { + if (isMergedFile) check(this.targetsMut.isEmpty()) { "Merged dump could only be loaded once." } lines.checkFileFormat(isMergedFile) - val bcvTargets = if (isMergedFile) { - lines.parseTargets() - } else { - targets - } - val aliases = if (isMergedFile) { - lines.parseAliases() - } else { - emptyMap() + val aliases = mutableMapOf>() + val bcvTargets = mutableSetOf() + if (isMergedFile) { + bcvTargets.addAll(lines.parseTargets()) + aliases.putAll(lines.parseAliases()) } - val header = lines.parseFileHeader() + if (!isMergedFile) { + if (targetName == null && header.underlyingTarget == "wasm") { + throw IllegalStateException( + "Currently, there is no way to distinguish dumps generated for " + + "different Wasm targets (wasmJs and wasmWasi), " + + "please specify the actual target name explicitly" + ) + } + bcvTargets.add(header.extractTarget(targetName ?: header.underlyingTarget!!)) + } if (isMergedFile || this.targetsMut.isEmpty()) { - headerContent.addAll(header) - } else if (headerContent != header) { + headerContent.addAll(header.content) + } else if (headerContent != header.content) { throw IllegalStateException("File header doesn't match the header of other files") } this.targetsMut.addAll(bcvTargets) @@ -188,9 +242,9 @@ internal class KlibAbiDumpMerger { private fun LinesProvider.parseAliases(): Map> { val aliases = mutableMapOf>() - while (peek()?.startsWith("// Alias: ") == true) { + while (peek()?.startsWith(ALIAS_PREFIX) == true) { val line = next() - val trimmedLine = line.substring("// Alias: ".length) + val trimmedLine = line.substring(ALIAS_PREFIX.length) val separatorIdx = trimmedLine.indexOf(" => [") if (separatorIdx == -1 || !trimmedLine.endsWith(']')) { throw IllegalStateException("Invalid alias line: $line") @@ -208,18 +262,50 @@ internal class KlibAbiDumpMerger { return aliases } - private fun LinesProvider.parseFileHeader(): List { + private fun LinesProvider.parseFileHeader(): KlibAbiDumpHeader { val header = mutableListOf() + var targets: String? = null + var platform: String? = null + + // read the common head first while (hasNext()) { val next = peek()!! - if ((next.startsWith(COMMENT_PREFIX) && !next.startsWith(TARGETS_LIST_PREFIX)) || next.isBlank()) { - header.add(next) - next() - } else { + if (next.isNotBlank() && !next.startsWith(COMMENT_PREFIX)) { + throw IllegalStateException("Library header has invalid format at line \"$next\"") + } + header.add(next) + next() + if (next.startsWith(LIBRARY_NAME_PREFIX)) { break } } - return header + // then try to parse a manifest + while (hasNext()) { + val next = peek()!! + if (!next.startsWith(COMMENT_PREFIX)) break + next() + when { + next.startsWith(PLATFORM_PREFIX) -> { + platform = next.split(": ")[1].trim() + } + + next.startsWith(NATIVE_TARGETS_PREFIX) -> { + targets = next.split(": ")[1].trim() + } + } + } + if (platform == null) { + return KlibAbiDumpHeader(header, null) + } + // TODO + if (targets?.contains(",") == true) throw IllegalStateException("Multi-target klibs are not supported.") + val underlyingTarget = when (platform) { + "NATIVE" -> konanTargetNameMapping[targets] + ?: throw IllegalStateException("The manifest is missing targets for native platform") + + else -> platform.toLowerCase() + } + return KlibAbiDumpHeader(header, underlyingTarget) } private fun LinesProvider.checkFileFormat(isMergedFile: Boolean) { @@ -297,11 +383,11 @@ internal class KlibAbiDumpMerger { private fun createFormatter(dumpFormat: KlibAbiDumpFormat): KLibsTargetsFormatter { return if (dumpFormat.useGroupAliases) { for (target in targets) { - val node = TargetHierarchy.hierarchyIndex[target.name] + val node = TargetHierarchy.hierarchyIndex[target.underlyingTarget] if (node != null && node.allLeafs.size == 1 && node.allLeafs.first() != node.node.name) { throw IllegalStateException( "Can't use target aliases as one of the this dump's targets" + - " has the same name as a group in the default targets hierarchy: ${target.name}" + " has the same name as a group in the default targets hierarchy: $target" ) } } @@ -511,8 +597,8 @@ private object DeclarationsComparator : Comparator { c0.text.compareTo(c1.text) } else { if (c0.targets.size == c1.targets.size) { - val c0targets = c0.targets.asSequence().map { it.name }.sorted().iterator() - val c1targets = c1.targets.asSequence().map { it.name }.sorted().iterator() + val c0targets = c0.targets.asSequence().map { it.toString() }.sorted().iterator() + val c1targets = c1.targets.asSequence().map { it.toString() }.sorted().iterator() var result = 0 while (c1targets.hasNext() && c0targets.hasNext() && result == 0) { result = c0targets.next().compareTo(c1targets.next()) @@ -538,8 +624,8 @@ private object DefaultFormatter : KLibsTargetsFormatter { } override fun formatDeclarationTargets(targets: Set): String { - return targets.sortedBy { it.name } - .joinToString(TARGETS_DELIMITER, TARGETS_LIST_PREFIX, TARGETS_LIST_SUFFIX) { it.name } + return targets.map { it.toString() }.sorted() + .joinToString(TARGETS_DELIMITER, TARGETS_LIST_PREFIX, TARGETS_LIST_SUFFIX) { it } } } @@ -556,9 +642,9 @@ private class GroupingFormatter(klibDump: KlibAbiDumpMerger) : KLibsTargetsForma .sortedWith(compareBy({ it.value.allLeafs.size }, { it.key })) .forEach { // intersect with all targets to use only enabled targets in aliases - val availableTargets = it.value.allLeafs.map { - Target(it) - }.intersect(allTargets) + // intersection is based on underlying target name as a set of such names is fixed + val leafs = it.value.allLeafs + val availableTargets = allTargets.asSequence().filter { leafs.contains(it.underlyingTarget) }.toSet() if (availableTargets.isNotEmpty()) { aliasesBuilder.add(Alias(it.key, availableTargets)) } @@ -597,15 +683,15 @@ private class GroupingFormatter(klibDump: KlibAbiDumpMerger) : KLibsTargetsForma override fun formatHeader(targets: Set): String { return buildString { append( - targets.asSequence().map { it.name }.sorted().joinToString( + targets.asSequence().map { it.toString() }.sorted().joinToString( prefix = TARGETS_LIST_PREFIX, postfix = TARGETS_LIST_SUFFIX, separator = TARGETS_DELIMITER ) ) aliases.forEach { - append("\n// Alias: ${it.name} => [") - append(it.targets.map { it.name }.sorted().joinToString(TARGETS_DELIMITER)) + append("\n$ALIAS_PREFIX${it.name} => [") + append(it.targets.map { it.toString() }.sorted().joinToString(TARGETS_DELIMITER)) append(TARGETS_LIST_SUFFIX) } } @@ -620,7 +706,7 @@ private class GroupingFormatter(klibDump: KlibAbiDumpMerger) : KLibsTargetsForma resultingTargets.add(alias.name) } } - resultingTargets.addAll(targetsMut.map { it.name }) + resultingTargets.addAll(targetsMut.map { it.toString() }) return resultingTargets.sorted().joinToString( prefix = TARGETS_LIST_PREFIX, postfix = TARGETS_LIST_SUFFIX, diff --git a/src/main/kotlin/klib/TargetHierarchy.kt b/src/main/kotlin/klib/TargetHierarchy.kt index c3c46163..cb25bb62 100644 --- a/src/main/kotlin/klib/TargetHierarchy.kt +++ b/src/main/kotlin/klib/TargetHierarchy.kt @@ -33,6 +33,7 @@ internal object TargetHierarchy { Node("js"), Node("wasmJs"), Node("wasmWasi"), + Node("wasm32"), Node( "native", Node( @@ -44,7 +45,9 @@ internal object TargetHierarchy { "linux", Node("linuxArm64"), Node("linuxArm32Hfp"), - Node("linuxX64") + Node("linuxX64"), + Node("linuxMips32"), + Node("linuxMipsel32") ), Node( "androidNative", @@ -113,14 +116,34 @@ internal object TargetHierarchy { fun targets(targetOrGroup: String): Set { return hierarchyIndex[targetOrGroup]?.allLeafs ?: emptySet() } - - fun nonLeafTargets(): Set { - return hierarchyIndex.values.asSequence() - .filter { - it.allLeafs.size > 1 || (it.allLeafs.size == 1 && it.allLeafs.first() != it.node.name) - } - .map { it.node.name } - .toSet() - } } +internal val konanTargetNameMapping = mapOf( + "android_x64" to "androidNativeX64", + "android_x86" to "androidNativeX86", + "android_arm32" to "androidNativeArm32", + "android_arm64" to "androidNativeArm64", + "ios_arm64" to "iosArm64", + "ios_x64" to "iosX64", + "ios_simulator_arm64" to "iosSimulatorArm64", + "watchos_arm32" to "watchosArm32", + "watchos_arm64" to "watchosArm64", + "watchos_x64" to "watchosX64", + "watchos_simulator_arm64" to "watchosSimulatorArm64", + "watchos_device_arm64" to "watchosDeviceArm64", + "tvos_arm64" to "tvosArm64", + "tvos_x64" to "tvosX64", + "tvos_simulator_arm64" to "tvosSimulatorArm64", + "linux_x64" to "linuxX64", + "mingw_x64" to "mingwX64", + "macos_x64" to "macosX64", + "macos_arm64" to "macosArm64", + "linux_arm64" to "linuxArm64", + "ios_arm32" to "iosArm32", + "watchos_x86" to "watchosX86", + "linux_arm32_hfp" to "linuxArm32Hfp", + "mingw_x86" to "mingwX86", + "linux_mips32" to "linuxMips32", + "linux_mipsel32" to "linuxMipsel32", + "wasm32" to "wasm32" +) diff --git a/src/test/kotlin/tests/KlibAbiMergingTest.kt b/src/test/kotlin/tests/KlibAbiMergingTest.kt index a156284b..680ea49c 100644 --- a/src/test/kotlin/tests/KlibAbiMergingTest.kt +++ b/src/test/kotlin/tests/KlibAbiMergingTest.kt @@ -16,9 +16,7 @@ import java.io.FileWriter import java.nio.file.Files import java.util.UUID import kotlin.random.Random -import kotlin.test.assertContentEquals -import kotlin.test.assertFails -import kotlin.test.assertFailsWith +import kotlin.test.* class KlibAbiMergingTest { @JvmField @@ -52,12 +50,37 @@ class KlibAbiMergingTest { return file } + @Test + fun testTargetNames() { + assertEquals("a.b", Target("a", "b").toString()) + assertEquals("a", Target("a").toString()) + assertEquals("a", Target("a", "a").toString()) + + assertFailsWith { Target.parse("a.b.c") } + assertFailsWith { Target.parse("a.") } + assertFailsWith { Target.parse(".a") } + + Target.parse("a.b").also { + assertEquals("a", it.name) + assertEquals("b", it.underlyingTarget) + } + + Target.parse("a.a").also { + assertEquals("a", it.name) + assertEquals("a", it.underlyingTarget) + } + + Target.parse("a").also { + assertEquals("a", it.name) + assertEquals("a", it.underlyingTarget) + } + } + @Test fun identicalDumpFiles() { val klib = KlibAbiDumpMerger() - listOf(Target("macosArm64"), Target("linuxX64")).forEach { - klib.addIndividualDump(it, file("/merge/identical/dump.abi")) - } + klib.addIndividualDump(file("/merge/identical/dump_macos_arm64.abi")) + klib.addIndividualDump(file("/merge/identical/dump_linux_x64.abi")) val merged = dumpToFile(klib) assertContentEquals( @@ -69,9 +92,8 @@ class KlibAbiMergingTest { @Test fun identicalDumpFilesWithAliases() { val klib = KlibAbiDumpMerger() - listOf(Target("macosArm64"), Target("linuxX64")).forEach { - klib.addIndividualDump(it, file("/merge/identical/dump.abi")) - } + klib.addIndividualDump(file("/merge/identical/dump_macos_arm64.abi")) + klib.addIndividualDump(file("/merge/identical/dump_linux_x64.abi")) val merged = dumpToFile(klib, useAliases = true) // there are no groups other than "all", so no aliases will be added @@ -89,7 +111,7 @@ class KlibAbiMergingTest { val klib = KlibAbiDumpMerger() targets.shuffle(random) targets.forEach { - klib.addIndividualDump(Target(it), file("/merge/diverging/$it.api")) + klib.addIndividualDump(file("/merge/diverging/$it.api")) } val merged = dumpToFile(klib) assertContentEquals( @@ -108,7 +130,7 @@ class KlibAbiMergingTest { val targets = mutableListOf("androidNativeArm64", "linuxArm64", "linuxX64", "tvOsX64") targets.shuffle(random) targets.forEach { - klib.addIndividualDump(Target(it), file("/merge/diverging/$it.api")) + klib.addIndividualDump(file("/merge/diverging/$it.api")) } val merged = dumpToFile(klib, useAliases = true) assertContentEquals( @@ -134,13 +156,13 @@ class KlibAbiMergingTest { fun mergeDumpsWithDivergedHeaders() { val klib = KlibAbiDumpMerger() klib.addIndividualDump( - Target("linuxArm64"), + "linuxArm64", file("/merge/header-mismatch/v1.abi") ) assertFailsWith { klib.addIndividualDump( - Target("linuxX64"), + "linuxX64", file("/merge/header-mismatch/v2.abi") ) } @@ -154,7 +176,7 @@ class KlibAbiMergingTest { val targets = listOf("androidNativeArm64", "linuxArm64", "linuxX64", "tvOsX64") targets.forEach { target -> klib.remove(Target(target)) - klib.addIndividualDump(Target(target), file("/merge/diverging/$target.api")) + klib.addIndividualDump(file("/merge/diverging/$target.api")) } val merged = dumpToFile(klib) @@ -247,21 +269,75 @@ class KlibAbiMergingTest { assertFails { KlibAbiDumpMerger().addIndividualDump( - Target("linuxX64"), file("/merge/illegalFiles/emptyFile.txt") + "linuxX64", file("/merge/illegalFiles/emptyFile.txt") ) } assertFails { KlibAbiDumpMerger().addIndividualDump( - Target("linuxX64"), file("/merge/illegalFiles/nonDumpFile.txt") + "linuxX64", file("/merge/illegalFiles/nonDumpFile.txt") ) } assertFails { // Not a single-target dump KlibAbiDumpMerger().addIndividualDump( - Target("linuxX64"), file("/merge/diverging/merged.api") + "linuxX64", file("/merge/diverging/merged.api") ) } } + + @Test + fun webTargets() { + val klib = KlibAbiDumpMerger() + klib.addIndividualDump(file("/merge/webTargets/js.abi")) + klib.addIndividualDump("wasmWasi", file("/merge/webTargets/wasmWasi.abi")) + klib.addIndividualDump("wasmJs", file("/merge/webTargets/wasmJs.abi")) + + val merged = dumpToFile(klib) + + assertContentEquals( + lines("/merge/webTargets/merged.abi"), + Files.readAllLines(merged.toPath()).asSequence() + ) + } + + @Test + fun unqualifiedWasmTarget() { + // currently, there's no way to distinguish wasmWasi from wasmJs + assertFailsWith { + KlibAbiDumpMerger().addIndividualDump(file("/merge/webTargets/wasmWasi.abi")) + } + } + + @Test + fun customTargetNames() { + val lib = KlibAbiDumpMerger().apply { + addIndividualDump("android", file("/merge/diverging/androidNativeArm64.api")) + addIndividualDump("linux", file("/merge/diverging/linuxArm64.api")) + addIndividualDump(file("/merge/diverging/linuxX64.api")) + addIndividualDump(file("/merge/diverging/tvOsX64.api")) + } + + val dump = dumpToFile(lib, useAliases = true) + assertContentEquals( + lines("/merge/diverging/merged_with_aliases_and_custom_names.abi"), + Files.readAllLines(dump.toPath()).asSequence() + ) + } + + @Test + fun customTargetExtraction() { + val lib = KlibAbiDumpMerger().apply { + loadMergedDump(file("/merge/diverging/merged_with_aliases_and_custom_names.abi")) + } + val targets = lib.targets.filter { it.underlyingTarget != "linuxArm64" } + targets.forEach { lib.remove(it) } + println(buildString { + lib.dump(this, KlibAbiDumpFormat( + useGroupAliases = false, + includeTargets = false + )) + }) + } } diff --git a/src/test/kotlin/tests/TargetHierarchyTest.kt b/src/test/kotlin/tests/TargetHierarchyTest.kt index 9b3c308e..a18b3a4b 100644 --- a/src/test/kotlin/tests/TargetHierarchyTest.kt +++ b/src/test/kotlin/tests/TargetHierarchyTest.kt @@ -5,9 +5,12 @@ package kotlinx.validation.klib +import org.jetbrains.kotlin.konan.target.KonanTarget import org.junit.Test import kotlin.test.assertContentEquals import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertTrue class TargetHierarchyTest { @Test @@ -34,6 +37,20 @@ class TargetHierarchyTest { assertEquals(emptySet(), TargetHierarchy.targets("unknown")) } + @Test + fun testEveryMappedTargetIsWithinTheHierarchy() { + konanTargetNameMapping.forEach { (underlyingTarget, name) -> + assertNotNull(TargetHierarchy.parent(name), + "Target $name.$underlyingTarget is missing from the hierarchy.") + } + } + + @Test + fun testAllTargetsAreMapped() { + val notMappedTargets = KonanTarget.predefinedTargets.keys.subtract(konanTargetNameMapping.keys) + assertTrue(notMappedTargets.isEmpty(), "Following targets are not mapped: $notMappedTargets") + } + @OptIn(ExperimentalStdlibApi::class) private fun hierarchyFrom(groupOrTarget: String): List { return buildList { diff --git a/src/test/resources/merge/diverging/androidNativeArm64.api b/src/test/resources/merge/diverging/androidNativeArm64.api index 9bc95b04..970b8860 100644 --- a/src/test/resources/merge/diverging/androidNativeArm64.api +++ b/src/test/resources/merge/diverging/androidNativeArm64.api @@ -4,6 +4,10 @@ // - Show declarations: true // Library unique name: +// Platform: NATIVE +// Native targets: android_arm64 +// Compiler version: 1.9.22 +// ABI version: 1.8.0 final class org.example/ShardedClass { // org.example/ShardedClass|null[0] final val value // org.example/ShardedClass.value|{}value[0] final fun (): kotlin/Int // org.example/ShardedClass.value.|(){}[0] diff --git a/src/test/resources/merge/diverging/linuxArm64.api b/src/test/resources/merge/diverging/linuxArm64.api index cdaeeaba..ee8b81c8 100644 --- a/src/test/resources/merge/diverging/linuxArm64.api +++ b/src/test/resources/merge/diverging/linuxArm64.api @@ -4,6 +4,10 @@ // - Show declarations: true // Library unique name: +// Platform: NATIVE +// Native targets: linux_arm64 +// Compiler version: 1.9.22 +// ABI version: 1.8.0 final class org.example/ShardedClass { // org.example/ShardedClass|null[0] final val value // org.example/ShardedClass.value|{}value[0] final fun (): kotlin/Int // org.example/ShardedClass.value.|(){}[0] diff --git a/src/test/resources/merge/diverging/linuxX64.api b/src/test/resources/merge/diverging/linuxX64.api index cdaeeaba..e827b6c6 100644 --- a/src/test/resources/merge/diverging/linuxX64.api +++ b/src/test/resources/merge/diverging/linuxX64.api @@ -4,6 +4,10 @@ // - Show declarations: true // Library unique name: +// Platform: NATIVE +// Native targets: linux_x64 +// Compiler version: 1.9.22 +// ABI version: 1.8.0 final class org.example/ShardedClass { // org.example/ShardedClass|null[0] final val value // org.example/ShardedClass.value|{}value[0] final fun (): kotlin/Int // org.example/ShardedClass.value.|(){}[0] diff --git a/src/test/resources/merge/diverging/merged.abi b/src/test/resources/merge/diverging/merged.abi index ab99abaa..96ced25f 100644 --- a/src/test/resources/merge/diverging/merged.abi +++ b/src/test/resources/merge/diverging/merged.abi @@ -1,5 +1,5 @@ // Merged KLib ABI Dump -// Targets: [androidNativeArm64, linuxArm64, linuxX64, tvOsX64] +// Targets: [androidNativeArm64, linuxArm64, linuxX64, tvosX64] // Rendering settings: // - Signature version: 2 // - Show manifest properties: false @@ -25,7 +25,7 @@ final class org.example/X { // org.example/X|null[0] final fun (org.example/X).org.example/add(org.example/X): org.example/X // org.example/add|add@org.example.X(org.example.X){}[0] // Targets: [androidNativeArm64] final fun org.example/add(org.example/X, org.example/X): org.example/X // org.example/add|add(org.example.X;org.example.X){}[0] -// Targets: [tvOsX64] +// Targets: [tvosX64] final fun (kotlin/Int).org.example/add(kotlin/Int): kotlin/Int // org.example/add|add@kotlin.Int(kotlin.Int){}[0] -// Targets: [tvOsX64] +// Targets: [tvosX64] final fun org.example/add(kotlin/Int, kotlin/Int): kotlin/Int // org.example/add|add(kotlin.Int;kotlin.Int){}[0] diff --git a/src/test/resources/merge/diverging/merged_with_aliases.abi b/src/test/resources/merge/diverging/merged_with_aliases.abi index 9e57d3c7..dedae58f 100644 --- a/src/test/resources/merge/diverging/merged_with_aliases.abi +++ b/src/test/resources/merge/diverging/merged_with_aliases.abi @@ -1,5 +1,5 @@ // Merged KLib ABI Dump -// Targets: [androidNativeArm64, linuxArm64, linuxX64, tvOsX64] +// Targets: [androidNativeArm64, linuxArm64, linuxX64, tvosX64] // Alias: linux => [linuxArm64, linuxX64] // Rendering settings: // - Signature version: 2 @@ -26,7 +26,7 @@ final class org.example/X { // org.example/X|null[0] final fun (org.example/X).org.example/add(org.example/X): org.example/X // org.example/add|add@org.example.X(org.example.X){}[0] // Targets: [androidNativeArm64] final fun org.example/add(org.example/X, org.example/X): org.example/X // org.example/add|add(org.example.X;org.example.X){}[0] -// Targets: [tvOsX64] +// Targets: [tvosX64] final fun (kotlin/Int).org.example/add(kotlin/Int): kotlin/Int // org.example/add|add@kotlin.Int(kotlin.Int){}[0] -// Targets: [tvOsX64] +// Targets: [tvosX64] final fun org.example/add(kotlin/Int, kotlin/Int): kotlin/Int // org.example/add|add(kotlin.Int;kotlin.Int){}[0] diff --git a/src/test/resources/merge/diverging/merged_with_aliases_and_custom_names.abi b/src/test/resources/merge/diverging/merged_with_aliases_and_custom_names.abi new file mode 100644 index 00000000..3c338b15 --- /dev/null +++ b/src/test/resources/merge/diverging/merged_with_aliases_and_custom_names.abi @@ -0,0 +1,32 @@ +// Merged KLib ABI Dump +// Targets: [android.androidNativeArm64, linux.linuxArm64, linuxX64, tvosX64] +// Alias: linux => [linux.linuxArm64, linuxX64] +// Rendering settings: +// - Signature version: 2 +// - Show manifest properties: false +// - Show declarations: true + +// Library unique name: +final class org.example/ShardedClass { // org.example/ShardedClass|null[0] + constructor (kotlin/Int) // org.example/ShardedClass.|(kotlin.Int){}[0] + final fun add(kotlin/Int): kotlin/Int // org.example/ShardedClass.add|add(kotlin.Int){}[0] + final val value // org.example/ShardedClass.value|{}value[0] + final fun (): kotlin/Int // org.example/ShardedClass.value.|(){}[0] +} +final fun org.example/ShardedClass(kotlin/Int, kotlin/Float, kotlin/Long): org.example/ShardedClass // org.example/ShardedClass|ShardedClass(kotlin.Int;kotlin.Float;kotlin.Long){}[0] +// Targets: [linux] +final fun (kotlin/Long).org.example/add(kotlin/Long): kotlin/Long // org.example/add|add@kotlin.Long(kotlin.Long){}[0] +// Targets: [linux] +final fun org.example/add(kotlin/Long, kotlin/Long): kotlin/Long // org.example/add|add(kotlin.Long;kotlin.Long){}[0] +// Targets: [android.androidNativeArm64] +final class org.example/X { // org.example/X|null[0] + constructor (kotlin/Int) // org.example/X.|(kotlin.Int){}[0] +} +// Targets: [android.androidNativeArm64] +final fun (org.example/X).org.example/add(org.example/X): org.example/X // org.example/add|add@org.example.X(org.example.X){}[0] +// Targets: [android.androidNativeArm64] +final fun org.example/add(org.example/X, org.example/X): org.example/X // org.example/add|add(org.example.X;org.example.X){}[0] +// Targets: [tvosX64] +final fun (kotlin/Int).org.example/add(kotlin/Int): kotlin/Int // org.example/add|add@kotlin.Int(kotlin.Int){}[0] +// Targets: [tvosX64] +final fun org.example/add(kotlin/Int, kotlin/Int): kotlin/Int // org.example/add|add(kotlin.Int;kotlin.Int){}[0] diff --git a/src/test/resources/merge/diverging/tvOsX64.api b/src/test/resources/merge/diverging/tvOsX64.api index a23dd25d..72bf3f99 100644 --- a/src/test/resources/merge/diverging/tvOsX64.api +++ b/src/test/resources/merge/diverging/tvOsX64.api @@ -4,6 +4,10 @@ // - Show declarations: true // Library unique name: +// Platform: NATIVE +// Native targets: tvos_x64 +// Compiler version: 1.9.22 +// ABI version: 1.8.0 final class org.example/ShardedClass { // org.example/ShardedClass|null[0] final val value // org.example/ShardedClass.value|{}value[0] final fun (): kotlin/Int // org.example/ShardedClass.value.|(){}[0] diff --git a/src/test/resources/merge/identical/dump_linux_x64.abi b/src/test/resources/merge/identical/dump_linux_x64.abi new file mode 100644 index 00000000..ca9ca7cf --- /dev/null +++ b/src/test/resources/merge/identical/dump_linux_x64.abi @@ -0,0 +1,17 @@ +// Rendering settings: +// - Signature version: 2 +// - Show manifest properties: false +// - Show declarations: true + +// Library unique name: +// Platform: NATIVE +// Native targets: linux_x64 +// Compiler version: 1.9.22 +// ABI version: 1.8.0 +final class org.example/ShardedClass { // org.example/ShardedClass|null[0] + final val value // org.example/ShardedClass.value|{}value[0] + final fun (): kotlin/Int // org.example/ShardedClass.value.|(){}[0] + constructor (kotlin/Int) // org.example/ShardedClass.|(kotlin.Int){}[0] + final fun add(kotlin/Int): kotlin/Int // org.example/ShardedClass.add|add(kotlin.Int){}[0] +} +final fun org.example/ShardedClass(kotlin/Int, kotlin/Float, kotlin/Long): org.example/ShardedClass // org.example/ShardedClass|ShardedClass(kotlin.Int;kotlin.Float;kotlin.Long){}[0] diff --git a/src/test/resources/merge/identical/dump_macos_arm64.abi b/src/test/resources/merge/identical/dump_macos_arm64.abi new file mode 100644 index 00000000..a5105085 --- /dev/null +++ b/src/test/resources/merge/identical/dump_macos_arm64.abi @@ -0,0 +1,17 @@ +// Rendering settings: +// - Signature version: 2 +// - Show manifest properties: false +// - Show declarations: true + +// Library unique name: +// Platform: NATIVE +// Native targets: macos_arm64 +// Compiler version: 1.9.22 +// ABI version: 1.8.0 +final class org.example/ShardedClass { // org.example/ShardedClass|null[0] + final val value // org.example/ShardedClass.value|{}value[0] + final fun (): kotlin/Int // org.example/ShardedClass.value.|(){}[0] + constructor (kotlin/Int) // org.example/ShardedClass.|(kotlin.Int){}[0] + final fun add(kotlin/Int): kotlin/Int // org.example/ShardedClass.add|add(kotlin.Int){}[0] +} +final fun org.example/ShardedClass(kotlin/Int, kotlin/Float, kotlin/Long): org.example/ShardedClass // org.example/ShardedClass|ShardedClass(kotlin.Int;kotlin.Float;kotlin.Long){}[0] diff --git a/src/test/resources/merge/webTargets/js.abi b/src/test/resources/merge/webTargets/js.abi new file mode 100644 index 00000000..0afbc790 --- /dev/null +++ b/src/test/resources/merge/webTargets/js.abi @@ -0,0 +1,16 @@ +// Rendering settings: +// - Signature version: 2 +// - Show manifest properties: false +// - Show declarations: true + +// Library unique name: +// Platform: JS +// Compiler version: 1.9.22 +// ABI version: 1.8.0 +final class org.example/ShardedClass { // org.example/ShardedClass|null[0] + final val value // org.example/ShardedClass.value|{}value[0] + final fun (): kotlin/Int // org.example/ShardedClass.value.|(){}[0] + constructor (kotlin/Int) // org.example/ShardedClass.|(kotlin.Int){}[0] + final fun add(kotlin/Int): kotlin/Int // org.example/ShardedClass.add|add(kotlin.Int){}[0] +} +final fun org.example/ShardedClass(kotlin/Int, kotlin/Float, kotlin/Long): org.example/ShardedClass // org.example/ShardedClass|ShardedClass(kotlin.Int;kotlin.Float;kotlin.Long){}[0] diff --git a/src/test/resources/merge/identical/dump.abi b/src/test/resources/merge/webTargets/merged.abi similarity index 93% rename from src/test/resources/merge/identical/dump.abi rename to src/test/resources/merge/webTargets/merged.abi index 086d9048..451047a7 100644 --- a/src/test/resources/merge/identical/dump.abi +++ b/src/test/resources/merge/webTargets/merged.abi @@ -1,3 +1,5 @@ +// Merged KLib ABI Dump +// Targets: [js, wasmJs, wasmWasi] // Rendering settings: // - Signature version: 2 // - Show manifest properties: false @@ -5,9 +7,9 @@ // Library unique name: final class org.example/ShardedClass { // org.example/ShardedClass|null[0] - final val value // org.example/ShardedClass.value|{}value[0] - final fun (): kotlin/Int // org.example/ShardedClass.value.|(){}[0] constructor (kotlin/Int) // org.example/ShardedClass.|(kotlin.Int){}[0] final fun add(kotlin/Int): kotlin/Int // org.example/ShardedClass.add|add(kotlin.Int){}[0] + final val value // org.example/ShardedClass.value|{}value[0] + final fun (): kotlin/Int // org.example/ShardedClass.value.|(){}[0] } final fun org.example/ShardedClass(kotlin/Int, kotlin/Float, kotlin/Long): org.example/ShardedClass // org.example/ShardedClass|ShardedClass(kotlin.Int;kotlin.Float;kotlin.Long){}[0] diff --git a/src/test/resources/merge/webTargets/wasmJs.abi b/src/test/resources/merge/webTargets/wasmJs.abi new file mode 100644 index 00000000..56ba4e9b --- /dev/null +++ b/src/test/resources/merge/webTargets/wasmJs.abi @@ -0,0 +1,16 @@ +// Rendering settings: +// - Signature version: 2 +// - Show manifest properties: false +// - Show declarations: true + +// Library unique name: +// Platform: WASM +// Compiler version: 1.9.22 +// ABI version: 1.8.0 +final class org.example/ShardedClass { // org.example/ShardedClass|null[0] + final val value // org.example/ShardedClass.value|{}value[0] + final fun (): kotlin/Int // org.example/ShardedClass.value.|(){}[0] + constructor (kotlin/Int) // org.example/ShardedClass.|(kotlin.Int){}[0] + final fun add(kotlin/Int): kotlin/Int // org.example/ShardedClass.add|add(kotlin.Int){}[0] +} +final fun org.example/ShardedClass(kotlin/Int, kotlin/Float, kotlin/Long): org.example/ShardedClass // org.example/ShardedClass|ShardedClass(kotlin.Int;kotlin.Float;kotlin.Long){}[0] diff --git a/src/test/resources/merge/webTargets/wasmWasi.abi b/src/test/resources/merge/webTargets/wasmWasi.abi new file mode 100644 index 00000000..56ba4e9b --- /dev/null +++ b/src/test/resources/merge/webTargets/wasmWasi.abi @@ -0,0 +1,16 @@ +// Rendering settings: +// - Signature version: 2 +// - Show manifest properties: false +// - Show declarations: true + +// Library unique name: +// Platform: WASM +// Compiler version: 1.9.22 +// ABI version: 1.8.0 +final class org.example/ShardedClass { // org.example/ShardedClass|null[0] + final val value // org.example/ShardedClass.value|{}value[0] + final fun (): kotlin/Int // org.example/ShardedClass.value.|(){}[0] + constructor (kotlin/Int) // org.example/ShardedClass.|(kotlin.Int){}[0] + final fun add(kotlin/Int): kotlin/Int // org.example/ShardedClass.add|add(kotlin.Int){}[0] +} +final fun org.example/ShardedClass(kotlin/Int, kotlin/Float, kotlin/Long): org.example/ShardedClass // org.example/ShardedClass|ShardedClass(kotlin.Int;kotlin.Float;kotlin.Long){}[0] From 617189f38cb4b2d87533a3dbecd670a4eb1af344 Mon Sep 17 00:00:00 2001 From: Filipp Zhinkin Date: Thu, 29 Feb 2024 17:45:57 +0100 Subject: [PATCH 23/53] Fix unsupported targets filtration --- api/binary-compatibility-validator.api | 6 +- .../validation/test/KLibVerificationTests.kt | 4 +- ...lDeclarations.klib.with.guessed.linux.dump | 67 +++++++++++++++++++ .../grouping/clashingTargetNames.gradle.kts | 2 +- .../BinaryCompatibilityValidatorPlugin.kt | 21 +++--- ...linKlibInferAbiForUnsupportedTargetTask.kt | 7 +- 6 files changed, 90 insertions(+), 17 deletions(-) create mode 100644 src/functionalTest/resources/examples/classes/TopLevelDeclarations.klib.with.guessed.linux.dump diff --git a/api/binary-compatibility-validator.api b/api/binary-compatibility-validator.api index 75c7bbcd..b1c8cda2 100644 --- a/api/binary-compatibility-validator.api +++ b/api/binary-compatibility-validator.api @@ -124,7 +124,7 @@ public abstract class kotlinx/validation/KotlinKlibInferAbiForUnsupportedTargetT public field inputImageFile Ljava/io/File; public field outputApiDir Ljava/lang/String; public field outputFile Ljava/io/File; - public field supportedTargets Ljava/util/Set; + public field supportedTargets Lorg/gradle/api/provider/Provider; public field unsupportedTargetName Ljava/lang/String; public field unsupportedUnderlyingTarget Ljava/lang/String; public fun ()V @@ -132,14 +132,14 @@ public abstract class kotlinx/validation/KotlinKlibInferAbiForUnsupportedTargetT public final fun getInputImageFile ()Ljava/io/File; public final fun getOutputApiDir ()Ljava/lang/String; public final fun getOutputFile ()Ljava/io/File; - public final fun getSupportedTargets ()Ljava/util/Set; + public final fun getSupportedTargets ()Lorg/gradle/api/provider/Provider; public final fun getUnsupportedTargetName ()Ljava/lang/String; public final fun getUnsupportedUnderlyingTarget ()Ljava/lang/String; public final fun setDumpFileName (Ljava/lang/String;)V public final fun setInputImageFile (Ljava/io/File;)V public final fun setOutputApiDir (Ljava/lang/String;)V public final fun setOutputFile (Ljava/io/File;)V - public final fun setSupportedTargets (Ljava/util/Set;)V + public final fun setSupportedTargets (Lorg/gradle/api/provider/Provider;)V public final fun setUnsupportedTargetName (Ljava/lang/String;)V public final fun setUnsupportedUnderlyingTarget (Ljava/lang/String;)V } diff --git a/src/functionalTest/kotlin/kotlinx/validation/test/KLibVerificationTests.kt b/src/functionalTest/kotlin/kotlinx/validation/test/KLibVerificationTests.kt index 54d62409..fe1e1825 100644 --- a/src/functionalTest/kotlin/kotlinx/validation/test/KLibVerificationTests.kt +++ b/src/functionalTest/kotlin/kotlinx/validation/test/KLibVerificationTests.kt @@ -369,13 +369,13 @@ internal class KLibVerificationTests : BaseKotlinGradleTest() { addToSrcSet("/examples/classes/TopLevelDeclarations.kt") addToSrcSet("/examples/classes/AnotherBuildConfigLinuxArm64.kt", "linuxMain") runner { - arguments.add("-P$BANNED_TARGETS_PROPERTY_NAME=linuxx") + arguments.add("-P$BANNED_TARGETS_PROPERTY_NAME=linux") arguments.add(":klibApiDump") } } checkKlibDump( - runner.build(), "/examples/classes/TopLevelDeclarations.klib.with.linux.dump", + runner.build(), "/examples/classes/TopLevelDeclarations.klib.with.guessed.linux.dump", dumpTask = ":klibApiDump" ) } diff --git a/src/functionalTest/resources/examples/classes/TopLevelDeclarations.klib.with.guessed.linux.dump b/src/functionalTest/resources/examples/classes/TopLevelDeclarations.klib.with.guessed.linux.dump new file mode 100644 index 00000000..44b5e3f9 --- /dev/null +++ b/src/functionalTest/resources/examples/classes/TopLevelDeclarations.klib.with.guessed.linux.dump @@ -0,0 +1,67 @@ +// Merged KLib ABI Dump +// Targets: [androidNativeArm32, androidNativeArm64, androidNativeX64, androidNativeX86, linuxArm64, linux.linuxX64, mingwX64] +// Rendering settings: +// - Signature version: 2 +// - Show manifest properties: true +// - Show declarations: true + +// Library unique name: +abstract class examples.classes/AC { // examples.classes/AC|null[0] + abstract fun a() // examples.classes/AC.a|a(){}[0] + constructor () // examples.classes/AC.|(){}[0] + final fun b() // examples.classes/AC.b|b(){}[0] +} +abstract interface examples.classes/I // examples.classes/I|null[0] +final class examples.classes/C { // examples.classes/C|null[0] + constructor (kotlin/Any) // examples.classes/C.|(kotlin.Any){}[0] + final fun m() // examples.classes/C.m|m(){}[0] + final val v // examples.classes/C.v|{}v[0] + final fun (): kotlin/Any // examples.classes/C.v.|(){}[0] +} +final class examples.classes/D { // examples.classes/D|null[0] + constructor (kotlin/Int) // examples.classes/D.|(kotlin.Int){}[0] + final fun component1(): kotlin/Int // examples.classes/D.component1|component1(){}[0] + final fun copy(kotlin/Int =...): examples.classes/D // examples.classes/D.copy|copy(kotlin.Int){}[0] + final fun equals(kotlin/Any?): kotlin/Boolean // examples.classes/D.equals|equals(kotlin.Any?){}[0] + final fun hashCode(): kotlin/Int // examples.classes/D.hashCode|hashCode(){}[0] + final fun toString(): kotlin/String // examples.classes/D.toString|toString(){}[0] + final val x // examples.classes/D.x|{}x[0] + final fun (): kotlin/Int // examples.classes/D.x.|(){}[0] +} +final class examples.classes/Outer { // examples.classes/Outer|null[0] + constructor () // examples.classes/Outer.|(){}[0] + final class Nested { // examples.classes/Outer.Nested|null[0] + constructor () // examples.classes/Outer.Nested.|(){}[0] + final inner class Inner { // examples.classes/Outer.Nested.Inner|null[0] + constructor () // examples.classes/Outer.Nested.Inner.|(){}[0] + } + } +} +final const val examples.classes/con // examples.classes/con|{}con[0] + final fun (): kotlin/String // examples.classes/con.|(){}[0] +final enum class examples.classes/E : kotlin/Enum { // examples.classes/E|null[0] + enum entry A // examples.classes/E.A|null[0] + enum entry B // examples.classes/E.B|null[0] + enum entry C // examples.classes/E.C|null[0] + final fun valueOf(kotlin/String): examples.classes/E // examples.classes/E.valueOf|valueOf#static(kotlin.String){}[0] + final fun values(): kotlin/Array // examples.classes/E.values|values#static(){}[0] + final val entries // examples.classes/E.entries|#static{}entries[0] + final fun (): kotlin.enums/EnumEntries // examples.classes/E.entries.|#static(){}[0] +} +final fun <#A: kotlin/Any?> examples.classes/consume(#A) // examples.classes/consume|consume(0:0){0§}[0] +final fun examples.classes/testFun(): kotlin/Int // examples.classes/testFun|testFun(){}[0] +final inline fun examples.classes/testInlineFun() // examples.classes/testInlineFun|testInlineFun(){}[0] +final object examples.classes/O // examples.classes/O|null[0] +final val examples.classes/l // examples.classes/l|{}l[0] + final fun (): kotlin/Long // examples.classes/l.|(){}[0] +final var examples.classes/r // examples.classes/r|{}r[0] + final fun (): kotlin/Float // examples.classes/r.|(){}[0] + final fun (kotlin/Float) // examples.classes/r.|(kotlin.Float){}[0] +open annotation class examples.classes/A : kotlin/Annotation { // examples.classes/A|null[0] + constructor () // examples.classes/A.|(){}[0] +} +open class examples.classes/OC { // examples.classes/OC|null[0] + constructor () // examples.classes/OC.|(){}[0] + final fun c() // examples.classes/OC.c|c(){}[0] + open fun o(): kotlin/Int // examples.classes/OC.o|o(){}[0] +} diff --git a/src/functionalTest/resources/examples/gradle/configuration/grouping/clashingTargetNames.gradle.kts b/src/functionalTest/resources/examples/gradle/configuration/grouping/clashingTargetNames.gradle.kts index dbce3862..94581efc 100644 --- a/src/functionalTest/resources/examples/gradle/configuration/grouping/clashingTargetNames.gradle.kts +++ b/src/functionalTest/resources/examples/gradle/configuration/grouping/clashingTargetNames.gradle.kts @@ -4,7 +4,7 @@ */ kotlin { - linuxX64("linuxx") + linuxX64("linux") linuxArm64() mingwX64() androidNativeArm32() diff --git a/src/main/kotlin/BinaryCompatibilityValidatorPlugin.kt b/src/main/kotlin/BinaryCompatibilityValidatorPlugin.kt index a3887e55..27e9035e 100644 --- a/src/main/kotlin/BinaryCompatibilityValidatorPlugin.kt +++ b/src/main/kotlin/BinaryCompatibilityValidatorPlugin.kt @@ -5,7 +5,6 @@ package kotlinx.validation -import kotlinx.validation.klib.Target import kotlinx.validation.klib.konanTargetNameMapping import org.gradle.api.* import org.gradle.api.plugins.* @@ -14,7 +13,6 @@ import org.gradle.api.tasks.* import org.jetbrains.kotlin.gradle.dsl.* import org.jetbrains.kotlin.gradle.plugin.* import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget -import org.jetbrains.kotlin.gradle.targets.js.KotlinJsTarget import org.jetbrains.kotlin.konan.target.HostManager import org.jetbrains.kotlin.library.abi.ExperimentalLibraryAbiReader import org.jetbrains.kotlin.library.abi.LibraryAbiReader @@ -499,7 +497,7 @@ private class KlibValidationPipelineBuilder( ) { val kotlin = project.kotlinMultiplatform - val supportedTargets = supportedTargets() + val supportedTargetsProvider = supportedTargets() kotlin.targets.matching { it.emitsKlib }.configureEach { currentTarget -> val mainCompilations = currentTarget.mainCompilations if (mainCompilations.none()) { @@ -509,8 +507,7 @@ private class KlibValidationPipelineBuilder( val targetName = currentTarget.targetName val targetConfig = TargetConfig(project, extension, targetName, intermediateFilesConfig) val apiBuildDir = targetConfig.apiDir.map { project.layout.buildDirectory.asFile.get().resolve(it) }.get() - - val targetSupported = targetName in supportedTargets.get() + val targetSupported = targetIsSupported(currentTarget) // If a target is supported, the workflow is simple: create a dump, then merge it along with other dumps. if (targetSupported) { mainCompilations.all { @@ -539,7 +536,7 @@ private class KlibValidationPipelineBuilder( // it with other supported target dumps. val proxy = unsupportedTargetDumpProxy(klibApiDir, targetConfig, extractUnderlyingTarget(currentTarget), - apiBuildDir, supportedTargets.get()) + apiBuildDir, supportedTargetsProvider) mergeInferredTask.configure { it.addInput(targetName, apiBuildDir) it.dependsOn(proxy) @@ -547,7 +544,7 @@ private class KlibValidationPipelineBuilder( } mergeTask.configure { it.doFirst { - if (supportedTargets.get().isEmpty()) { + if (supportedTargetsProvider.get().isEmpty()) { throw IllegalStateException( "KLib ABI dump/validation requires at least one enabled klib target, but none were found." ) @@ -556,6 +553,14 @@ private class KlibValidationPipelineBuilder( } } + private fun Project.targetIsSupported(target: KotlinTarget): Boolean { + if (bannedTargets().contains(target.targetName)) return false + return when(target) { + is KotlinNativeTarget -> HostManager().isEnabled(target.konanTarget) + else -> true + } + } + private fun Project.supportedTargets(): Provider> { val banned = bannedTargets() // for testing only return project.provider { @@ -620,7 +625,7 @@ private class KlibValidationPipelineBuilder( targetConfig: TargetConfig, underlyingTarget: String, apiBuildDir: File, - supportedTargets: Set + supportedTargets: Provider> ): TaskProvider { val targetName = targetConfig.targetName!! return project.task(targetConfig.apiTaskName("Infer")) { diff --git a/src/main/kotlin/KotlinKlibInferAbiForUnsupportedTargetTask.kt b/src/main/kotlin/KotlinKlibInferAbiForUnsupportedTargetTask.kt index 093d3443..fcb7ce97 100644 --- a/src/main/kotlin/KotlinKlibInferAbiForUnsupportedTargetTask.kt +++ b/src/main/kotlin/KotlinKlibInferAbiForUnsupportedTargetTask.kt @@ -10,6 +10,7 @@ import kotlinx.validation.klib.KlibAbiDumpMerger import kotlinx.validation.klib.Target import kotlinx.validation.klib.TargetHierarchy import org.gradle.api.DefaultTask +import org.gradle.api.provider.Provider import org.gradle.api.tasks.* import org.jetbrains.kotlin.utils.keysToMap import java.io.File @@ -51,7 +52,7 @@ public abstract class KotlinKlibInferAbiForUnsupportedTargetTask : DefaultTask() * Set of all supported targets. */ @Input - public lateinit var supportedTargets: Set + public lateinit var supportedTargets: Provider> /** * Previously generated merged ABI dump file, the golden image every dump should be verified against. @@ -76,7 +77,7 @@ public abstract class KotlinKlibInferAbiForUnsupportedTargetTask : DefaultTask() val unsupportedTarget = Target(unsupportedTargetName, unsupportedUnderlyingTarget) // find a set of supported targets that are closer to unsupported target in the hierarchy val matchingTargets = findMatchingTargets(unsupportedTarget) - val target2outFile = supportedTargets.keysToMap { + val target2outFile = supportedTargets.get().keysToMap { File(outputApiDir).parentFile.resolve(it).resolve(dumpFileName) } @@ -124,7 +125,7 @@ public abstract class KotlinKlibInferAbiForUnsupportedTargetTask : DefaultTask() var currentGroup: String? = unsupportedTarget.underlyingTarget while (currentGroup != null) { // If a current group has some supported targets, use them. - val groupTargets = TargetHierarchy.targets(currentGroup).intersect(supportedTargets) + val groupTargets = TargetHierarchy.targets(currentGroup).intersect(supportedTargets.get()) if (groupTargets.isNotEmpty()) { return groupTargets } From 5c0c76499e9758a58b374835d9af444485f75b6c Mon Sep 17 00:00:00 2001 From: Filipp Zhinkin Date: Fri, 1 Mar 2024 10:34:37 +0100 Subject: [PATCH 24/53] Partially reconstruct a manifest when extracting a single target from a merged dump --- api/binary-compatibility-validator.api | 6 +-- ...lDeclarations.klib.with.guessed.linux.dump | 2 +- .../BinaryCompatibilityValidatorPlugin.kt | 2 +- ...linKlibInferAbiForUnsupportedTargetTask.kt | 8 +-- src/main/kotlin/klib/KlibAbiDumpFileMerger.kt | 43 +++++++++++----- src/main/kotlin/klib/TargetHierarchy.kt | 16 ++++++ src/test/kotlin/tests/KlibAbiMergingTest.kt | 50 +++++++++++++------ .../merge/diverging/linuxArm64.extracted.api | 17 +++++++ src/test/resources/merge/guess/guessed.api | 2 + .../resources/merge/webTargets/js.ext.abi | 14 ++++++ .../resources/merge/webTargets/wasm.ext.abi | 14 ++++++ 11 files changed, 138 insertions(+), 36 deletions(-) create mode 100644 src/test/resources/merge/diverging/linuxArm64.extracted.api create mode 100644 src/test/resources/merge/webTargets/js.ext.abi create mode 100644 src/test/resources/merge/webTargets/wasm.ext.abi diff --git a/api/binary-compatibility-validator.api b/api/binary-compatibility-validator.api index b1c8cda2..1715269b 100644 --- a/api/binary-compatibility-validator.api +++ b/api/binary-compatibility-validator.api @@ -125,23 +125,23 @@ public abstract class kotlinx/validation/KotlinKlibInferAbiForUnsupportedTargetT public field outputApiDir Ljava/lang/String; public field outputFile Ljava/io/File; public field supportedTargets Lorg/gradle/api/provider/Provider; + public field unsupportedTargetCanonicalName Ljava/lang/String; public field unsupportedTargetName Ljava/lang/String; - public field unsupportedUnderlyingTarget Ljava/lang/String; public fun ()V public final fun getDumpFileName ()Ljava/lang/String; public final fun getInputImageFile ()Ljava/io/File; public final fun getOutputApiDir ()Ljava/lang/String; public final fun getOutputFile ()Ljava/io/File; public final fun getSupportedTargets ()Lorg/gradle/api/provider/Provider; + public final fun getUnsupportedTargetCanonicalName ()Ljava/lang/String; public final fun getUnsupportedTargetName ()Ljava/lang/String; - public final fun getUnsupportedUnderlyingTarget ()Ljava/lang/String; public final fun setDumpFileName (Ljava/lang/String;)V public final fun setInputImageFile (Ljava/io/File;)V public final fun setOutputApiDir (Ljava/lang/String;)V public final fun setOutputFile (Ljava/io/File;)V public final fun setSupportedTargets (Lorg/gradle/api/provider/Provider;)V + public final fun setUnsupportedTargetCanonicalName (Ljava/lang/String;)V public final fun setUnsupportedTargetName (Ljava/lang/String;)V - public final fun setUnsupportedUnderlyingTarget (Ljava/lang/String;)V } public abstract class kotlinx/validation/KotlinKlibMergeAbiTask : org/gradle/api/DefaultTask { diff --git a/src/functionalTest/resources/examples/classes/TopLevelDeclarations.klib.with.guessed.linux.dump b/src/functionalTest/resources/examples/classes/TopLevelDeclarations.klib.with.guessed.linux.dump index 44b5e3f9..501732fc 100644 --- a/src/functionalTest/resources/examples/classes/TopLevelDeclarations.klib.with.guessed.linux.dump +++ b/src/functionalTest/resources/examples/classes/TopLevelDeclarations.klib.with.guessed.linux.dump @@ -1,5 +1,5 @@ // Merged KLib ABI Dump -// Targets: [androidNativeArm32, androidNativeArm64, androidNativeX64, androidNativeX86, linuxArm64, linux.linuxX64, mingwX64] +// Targets: [androidNativeArm32, androidNativeArm64, androidNativeX64, androidNativeX86, linux.linuxX64, linuxArm64, mingwX64] // Rendering settings: // - Signature version: 2 // - Show manifest properties: true diff --git a/src/main/kotlin/BinaryCompatibilityValidatorPlugin.kt b/src/main/kotlin/BinaryCompatibilityValidatorPlugin.kt index 27e9035e..40c60b33 100644 --- a/src/main/kotlin/BinaryCompatibilityValidatorPlugin.kt +++ b/src/main/kotlin/BinaryCompatibilityValidatorPlugin.kt @@ -638,7 +638,7 @@ private class KlibValidationPipelineBuilder( outputApiDir = apiBuildDir.toString() outputFile = apiBuildDir.resolve(klibDumpFileName) unsupportedTargetName = targetConfig.targetName - unsupportedUnderlyingTarget = underlyingTarget + unsupportedTargetCanonicalName = underlyingTarget dumpFileName = klibDumpFileName dependsOn(project.tasks.withType(KotlinKlibAbiBuildTask::class.java)) } diff --git a/src/main/kotlin/KotlinKlibInferAbiForUnsupportedTargetTask.kt b/src/main/kotlin/KotlinKlibInferAbiForUnsupportedTargetTask.kt index fcb7ce97..38889c21 100644 --- a/src/main/kotlin/KotlinKlibInferAbiForUnsupportedTargetTask.kt +++ b/src/main/kotlin/KotlinKlibInferAbiForUnsupportedTargetTask.kt @@ -39,7 +39,7 @@ public abstract class KotlinKlibInferAbiForUnsupportedTargetTask : DefaultTask() * The name of a target to infer a dump for. */ @Input - public lateinit var unsupportedUnderlyingTarget: String + public lateinit var unsupportedTargetCanonicalName: String /** * A root directory containing dumps successfully generated for each supported target. @@ -74,7 +74,7 @@ public abstract class KotlinKlibInferAbiForUnsupportedTargetTask : DefaultTask() @TaskAction internal fun generate() { - val unsupportedTarget = Target(unsupportedTargetName, unsupportedUnderlyingTarget) + val unsupportedTarget = Target(unsupportedTargetName, unsupportedTargetCanonicalName) // find a set of supported targets that are closer to unsupported target in the hierarchy val matchingTargets = findMatchingTargets(unsupportedTarget) val target2outFile = supportedTargets.get().keysToMap { @@ -109,7 +109,7 @@ public abstract class KotlinKlibInferAbiForUnsupportedTargetTask : DefaultTask() commonDump.overrideTargets(setOf(unsupportedTarget)) outputFile.bufferedWriter().use { - commonDump.dump(it, KlibAbiDumpFormat(includeTargets = false)) + commonDump.dump(it, KlibAbiDumpFormat(singleTargetDump = true)) } logger.warn( @@ -122,7 +122,7 @@ public abstract class KotlinKlibInferAbiForUnsupportedTargetTask : DefaultTask() } private fun findMatchingTargets(unsupportedTarget: Target): Set { - var currentGroup: String? = unsupportedTarget.underlyingTarget + var currentGroup: String? = unsupportedTarget.canonicalName while (currentGroup != null) { // If a current group has some supported targets, use them. val groupTargets = TargetHierarchy.targets(currentGroup).intersect(supportedTargets.get()) diff --git a/src/main/kotlin/klib/KlibAbiDumpFileMerger.kt b/src/main/kotlin/klib/KlibAbiDumpFileMerger.kt index 2d626bda..6652abd7 100644 --- a/src/main/kotlin/klib/KlibAbiDumpFileMerger.kt +++ b/src/main/kotlin/klib/KlibAbiDumpFileMerger.kt @@ -9,23 +9,24 @@ import java.io.File import java.nio.file.Files /** - * Target name consisting of two parts: a [name] that could be configured by a user, and an [underlyingTarget] + * Target name consisting of two parts: a [name] that could be configured by a user, and an [canonicalName] * that names a target platform and could not be configured by a user. * - * When serialized, the target represented as a tuple `.`, like `ios.iosArm64`. + * When serialized, the target represented as a tuple `.`, like `ios.iosArm64`. * If both names are the same (they are by default, unless a user decides to use a custom name), the serialized * from is shortened to a single term. For example, `macosArm64.macosArm64` and `macosArm64` are a long and a short * serialized forms of the same target. */ -internal data class Target(val name: String, val underlyingTarget: String) { +internal data class Target(val name: String, val canonicalName: String) { companion object { fun parse(line: String): Target { + require(line.isNotBlank()) { "Target name could not be blank." } if (!line.contains('.')) { return Target(line) } val parts = line.split('.') if (parts.size != 2 || parts.any { it.isBlank() }) { - throw IllegalStateException( + throw IllegalArgumentException( "Target has illegal name format: \"$line\", expected: ." ) } @@ -33,7 +34,7 @@ internal data class Target(val name: String, val underlyingTarget: String) { } } - override fun toString(): String = if (name == underlyingTarget) name else "$name.$underlyingTarget" + override fun toString(): String = if (name == canonicalName) name else "$name.$canonicalName" } internal fun Target(name: String) = Target(name, name) @@ -99,7 +100,12 @@ private fun parseBcvTargetsLine(line: String): Set { } internal data class KlibAbiDumpFormat( - val includeTargets: Boolean = true, + /** + * Reconstruct a dump as it would look like after dumping a klib. + * This flag is mostly intended for dumping an inferred ABI and + * requires a [KlibAbiDumpMerger] containing only a single target. + */ + val singleTargetDump: Boolean = false, val useGroupAliases: Boolean = false ) @@ -364,17 +370,28 @@ internal class KlibAbiDumpMerger { fun dump(appendable: Appendable, dumpFormat: KlibAbiDumpFormat = KlibAbiDumpFormat()) { val formatter = createFormatter(dumpFormat) - if (dumpFormat.includeTargets) { - appendable.append(MERGED_DUMP_FILE_HEADER).append('\n') - appendable.append(formatter.formatHeader(targets)).append('\n') - } else { + if (dumpFormat.singleTargetDump) { require(targets.size == 1) { "Can skip target inclusion only if the dump contains a single target, but it contains: $targets" } + } else { + appendable.append(MERGED_DUMP_FILE_HEADER).append('\n') + appendable.append(formatter.formatHeader(targets)).append('\n') } headerContent.forEach { appendable.append(it).append('\n') } + if (dumpFormat.singleTargetDump) { + // Reconstruct a manifest partially for a single target dump, + // so the dump could be later merged correctly. + val canonicalName = targets.single().canonicalName + val platform = platformByCanonicalName(canonicalName) + appendable.append(PLATFORM_PREFIX).append(platform).append('\n') + if (platform == "NATIVE") { + val nativeTarget = konanTargetNameReverseMapping[canonicalName]!! + appendable.append(NATIVE_TARGETS_PREFIX).append(nativeTarget).append('\n') + } + } topLevelDeclaration.children.values.sortedWith(DeclarationsComparator).forEach { it.dump(appendable, targetsMut, dumpFormat, formatter) } @@ -383,7 +400,7 @@ internal class KlibAbiDumpMerger { private fun createFormatter(dumpFormat: KlibAbiDumpFormat): KLibsTargetsFormatter { return if (dumpFormat.useGroupAliases) { for (target in targets) { - val node = TargetHierarchy.hierarchyIndex[target.underlyingTarget] + val node = TargetHierarchy.hierarchyIndex[target.canonicalName] if (node != null && node.allLeafs.size == 1 && node.allLeafs.first() != node.node.name) { throw IllegalStateException( "Can't use target aliases as one of the this dump's targets" + @@ -494,7 +511,7 @@ internal class DeclarationContainer(val text: String, val parent: DeclarationCon appendable: Appendable, allTargets: Set, dumpFormat: KlibAbiDumpFormat, formatter: KLibsTargetsFormatter ) { - if (targets != allTargets && dumpFormat.includeTargets) { + if (targets != allTargets && !dumpFormat.singleTargetDump) { // Use the same indentation for target list as for the declaration itself appendable.append(" ".repeat(text.depth() * INDENT_WIDTH)) .append(formatter.formatDeclarationTargets(targets)) @@ -644,7 +661,7 @@ private class GroupingFormatter(klibDump: KlibAbiDumpMerger) : KLibsTargetsForma // intersect with all targets to use only enabled targets in aliases // intersection is based on underlying target name as a set of such names is fixed val leafs = it.value.allLeafs - val availableTargets = allTargets.asSequence().filter { leafs.contains(it.underlyingTarget) }.toSet() + val availableTargets = allTargets.asSequence().filter { leafs.contains(it.canonicalName) }.toSet() if (availableTargets.isNotEmpty()) { aliasesBuilder.add(Alias(it.key, availableTargets)) } diff --git a/src/main/kotlin/klib/TargetHierarchy.kt b/src/main/kotlin/klib/TargetHierarchy.kt index cb25bb62..cf76d58a 100644 --- a/src/main/kotlin/klib/TargetHierarchy.kt +++ b/src/main/kotlin/klib/TargetHierarchy.kt @@ -147,3 +147,19 @@ internal val konanTargetNameMapping = mapOf( "linux_mipsel32" to "linuxMipsel32", "wasm32" to "wasm32" ) + +internal val konanTargetNameReverseMapping = konanTargetNameMapping.asSequence().map { it.value to it.key }.toMap() + +internal fun platformByCanonicalName(canonicalName: String): String { + return when (canonicalName) { + "js" -> "JS" + "wasmJs" -> "WASM" + "wasmWasi" -> "WASM" + else -> { + require(konanTargetNameReverseMapping.containsKey(canonicalName)) { + "Unsupported target: $canonicalName" + } + return "NATIVE" + } + } +} diff --git a/src/test/kotlin/tests/KlibAbiMergingTest.kt b/src/test/kotlin/tests/KlibAbiMergingTest.kt index 680ea49c..771df9cb 100644 --- a/src/test/kotlin/tests/KlibAbiMergingTest.kt +++ b/src/test/kotlin/tests/KlibAbiMergingTest.kt @@ -43,7 +43,7 @@ class KlibAbiMergingTest { val file = tempDir.newFile() FileWriter(file).use { klib.dump(it, KlibAbiDumpFormat( - includeTargets = !singleTargetDump, + singleTargetDump = singleTargetDump, useGroupAliases = useAliases )) } @@ -56,23 +56,25 @@ class KlibAbiMergingTest { assertEquals("a", Target("a").toString()) assertEquals("a", Target("a", "a").toString()) - assertFailsWith { Target.parse("a.b.c") } - assertFailsWith { Target.parse("a.") } - assertFailsWith { Target.parse(".a") } + assertFailsWith { Target.parse("") } + assertFailsWith { Target.parse(" ") } + assertFailsWith { Target.parse("a.b.c") } + assertFailsWith { Target.parse("a.") } + assertFailsWith { Target.parse(".a") } Target.parse("a.b").also { assertEquals("a", it.name) - assertEquals("b", it.underlyingTarget) + assertEquals("b", it.canonicalName) } Target.parse("a.a").also { assertEquals("a", it.name) - assertEquals("a", it.underlyingTarget) + assertEquals("a", it.canonicalName) } Target.parse("a").also { assertEquals("a", it.name) - assertEquals("a", it.underlyingTarget) + assertEquals("a", it.canonicalName) } } @@ -331,13 +333,33 @@ class KlibAbiMergingTest { val lib = KlibAbiDumpMerger().apply { loadMergedDump(file("/merge/diverging/merged_with_aliases_and_custom_names.abi")) } - val targets = lib.targets.filter { it.underlyingTarget != "linuxArm64" } + val targets = lib.targets.filter { it.canonicalName != "linuxArm64" } targets.forEach { lib.remove(it) } - println(buildString { - lib.dump(this, KlibAbiDumpFormat( - useGroupAliases = false, - includeTargets = false - )) - }) + val extracted = dumpToFile(lib, singleTargetDump = true) + assertContentEquals( + lines("/merge/diverging/linuxArm64.extracted.api"), + Files.readAllLines(extracted.toPath()).asSequence() + ) + } + + @Test + fun webTargetsExtraction() { + val mergedPath = "/merge/webTargets/merged.abi" + + fun checkExtracted(targetName: String, expectedFile: String) { + val lib = KlibAbiDumpMerger().apply { loadMergedDump(file(mergedPath)) } + val targets = lib.targets + targets.filter { it.name != targetName }.forEach { lib.remove(it) } + val dump = dumpToFile(lib, singleTargetDump = true) + assertContentEquals( + lines(expectedFile), + Files.readAllLines(dump.toPath()).asSequence(), + "Dumps mismatched for target $targetName" + ) + } + + checkExtracted("js", "/merge/webTargets/js.ext.abi") + checkExtracted("wasmWasi", "/merge/webTargets/wasm.ext.abi") + checkExtracted("wasmJs", "/merge/webTargets/wasm.ext.abi") } } diff --git a/src/test/resources/merge/diverging/linuxArm64.extracted.api b/src/test/resources/merge/diverging/linuxArm64.extracted.api new file mode 100644 index 00000000..7174bad5 --- /dev/null +++ b/src/test/resources/merge/diverging/linuxArm64.extracted.api @@ -0,0 +1,17 @@ +// Rendering settings: +// - Signature version: 2 +// - Show manifest properties: false +// - Show declarations: true + +// Library unique name: +// Platform: NATIVE +// Native targets: linux_arm64 +final class org.example/ShardedClass { // org.example/ShardedClass|null[0] + constructor (kotlin/Int) // org.example/ShardedClass.|(kotlin.Int){}[0] + final fun add(kotlin/Int): kotlin/Int // org.example/ShardedClass.add|add(kotlin.Int){}[0] + final val value // org.example/ShardedClass.value|{}value[0] + final fun (): kotlin/Int // org.example/ShardedClass.value.|(){}[0] +} +final fun org.example/ShardedClass(kotlin/Int, kotlin/Float, kotlin/Long): org.example/ShardedClass // org.example/ShardedClass|ShardedClass(kotlin.Int;kotlin.Float;kotlin.Long){}[0] +final fun (kotlin/Long).org.example/add(kotlin/Long): kotlin/Long // org.example/add|add@kotlin.Long(kotlin.Long){}[0] +final fun org.example/add(kotlin/Long, kotlin/Long): kotlin/Long // org.example/add|add(kotlin.Long;kotlin.Long){}[0] diff --git a/src/test/resources/merge/guess/guessed.api b/src/test/resources/merge/guess/guessed.api index c06e2247..4e1e5f6f 100644 --- a/src/test/resources/merge/guess/guessed.api +++ b/src/test/resources/merge/guess/guessed.api @@ -4,6 +4,8 @@ // - Show declarations: true // Library unique name: +// Platform: NATIVE +// Native targets: linux_arm64 final class org.example/ShardedClass { // org.example/ShardedClass|null[0] constructor (kotlin/Int) // org.example/ShardedClass.|(kotlin.Int){}[0] final fun add(kotlin/Int): kotlin/Int // org.example/ShardedClass.add|add(kotlin.Int){}[0] diff --git a/src/test/resources/merge/webTargets/js.ext.abi b/src/test/resources/merge/webTargets/js.ext.abi new file mode 100644 index 00000000..6cbca640 --- /dev/null +++ b/src/test/resources/merge/webTargets/js.ext.abi @@ -0,0 +1,14 @@ +// Rendering settings: +// - Signature version: 2 +// - Show manifest properties: false +// - Show declarations: true + +// Library unique name: +// Platform: JS +final class org.example/ShardedClass { // org.example/ShardedClass|null[0] + constructor (kotlin/Int) // org.example/ShardedClass.|(kotlin.Int){}[0] + final fun add(kotlin/Int): kotlin/Int // org.example/ShardedClass.add|add(kotlin.Int){}[0] + final val value // org.example/ShardedClass.value|{}value[0] + final fun (): kotlin/Int // org.example/ShardedClass.value.|(){}[0] +} +final fun org.example/ShardedClass(kotlin/Int, kotlin/Float, kotlin/Long): org.example/ShardedClass // org.example/ShardedClass|ShardedClass(kotlin.Int;kotlin.Float;kotlin.Long){}[0] diff --git a/src/test/resources/merge/webTargets/wasm.ext.abi b/src/test/resources/merge/webTargets/wasm.ext.abi new file mode 100644 index 00000000..4de57b09 --- /dev/null +++ b/src/test/resources/merge/webTargets/wasm.ext.abi @@ -0,0 +1,14 @@ +// Rendering settings: +// - Signature version: 2 +// - Show manifest properties: false +// - Show declarations: true + +// Library unique name: +// Platform: WASM +final class org.example/ShardedClass { // org.example/ShardedClass|null[0] + constructor (kotlin/Int) // org.example/ShardedClass.|(kotlin.Int){}[0] + final fun add(kotlin/Int): kotlin/Int // org.example/ShardedClass.add|add(kotlin.Int){}[0] + final val value // org.example/ShardedClass.value|{}value[0] + final fun (): kotlin/Int // org.example/ShardedClass.value.|(){}[0] +} +final fun org.example/ShardedClass(kotlin/Int, kotlin/Float, kotlin/Long): org.example/ShardedClass // org.example/ShardedClass|ShardedClass(kotlin.Int;kotlin.Float;kotlin.Long){}[0] From 4ec6a11ccda321523c393c4e27a0d09cf1798e44 Mon Sep 17 00:00:00 2001 From: Filipp Zhinkin Date: Fri, 1 Mar 2024 11:06:14 +0100 Subject: [PATCH 25/53] Add a compound target name proposal to the design doc --- docs/design/KLibSupport.md | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/docs/design/KLibSupport.md b/docs/design/KLibSupport.md index ecefa3d9..2c82bd7b 100644 --- a/docs/design/KLibSupport.md +++ b/docs/design/KLibSupport.md @@ -171,6 +171,38 @@ The table below summarizes different scenarios and how the two different approac The "guessing" approach should succeed it a broader spectrum of scenarios, so we decided to use it. +### Target name representation + +Target name grouping and ABI dump inference described above heavily rely on target name. +Everything works fine with default names unless a user decides to rename a target: +```kotlin +kotiln { + macosArm64("macos") + linuxArm64("linux") + iosArm64() +} +``` +There are two main issues related to the renaming: +- target's name could no longer be found among targets constituting a target hierarchy (on the BCV side, not the KGP); +- new target's name may clash with existing group names or other target names. + +However, a klib's manifest contains all the information required to find an actual underlying target (but it does +not contain a custom name, though). And the same info could be included in a textual klib dump. + +To overcome the issue, it is proposed to represent target name as a tuple consisting of "visible" name +and a canonical underlying target name: `name.canonicalName`. + +For the example mentioned above, target such fully qualified target names are: +- `macos.macosArm64` for `macosArm64("macos")`; +- `linux.linuxArm64` for `linuxArm64("linux")`; +- `iosArm64.iosArm64` for `iosArm64()`. + +By default, when the visible and canonical names are the same, only one of them could be specified, so +`iosArm64.iosArm64` could be shortened to `iosArm64`. + +Given such compound names, we can correctly perform grouping and inferring by relying only on the underlying canonical +target name, not on the visible one. + ### Programmatic API for KLib ABI validation To support scenarios when Gradle plugin could not be used (like in the case of Kotlin stdlib), From 87c35f487f050cc5e6cc2c4dd1d058192d6c2e36 Mon Sep 17 00:00:00 2001 From: Filipp Zhinkin Date: Fri, 1 Mar 2024 11:26:31 +0100 Subject: [PATCH 26/53] Update the doc --- src/main/kotlin/ApiValidationExtension.kt | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/main/kotlin/ApiValidationExtension.kt b/src/main/kotlin/ApiValidationExtension.kt index 94a64c32..61773ab7 100644 --- a/src/main/kotlin/ApiValidationExtension.kt +++ b/src/main/kotlin/ApiValidationExtension.kt @@ -112,7 +112,6 @@ public open class KlibValidationSettings { * that will be dumped to prevent changes in a dump file. */ public var signatureVersion: Int? = null - /** * Fail validation if some build targets are not supported by the host compiler. * By default, ABI dumped only for supported files will be validated. This option makes validation behavior @@ -125,9 +124,6 @@ public open class KlibValidationSettings { * * Group aliases are based on [the default hierarchy template](https://kotlinlang.org/docs/multiplatform-hierarchy.html#see-the-full-hierarchy-template) * and enabled by default. - * - * Grouping will not be applied if a project has a target with a custom name that clashes - * with one of the group names. */ public var useTargetGroupAliases: Boolean = true } From e7bf172fd4741364eee8f267966beb0d39c18009 Mon Sep 17 00:00:00 2001 From: Filipp Zhinkin Date: Fri, 1 Mar 2024 11:31:02 +0100 Subject: [PATCH 27/53] Make klib-related Gradle tasks internal --- api/binary-compatibility-validator.api | 70 ------------------- src/main/kotlin/KotlinKlibAbiBuildTask.kt | 10 +-- ...otlinKlibExtractSupportedTargetsAbiTask.kt | 12 ++-- ...linKlibInferAbiForUnsupportedTargetTask.kt | 16 ++--- src/main/kotlin/KotlinKlibMergeAbiTask.kt | 12 ++-- 5 files changed, 24 insertions(+), 96 deletions(-) diff --git a/api/binary-compatibility-validator.api b/api/binary-compatibility-validator.api index 1715269b..969670f1 100644 --- a/api/binary-compatibility-validator.api +++ b/api/binary-compatibility-validator.api @@ -87,76 +87,6 @@ public class kotlinx/validation/KotlinApiCompareTask : org/gradle/api/DefaultTas public final fun setProjectApiFile (Ljava/io/File;)V } -public abstract class kotlinx/validation/KotlinKlibAbiBuildTask : kotlinx/validation/BuildTaskBase { - public field compilationDependencies Lorg/gradle/api/file/FileCollection; - public field klibFile Lorg/gradle/api/file/FileCollection; - public field target Ljava/lang/String; - public fun ()V - public final fun getCompilationDependencies ()Lorg/gradle/api/file/FileCollection; - public final fun getKlibFile ()Lorg/gradle/api/file/FileCollection; - public final fun getSignatureVersion ()Ljava/lang/Integer; - public final fun getTarget ()Ljava/lang/String; - public final fun setCompilationDependencies (Lorg/gradle/api/file/FileCollection;)V - public final fun setKlibFile (Lorg/gradle/api/file/FileCollection;)V - public final fun setSignatureVersion (Ljava/lang/Integer;)V - public final fun setTarget (Ljava/lang/String;)V -} - -public abstract class kotlinx/validation/KotlinKlibExtractSupportedTargetsAbiTask : org/gradle/api/DefaultTask { - public field inputAbiFile Ljava/io/File; - public field outputAbiFile Ljava/io/File; - public field targets Lorg/gradle/api/provider/Provider; - public fun ()V - public final fun getGroupTargetNames ()Z - public final fun getInputAbiFile ()Ljava/io/File; - public final fun getOutputAbiFile ()Ljava/io/File; - public final fun getStrictValidation ()Z - public final fun getTargets ()Lorg/gradle/api/provider/Provider; - public final fun setGroupTargetNames (Z)V - public final fun setInputAbiFile (Ljava/io/File;)V - public final fun setOutputAbiFile (Ljava/io/File;)V - public final fun setStrictValidation (Z)V - public final fun setTargets (Lorg/gradle/api/provider/Provider;)V -} - -public abstract class kotlinx/validation/KotlinKlibInferAbiForUnsupportedTargetTask : org/gradle/api/DefaultTask { - public field dumpFileName Ljava/lang/String; - public field inputImageFile Ljava/io/File; - public field outputApiDir Ljava/lang/String; - public field outputFile Ljava/io/File; - public field supportedTargets Lorg/gradle/api/provider/Provider; - public field unsupportedTargetCanonicalName Ljava/lang/String; - public field unsupportedTargetName Ljava/lang/String; - public fun ()V - public final fun getDumpFileName ()Ljava/lang/String; - public final fun getInputImageFile ()Ljava/io/File; - public final fun getOutputApiDir ()Ljava/lang/String; - public final fun getOutputFile ()Ljava/io/File; - public final fun getSupportedTargets ()Lorg/gradle/api/provider/Provider; - public final fun getUnsupportedTargetCanonicalName ()Ljava/lang/String; - public final fun getUnsupportedTargetName ()Ljava/lang/String; - public final fun setDumpFileName (Ljava/lang/String;)V - public final fun setInputImageFile (Ljava/io/File;)V - public final fun setOutputApiDir (Ljava/lang/String;)V - public final fun setOutputFile (Ljava/io/File;)V - public final fun setSupportedTargets (Lorg/gradle/api/provider/Provider;)V - public final fun setUnsupportedTargetCanonicalName (Ljava/lang/String;)V - public final fun setUnsupportedTargetName (Ljava/lang/String;)V -} - -public abstract class kotlinx/validation/KotlinKlibMergeAbiTask : org/gradle/api/DefaultTask { - public field dumpFileName Ljava/lang/String; - public field mergedFile Ljava/io/File; - public fun ()V - public final fun getDumpFileName ()Ljava/lang/String; - public final fun getGroupTargetNames ()Z - public final fun getMergedFile ()Ljava/io/File; - public final fun getTargets ()Ljava/util/Set; - public final fun setDumpFileName (Ljava/lang/String;)V - public final fun setGroupTargetNames (Z)V - public final fun setMergedFile (Ljava/io/File;)V -} - public final class kotlinx/validation/api/ClassBinarySignature { public final fun copy (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/List;Ljava/util/List;Lkotlinx/validation/api/AccessFlags;ZZLjava/util/List;)Lkotlinx/validation/api/ClassBinarySignature; public static synthetic fun copy$default (Lkotlinx/validation/api/ClassBinarySignature;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/List;Ljava/util/List;Lkotlinx/validation/api/AccessFlags;ZZLjava/util/List;ILjava/lang/Object;)Lkotlinx/validation/api/ClassBinarySignature; diff --git a/src/main/kotlin/KotlinKlibAbiBuildTask.kt b/src/main/kotlin/KotlinKlibAbiBuildTask.kt index 31a70a78..55b10c3d 100644 --- a/src/main/kotlin/KotlinKlibAbiBuildTask.kt +++ b/src/main/kotlin/KotlinKlibAbiBuildTask.kt @@ -15,32 +15,32 @@ import org.jetbrains.kotlin.library.abi.* /** * Generates a text file with a KLib ABI dump for a single klib. */ -public abstract class KotlinKlibAbiBuildTask : BuildTaskBase() { +internal abstract class KotlinKlibAbiBuildTask : BuildTaskBase() { /** * Path to a klib to dump. */ @InputFiles - public lateinit var klibFile: FileCollection + lateinit var klibFile: FileCollection /** * Bind this task with a klib compilation. */ @InputFiles - public lateinit var compilationDependencies: FileCollection + lateinit var compilationDependencies: FileCollection /** * Refer to [KlibValidationSettings.signatureVersion] for details. */ @Optional @get:Input - public var signatureVersion: Int? = null + var signatureVersion: Int? = null /** * Name of a target [klibFile] was compiled for. */ @Input - public lateinit var target: String + lateinit var target: String @ExperimentalStdlibApi @ExperimentalLibraryAbiReader diff --git a/src/main/kotlin/KotlinKlibExtractSupportedTargetsAbiTask.kt b/src/main/kotlin/KotlinKlibExtractSupportedTargetsAbiTask.kt index c74fe4d6..b34df1a3 100644 --- a/src/main/kotlin/KotlinKlibExtractSupportedTargetsAbiTask.kt +++ b/src/main/kotlin/KotlinKlibExtractSupportedTargetsAbiTask.kt @@ -16,7 +16,7 @@ import java.io.File /** * Extracts dump for targets supported by the host compiler from a merged API dump stored in a project. */ -public abstract class KotlinKlibExtractSupportedTargetsAbiTask : DefaultTask() { +internal abstract class KotlinKlibExtractSupportedTargetsAbiTask : DefaultTask() { @get:Internal internal val projectName = project.name @@ -24,31 +24,31 @@ public abstract class KotlinKlibExtractSupportedTargetsAbiTask : DefaultTask() { * Merged KLib dump that should be filtered by this task. */ @InputFiles - public lateinit var inputAbiFile: File + lateinit var inputAbiFile: File /** * A path to the resulting dump file. */ @OutputFile - public lateinit var outputAbiFile: File + lateinit var outputAbiFile: File /** * Provider returning targets supported by the host compiler. */ @get:Input - public lateinit var targets: Provider> + lateinit var targets: Provider> /** * Refer to [KlibValidationSettings.strictValidation] for details. */ @Input - public var strictValidation: Boolean = false + var strictValidation: Boolean = false /** * Refer to [KlibValidationSettings.useTargetGroupAliases] for details. */ @Input - public var groupTargetNames: Boolean = true + var groupTargetNames: Boolean = true @TaskAction internal fun generate() { diff --git a/src/main/kotlin/KotlinKlibInferAbiForUnsupportedTargetTask.kt b/src/main/kotlin/KotlinKlibInferAbiForUnsupportedTargetTask.kt index 38889c21..398b588d 100644 --- a/src/main/kotlin/KotlinKlibInferAbiForUnsupportedTargetTask.kt +++ b/src/main/kotlin/KotlinKlibInferAbiForUnsupportedTargetTask.kt @@ -25,7 +25,7 @@ import java.io.File * from it and merged into the common ABI extracted previously. * The resulting dump is then used as an inferred dump for the unsupported target. */ -public abstract class KotlinKlibInferAbiForUnsupportedTargetTask : DefaultTask() { +internal abstract class KotlinKlibInferAbiForUnsupportedTargetTask : DefaultTask() { @get:Internal internal val projectName = project.name @@ -33,44 +33,44 @@ public abstract class KotlinKlibInferAbiForUnsupportedTargetTask : DefaultTask() * The name of a target to infer a dump for. */ @Input - public lateinit var unsupportedTargetName: String + lateinit var unsupportedTargetName: String /** * The name of a target to infer a dump for. */ @Input - public lateinit var unsupportedTargetCanonicalName: String + lateinit var unsupportedTargetCanonicalName: String /** * A root directory containing dumps successfully generated for each supported target. * It is assumed that this directory contains subdirectories named after targets. */ @InputFiles - public lateinit var outputApiDir: String + lateinit var outputApiDir: String /** * Set of all supported targets. */ @Input - public lateinit var supportedTargets: Provider> + lateinit var supportedTargets: Provider> /** * Previously generated merged ABI dump file, the golden image every dump should be verified against. */ @InputFiles - public lateinit var inputImageFile: File + lateinit var inputImageFile: File /** * The name of a dump file. */ @Input - public lateinit var dumpFileName: String + lateinit var dumpFileName: String /** * A path to an inferred dump file. */ @OutputFile - public lateinit var outputFile: File + lateinit var outputFile: File @TaskAction internal fun generate() { diff --git a/src/main/kotlin/KotlinKlibMergeAbiTask.kt b/src/main/kotlin/KotlinKlibMergeAbiTask.kt index c836f254..810a7281 100644 --- a/src/main/kotlin/KotlinKlibMergeAbiTask.kt +++ b/src/main/kotlin/KotlinKlibMergeAbiTask.kt @@ -7,8 +7,6 @@ package kotlinx.validation import kotlinx.validation.klib.KlibAbiDumpFormat import kotlinx.validation.klib.KlibAbiDumpMerger -import kotlinx.validation.klib.Target -import kotlinx.validation.klib.TargetHierarchy import org.gradle.api.DefaultTask import org.gradle.api.tasks.* import java.io.File @@ -16,7 +14,7 @@ import java.io.File /** * Merges multiple individual KLib ABI dumps into a single merged dump. */ -public abstract class KotlinKlibMergeAbiTask : DefaultTask() { +internal abstract class KotlinKlibMergeAbiTask : DefaultTask() { private val targetToFile = mutableMapOf() @get:Internal @@ -26,7 +24,7 @@ public abstract class KotlinKlibMergeAbiTask : DefaultTask() { * Set of targets whose dumps should be merged. */ @get:Input - public val targets: Set + val targets: Set get() = targetToFile.keys // Required to enforce task rerun on klibs update @@ -39,19 +37,19 @@ public abstract class KotlinKlibMergeAbiTask : DefaultTask() { * A path to a resulting merged dump. */ @OutputFile - public lateinit var mergedFile: File + lateinit var mergedFile: File /** * The name of a dump file. */ @Input - public lateinit var dumpFileName: String + lateinit var dumpFileName: String /** * Refer to [KlibValidationSettings.useTargetGroupAliases] for details. */ @Input - public var groupTargetNames: Boolean = true + var groupTargetNames: Boolean = true internal fun addInput(target: String, file: File) { targetToFile[target] = file From e6171f9fa25dfe666a4284022775fbcfe722bf71 Mon Sep 17 00:00:00 2001 From: Filipp Zhinkin Date: Fri, 1 Mar 2024 15:10:36 +0100 Subject: [PATCH 28/53] Use canonical target names when filtering out targets to validate --- .../validation/test/KLibVerificationTests.kt | 18 +++++++++++++ ...AnotherBuildConfig.klib.renamedTarget.dump | 14 ++++++++++ .../BinaryCompatibilityValidatorPlugin.kt | 27 ++++++++++++------- ...otlinKlibExtractSupportedTargetsAbiTask.kt | 10 ++++--- 4 files changed, 56 insertions(+), 13 deletions(-) create mode 100644 src/functionalTest/resources/examples/classes/AnotherBuildConfig.klib.renamedTarget.dump diff --git a/src/functionalTest/kotlin/kotlinx/validation/test/KLibVerificationTests.kt b/src/functionalTest/kotlin/kotlinx/validation/test/KLibVerificationTests.kt index fe1e1825..bb573b2c 100644 --- a/src/functionalTest/kotlin/kotlinx/validation/test/KLibVerificationTests.kt +++ b/src/functionalTest/kotlin/kotlinx/validation/test/KLibVerificationTests.kt @@ -615,4 +615,22 @@ internal class KLibVerificationTests : BaseKotlinGradleTest() { assertTaskFailure(":klibApiCheck") } } + + @Test + fun `validation should fail on target rename`() { + val runner = test { + baseProjectSetting() + addToSrcSet("/examples/classes/AnotherBuildConfig.kt") + abiFile(projectName = "testproject") { + resolve("/examples/classes/AnotherBuildConfig.klib.renamedTarget.dump") + } + runApiCheck() + } + runner.buildAndFail().apply { + Assertions.assertThat(output).contains( + " -// Targets: [androidNativeArm32, androidNativeArm64, androidNativeX64, " + + "androidNativeX86, linux.linuxArm64, linuxX64, mingwX64]" + ) + } + } } diff --git a/src/functionalTest/resources/examples/classes/AnotherBuildConfig.klib.renamedTarget.dump b/src/functionalTest/resources/examples/classes/AnotherBuildConfig.klib.renamedTarget.dump new file mode 100644 index 00000000..effc6a79 --- /dev/null +++ b/src/functionalTest/resources/examples/classes/AnotherBuildConfig.klib.renamedTarget.dump @@ -0,0 +1,14 @@ +// Merged KLib ABI Dump +// Targets: [androidNativeArm32, androidNativeArm64, androidNativeX64, androidNativeX86, linux.linuxArm64, linuxX64, mingwX64] +// Rendering settings: +// - Signature version: 2 +// - Show manifest properties: true +// - Show declarations: true + +// Library unique name: +final class org.different.pack/BuildConfig { // org.different.pack/BuildConfig|null[0] + constructor () // org.different.pack/BuildConfig.|(){}[0] + final fun f1(): kotlin/Int // org.different.pack/BuildConfig.f1|f1(){}[0] + final val p1 // org.different.pack/BuildConfig.p1|{}p1[0] + final fun (): kotlin/Int // org.different.pack/BuildConfig.p1.|(){}[0] +} diff --git a/src/main/kotlin/BinaryCompatibilityValidatorPlugin.kt b/src/main/kotlin/BinaryCompatibilityValidatorPlugin.kt index 40c60b33..b1e7c0c3 100644 --- a/src/main/kotlin/BinaryCompatibilityValidatorPlugin.kt +++ b/src/main/kotlin/BinaryCompatibilityValidatorPlugin.kt @@ -13,6 +13,8 @@ import org.gradle.api.tasks.* import org.jetbrains.kotlin.gradle.dsl.* import org.jetbrains.kotlin.gradle.plugin.* import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget +import org.jetbrains.kotlin.gradle.targets.js.KotlinWasmTargetType +import org.jetbrains.kotlin.gradle.targets.js.ir.KotlinJsIrTarget import org.jetbrains.kotlin.konan.target.HostManager import org.jetbrains.kotlin.library.abi.ExperimentalLibraryAbiReader import org.jetbrains.kotlin.library.abi.LibraryAbiReader @@ -444,7 +446,7 @@ private class KlibValidationPipelineBuilder( group = "other" strictValidation = extension.klib.strictValidation groupTargetNames = extension.klib.useTargetGroupAliases - targets = supportedTargets() + supportedCanonicalTargets = supportedCanonicalTargetNames() inputAbiFile = klibApiDir.get().resolve(klibDumpFileName) outputAbiFile = klibOutputDir.resolve(klibDumpFileName) } @@ -497,7 +499,7 @@ private class KlibValidationPipelineBuilder( ) { val kotlin = project.kotlinMultiplatform - val supportedTargetsProvider = supportedTargets() + val supportedTargetsProvider = supportedCanonicalTargetNames() kotlin.targets.matching { it.emitsKlib }.configureEach { currentTarget -> val mainCompilations = currentTarget.mainCompilations if (mainCompilations.none()) { @@ -561,7 +563,7 @@ private class KlibValidationPipelineBuilder( } } - private fun Project.supportedTargets(): Provider> { + private fun Project.supportedCanonicalTargetNames(): Provider> { val banned = bannedTargets() // for testing only return project.provider { val hm = HostManager() @@ -574,9 +576,8 @@ private class KlibValidationPipelineBuilder( true } } - .map { - it.targetName - }.toSet() + .map { extractUnderlyingTarget(it) } + .toSet() } } @@ -660,9 +661,17 @@ private val KotlinTarget.jvmBased: Boolean } private fun extractUnderlyingTarget(target: KotlinTarget): String { - return when (target) { - is KotlinNativeTarget -> konanTargetNameMapping[target.konanTarget.name]!! - else -> target.name + if (target is KotlinNativeTarget) { + return konanTargetNameMapping[target.konanTarget.name]!! + } + return when (target.platformType) { + KotlinPlatformType.js -> "js" + KotlinPlatformType.wasm -> when ((target as KotlinJsIrTarget).wasmTargetType) { + KotlinWasmTargetType.WASI -> "wasmWasi" + KotlinWasmTargetType.JS -> "wasmJs" + else -> throw IllegalStateException("Unreachable") + } + else -> throw IllegalArgumentException("Unsupported platform type: ${target.platformType}") } } diff --git a/src/main/kotlin/KotlinKlibExtractSupportedTargetsAbiTask.kt b/src/main/kotlin/KotlinKlibExtractSupportedTargetsAbiTask.kt index b34df1a3..a20b3aac 100644 --- a/src/main/kotlin/KotlinKlibExtractSupportedTargetsAbiTask.kt +++ b/src/main/kotlin/KotlinKlibExtractSupportedTargetsAbiTask.kt @@ -7,7 +7,6 @@ package kotlinx.validation import kotlinx.validation.klib.KlibAbiDumpFormat import kotlinx.validation.klib.KlibAbiDumpMerger -import kotlinx.validation.klib.TargetHierarchy import org.gradle.api.DefaultTask import org.gradle.api.provider.Provider import org.gradle.api.tasks.* @@ -34,9 +33,12 @@ internal abstract class KotlinKlibExtractSupportedTargetsAbiTask : DefaultTask() /** * Provider returning targets supported by the host compiler. + * + * Canonical target names ([kotlinx.validation.klib.Target.canonicalName]) are used + * instead of user-defined named to filter out only targets that are actually not supported by the host compiler. */ @get:Input - lateinit var targets: Provider> + lateinit var supportedCanonicalTargets: Provider> /** * Refer to [KlibValidationSettings.strictValidation] for details. @@ -56,8 +58,8 @@ internal abstract class KotlinKlibExtractSupportedTargetsAbiTask : DefaultTask() error("Project ABI file $inputAbiFile is empty.") } val dump = KlibAbiDumpMerger().apply { loadMergedDump(inputAbiFile) } - val enabledTargets = targets.get() - val targetsToRemove = dump.targets.filter { it.name !in enabledTargets } + val enabledTargets = supportedCanonicalTargets.get() + val targetsToRemove = dump.targets.filter { it.canonicalName !in enabledTargets } if (targetsToRemove.isNotEmpty() && strictValidation) { throw IllegalStateException( "Validation could not be performed as some targets are not available " + From 8621b3ee5c323d9c068ab97aba690567c4fcb211 Mon Sep 17 00:00:00 2001 From: Filipp Zhinkin Date: Fri, 1 Mar 2024 15:30:51 +0100 Subject: [PATCH 29/53] Use fully qualified names of supported targets Alternatively, Target instances could be tossed around, but that require making them Serializable. The next step would be to make Targets public, and it's not that nice to expose Serializable class publicly (as it's not really meant to be serializable). --- .../BinaryCompatibilityValidatorPlugin.kt | 9 ++++---- ...otlinKlibExtractSupportedTargetsAbiTask.kt | 8 +++---- ...linKlibInferAbiForUnsupportedTargetTask.kt | 21 +++++++++++-------- 3 files changed, 20 insertions(+), 18 deletions(-) diff --git a/src/main/kotlin/BinaryCompatibilityValidatorPlugin.kt b/src/main/kotlin/BinaryCompatibilityValidatorPlugin.kt index b1e7c0c3..88d7faf3 100644 --- a/src/main/kotlin/BinaryCompatibilityValidatorPlugin.kt +++ b/src/main/kotlin/BinaryCompatibilityValidatorPlugin.kt @@ -5,6 +5,7 @@ package kotlinx.validation +import kotlinx.validation.klib.Target import kotlinx.validation.klib.konanTargetNameMapping import org.gradle.api.* import org.gradle.api.plugins.* @@ -446,7 +447,7 @@ private class KlibValidationPipelineBuilder( group = "other" strictValidation = extension.klib.strictValidation groupTargetNames = extension.klib.useTargetGroupAliases - supportedCanonicalTargets = supportedCanonicalTargetNames() + supportedTargets = supportedTargets() inputAbiFile = klibApiDir.get().resolve(klibDumpFileName) outputAbiFile = klibOutputDir.resolve(klibDumpFileName) } @@ -499,7 +500,7 @@ private class KlibValidationPipelineBuilder( ) { val kotlin = project.kotlinMultiplatform - val supportedTargetsProvider = supportedCanonicalTargetNames() + val supportedTargetsProvider = supportedTargets() kotlin.targets.matching { it.emitsKlib }.configureEach { currentTarget -> val mainCompilations = currentTarget.mainCompilations if (mainCompilations.none()) { @@ -563,7 +564,7 @@ private class KlibValidationPipelineBuilder( } } - private fun Project.supportedCanonicalTargetNames(): Provider> { + private fun Project.supportedTargets(): Provider> { val banned = bannedTargets() // for testing only return project.provider { val hm = HostManager() @@ -576,7 +577,7 @@ private class KlibValidationPipelineBuilder( true } } - .map { extractUnderlyingTarget(it) } + .map { Target(it.targetName, extractUnderlyingTarget(it)).toString() } .toSet() } } diff --git a/src/main/kotlin/KotlinKlibExtractSupportedTargetsAbiTask.kt b/src/main/kotlin/KotlinKlibExtractSupportedTargetsAbiTask.kt index a20b3aac..7e31469c 100644 --- a/src/main/kotlin/KotlinKlibExtractSupportedTargetsAbiTask.kt +++ b/src/main/kotlin/KotlinKlibExtractSupportedTargetsAbiTask.kt @@ -7,6 +7,7 @@ package kotlinx.validation import kotlinx.validation.klib.KlibAbiDumpFormat import kotlinx.validation.klib.KlibAbiDumpMerger +import kotlinx.validation.klib.Target import org.gradle.api.DefaultTask import org.gradle.api.provider.Provider import org.gradle.api.tasks.* @@ -33,12 +34,9 @@ internal abstract class KotlinKlibExtractSupportedTargetsAbiTask : DefaultTask() /** * Provider returning targets supported by the host compiler. - * - * Canonical target names ([kotlinx.validation.klib.Target.canonicalName]) are used - * instead of user-defined named to filter out only targets that are actually not supported by the host compiler. */ @get:Input - lateinit var supportedCanonicalTargets: Provider> + lateinit var supportedTargets: Provider> /** * Refer to [KlibValidationSettings.strictValidation] for details. @@ -58,7 +56,7 @@ internal abstract class KotlinKlibExtractSupportedTargetsAbiTask : DefaultTask() error("Project ABI file $inputAbiFile is empty.") } val dump = KlibAbiDumpMerger().apply { loadMergedDump(inputAbiFile) } - val enabledTargets = supportedCanonicalTargets.get() + val enabledTargets = supportedTargets.get().map { Target.parse(it).canonicalName } val targetsToRemove = dump.targets.filter { it.canonicalName !in enabledTargets } if (targetsToRemove.isNotEmpty() && strictValidation) { throw IllegalStateException( diff --git a/src/main/kotlin/KotlinKlibInferAbiForUnsupportedTargetTask.kt b/src/main/kotlin/KotlinKlibInferAbiForUnsupportedTargetTask.kt index 398b588d..0f168200 100644 --- a/src/main/kotlin/KotlinKlibInferAbiForUnsupportedTargetTask.kt +++ b/src/main/kotlin/KotlinKlibInferAbiForUnsupportedTargetTask.kt @@ -75,17 +75,19 @@ internal abstract class KotlinKlibInferAbiForUnsupportedTargetTask : DefaultTask @TaskAction internal fun generate() { val unsupportedTarget = Target(unsupportedTargetName, unsupportedTargetCanonicalName) - // find a set of supported targets that are closer to unsupported target in the hierarchy - val matchingTargets = findMatchingTargets(unsupportedTarget) - val target2outFile = supportedTargets.get().keysToMap { - File(outputApiDir).parentFile.resolve(it).resolve(dumpFileName) + val supportedTargetNames = supportedTargets.get().map { Target.parse(it) }.toSet() + // Find a set of supported targets that are closer to unsupported target in the hierarchy. + // Note that dumps are stored using configurable name, but grouped by the canonical target name. + val matchingTargets = findMatchingTargets(supportedTargetNames, unsupportedTarget) + val target2outFile = supportedTargetNames.keysToMap { + File(outputApiDir).parentFile.resolve(it.name).resolve(dumpFileName) } // given a set of similar targets, combine their ABI files into a single merged dump and consider it // a common ABI that should be shared by the unsupported target as well val commonDump = KlibAbiDumpMerger() for (target in matchingTargets) { - commonDump.addIndividualDump(target, target2outFile[target]!!) + commonDump.addIndividualDump(target.name, target2outFile[target]!!) } commonDump.retainCommonAbi() @@ -121,13 +123,14 @@ internal abstract class KotlinKlibInferAbiForUnsupportedTargetTask : DefaultTask ) } - private fun findMatchingTargets(unsupportedTarget: Target): Set { + private fun findMatchingTargets(supportedTargets: Set, unsupportedTarget: Target): Collection { var currentGroup: String? = unsupportedTarget.canonicalName while (currentGroup != null) { // If a current group has some supported targets, use them. - val groupTargets = TargetHierarchy.targets(currentGroup).intersect(supportedTargets.get()) - if (groupTargets.isNotEmpty()) { - return groupTargets + val groupTargets = TargetHierarchy.targets(currentGroup) + val matchingTargets = supportedTargets.filter { groupTargets.contains(it.canonicalName) } + if (matchingTargets.isNotEmpty()) { + return matchingTargets } // Otherwise, walk up the target hierarchy. currentGroup = TargetHierarchy.parent(currentGroup) From 672901bf81d9fdc86802a129d5b6217e3627c05c Mon Sep 17 00:00:00 2001 From: Filipp Zhinkin Date: Wed, 6 Mar 2024 16:40:27 +0100 Subject: [PATCH 30/53] Made KlibTarget public, renamed its components and reordered them in a serialized form --- api/binary-compatibility-validator.api | 13 +++ .../validation/test/KLibVerificationTests.kt | 2 +- ...> MultiPlatformSingleJvmKlibTargetTest.kt} | 2 +- .../AnotherBuildConfig.klib.clash.dump | 4 +- .../AnotherBuildConfig.klib.custom.dump | 2 +- ...AnotherBuildConfig.klib.renamedTarget.dump | 2 +- ...lDeclarations.klib.with.guessed.linux.dump | 2 +- .../BinaryCompatibilityValidatorPlugin.kt | 4 +- ...otlinKlibExtractSupportedTargetsAbiTask.kt | 6 +- ...linKlibInferAbiForUnsupportedTargetTask.kt | 16 +-- src/main/kotlin/api/klib/KlibTarget.kt | 61 ++++++++++ src/main/kotlin/klib/KlibAbiDumpFileMerger.kt | 108 +++++++----------- src/test/kotlin/tests/KlibAbiMergingTest.kt | 56 ++++----- ...rchyTest.kt => KlibTargetHierarchyTest.kt} | 2 +- .../merged_with_aliases_and_custom_names.abi | 10 +- 15 files changed, 167 insertions(+), 123 deletions(-) rename src/functionalTest/kotlin/kotlinx/validation/test/{MultiPlatformSingleJvmTargetTest.kt => MultiPlatformSingleJvmKlibTargetTest.kt} (97%) create mode 100644 src/main/kotlin/api/klib/KlibTarget.kt rename src/test/kotlin/tests/{TargetHierarchyTest.kt => KlibTargetHierarchyTest.kt} (98%) diff --git a/api/binary-compatibility-validator.api b/api/binary-compatibility-validator.api index 969670f1..ba74d668 100644 --- a/api/binary-compatibility-validator.api +++ b/api/binary-compatibility-validator.api @@ -110,3 +110,16 @@ public final class kotlinx/validation/api/KotlinSignaturesLoadingKt { public static synthetic fun retainExplicitlyIncludedIfDeclared$default (Ljava/util/List;Ljava/util/Collection;Ljava/util/Collection;Ljava/util/Collection;ILjava/lang/Object;)Ljava/util/List; } +public final class kotlinx/validation/api/klib/KlibTarget { + public static final field Companion Lkotlinx/validation/api/klib/KlibTarget$Companion; + public fun equals (Ljava/lang/Object;)Z + public final fun getConfigurableName ()Ljava/lang/String; + public final fun getTargetName ()Ljava/lang/String; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class kotlinx/validation/api/klib/KlibTarget$Companion { + public final fun parse (Ljava/lang/String;)Lkotlinx/validation/api/klib/KlibTarget; +} + diff --git a/src/functionalTest/kotlin/kotlinx/validation/test/KLibVerificationTests.kt b/src/functionalTest/kotlin/kotlinx/validation/test/KLibVerificationTests.kt index bb573b2c..4f774819 100644 --- a/src/functionalTest/kotlin/kotlinx/validation/test/KLibVerificationTests.kt +++ b/src/functionalTest/kotlin/kotlinx/validation/test/KLibVerificationTests.kt @@ -629,7 +629,7 @@ internal class KLibVerificationTests : BaseKotlinGradleTest() { runner.buildAndFail().apply { Assertions.assertThat(output).contains( " -// Targets: [androidNativeArm32, androidNativeArm64, androidNativeX64, " + - "androidNativeX86, linux.linuxArm64, linuxX64, mingwX64]" + "androidNativeX86, linuxArm64.linux, linuxX64, mingwX64]" ) } } diff --git a/src/functionalTest/kotlin/kotlinx/validation/test/MultiPlatformSingleJvmTargetTest.kt b/src/functionalTest/kotlin/kotlinx/validation/test/MultiPlatformSingleJvmKlibTargetTest.kt similarity index 97% rename from src/functionalTest/kotlin/kotlinx/validation/test/MultiPlatformSingleJvmTargetTest.kt rename to src/functionalTest/kotlin/kotlinx/validation/test/MultiPlatformSingleJvmKlibTargetTest.kt index f141fa83..dde16426 100644 --- a/src/functionalTest/kotlin/kotlinx/validation/test/MultiPlatformSingleJvmTargetTest.kt +++ b/src/functionalTest/kotlin/kotlinx/validation/test/MultiPlatformSingleJvmKlibTargetTest.kt @@ -10,7 +10,7 @@ import org.assertj.core.api.Assertions.assertThat import org.junit.Test import java.io.File -internal class MultiPlatformSingleJvmTargetTest : BaseKotlinGradleTest() { +internal class MultiPlatformSingleJvmKlibTargetTest : BaseKotlinGradleTest() { private fun BaseKotlinScope.createProjectHierarchyWithPluginOnRoot() { settingsGradleKts { resolve("/examples/gradle/settings/settings-name-testproject.gradle.kts") diff --git a/src/functionalTest/resources/examples/classes/AnotherBuildConfig.klib.clash.dump b/src/functionalTest/resources/examples/classes/AnotherBuildConfig.klib.clash.dump index c5cdbe7c..c3493903 100644 --- a/src/functionalTest/resources/examples/classes/AnotherBuildConfig.klib.clash.dump +++ b/src/functionalTest/resources/examples/classes/AnotherBuildConfig.klib.clash.dump @@ -1,6 +1,6 @@ // Merged KLib ABI Dump -// Targets: [androidNativeArm32, androidNativeArm64, androidNativeX64, androidNativeX86, linux.linuxX64, linuxArm64, mingwX64] -// Alias: linux => [linux.linuxX64, linuxArm64] +// Targets: [androidNativeArm32, androidNativeArm64, androidNativeX64, androidNativeX86, linuxArm64, linuxX64.linux, mingwX64] +// Alias: linux => [linuxArm64, linuxX64.linux] // Rendering settings: // - Signature version: 2 // - Show manifest properties: true diff --git a/src/functionalTest/resources/examples/classes/AnotherBuildConfig.klib.custom.dump b/src/functionalTest/resources/examples/classes/AnotherBuildConfig.klib.custom.dump index 4de2ecfc..d09de432 100644 --- a/src/functionalTest/resources/examples/classes/AnotherBuildConfig.klib.custom.dump +++ b/src/functionalTest/resources/examples/classes/AnotherBuildConfig.klib.custom.dump @@ -1,5 +1,5 @@ // Merged KLib ABI Dump -// Targets: [linuxA.linuxX64, linuxB.linuxX64] +// Targets: [linuxX64.linuxA, linuxX64.linuxB] // Rendering settings: // - Signature version: 2 // - Show manifest properties: true diff --git a/src/functionalTest/resources/examples/classes/AnotherBuildConfig.klib.renamedTarget.dump b/src/functionalTest/resources/examples/classes/AnotherBuildConfig.klib.renamedTarget.dump index effc6a79..ede29f43 100644 --- a/src/functionalTest/resources/examples/classes/AnotherBuildConfig.klib.renamedTarget.dump +++ b/src/functionalTest/resources/examples/classes/AnotherBuildConfig.klib.renamedTarget.dump @@ -1,5 +1,5 @@ // Merged KLib ABI Dump -// Targets: [androidNativeArm32, androidNativeArm64, androidNativeX64, androidNativeX86, linux.linuxArm64, linuxX64, mingwX64] +// Targets: [androidNativeArm32, androidNativeArm64, androidNativeX64, androidNativeX86, linuxArm64.linux, linuxX64, mingwX64] // Rendering settings: // - Signature version: 2 // - Show manifest properties: true diff --git a/src/functionalTest/resources/examples/classes/TopLevelDeclarations.klib.with.guessed.linux.dump b/src/functionalTest/resources/examples/classes/TopLevelDeclarations.klib.with.guessed.linux.dump index 501732fc..a485b92e 100644 --- a/src/functionalTest/resources/examples/classes/TopLevelDeclarations.klib.with.guessed.linux.dump +++ b/src/functionalTest/resources/examples/classes/TopLevelDeclarations.klib.with.guessed.linux.dump @@ -1,5 +1,5 @@ // Merged KLib ABI Dump -// Targets: [androidNativeArm32, androidNativeArm64, androidNativeX64, androidNativeX86, linux.linuxX64, linuxArm64, mingwX64] +// Targets: [androidNativeArm32, androidNativeArm64, androidNativeX64, androidNativeX86, linuxArm64, linuxX64.linux, mingwX64] // Rendering settings: // - Signature version: 2 // - Show manifest properties: true diff --git a/src/main/kotlin/BinaryCompatibilityValidatorPlugin.kt b/src/main/kotlin/BinaryCompatibilityValidatorPlugin.kt index 88d7faf3..dbb7acbe 100644 --- a/src/main/kotlin/BinaryCompatibilityValidatorPlugin.kt +++ b/src/main/kotlin/BinaryCompatibilityValidatorPlugin.kt @@ -5,7 +5,7 @@ package kotlinx.validation -import kotlinx.validation.klib.Target +import kotlinx.validation.api.klib.KlibTarget import kotlinx.validation.klib.konanTargetNameMapping import org.gradle.api.* import org.gradle.api.plugins.* @@ -577,7 +577,7 @@ private class KlibValidationPipelineBuilder( true } } - .map { Target(it.targetName, extractUnderlyingTarget(it)).toString() } + .map { KlibTarget(it.targetName, extractUnderlyingTarget(it)).toString() } .toSet() } } diff --git a/src/main/kotlin/KotlinKlibExtractSupportedTargetsAbiTask.kt b/src/main/kotlin/KotlinKlibExtractSupportedTargetsAbiTask.kt index 7e31469c..9f1acda5 100644 --- a/src/main/kotlin/KotlinKlibExtractSupportedTargetsAbiTask.kt +++ b/src/main/kotlin/KotlinKlibExtractSupportedTargetsAbiTask.kt @@ -7,7 +7,7 @@ package kotlinx.validation import kotlinx.validation.klib.KlibAbiDumpFormat import kotlinx.validation.klib.KlibAbiDumpMerger -import kotlinx.validation.klib.Target +import kotlinx.validation.api.klib.KlibTarget import org.gradle.api.DefaultTask import org.gradle.api.provider.Provider import org.gradle.api.tasks.* @@ -56,8 +56,8 @@ internal abstract class KotlinKlibExtractSupportedTargetsAbiTask : DefaultTask() error("Project ABI file $inputAbiFile is empty.") } val dump = KlibAbiDumpMerger().apply { loadMergedDump(inputAbiFile) } - val enabledTargets = supportedTargets.get().map { Target.parse(it).canonicalName } - val targetsToRemove = dump.targets.filter { it.canonicalName !in enabledTargets } + val enabledTargets = supportedTargets.get().map { KlibTarget.parse(it).targetName } + val targetsToRemove = dump.targets.filter { it.targetName !in enabledTargets } if (targetsToRemove.isNotEmpty() && strictValidation) { throw IllegalStateException( "Validation could not be performed as some targets are not available " + diff --git a/src/main/kotlin/KotlinKlibInferAbiForUnsupportedTargetTask.kt b/src/main/kotlin/KotlinKlibInferAbiForUnsupportedTargetTask.kt index 0f168200..3a3a43e2 100644 --- a/src/main/kotlin/KotlinKlibInferAbiForUnsupportedTargetTask.kt +++ b/src/main/kotlin/KotlinKlibInferAbiForUnsupportedTargetTask.kt @@ -7,7 +7,7 @@ package kotlinx.validation import kotlinx.validation.klib.KlibAbiDumpFormat import kotlinx.validation.klib.KlibAbiDumpMerger -import kotlinx.validation.klib.Target +import kotlinx.validation.api.klib.KlibTarget import kotlinx.validation.klib.TargetHierarchy import org.gradle.api.DefaultTask import org.gradle.api.provider.Provider @@ -74,20 +74,20 @@ internal abstract class KotlinKlibInferAbiForUnsupportedTargetTask : DefaultTask @TaskAction internal fun generate() { - val unsupportedTarget = Target(unsupportedTargetName, unsupportedTargetCanonicalName) - val supportedTargetNames = supportedTargets.get().map { Target.parse(it) }.toSet() + val unsupportedTarget = KlibTarget(unsupportedTargetName, unsupportedTargetCanonicalName) + val supportedTargetNames = supportedTargets.get().map { KlibTarget.parse(it) }.toSet() // Find a set of supported targets that are closer to unsupported target in the hierarchy. // Note that dumps are stored using configurable name, but grouped by the canonical target name. val matchingTargets = findMatchingTargets(supportedTargetNames, unsupportedTarget) val target2outFile = supportedTargetNames.keysToMap { - File(outputApiDir).parentFile.resolve(it.name).resolve(dumpFileName) + File(outputApiDir).parentFile.resolve(it.configurableName).resolve(dumpFileName) } // given a set of similar targets, combine their ABI files into a single merged dump and consider it // a common ABI that should be shared by the unsupported target as well val commonDump = KlibAbiDumpMerger() for (target in matchingTargets) { - commonDump.addIndividualDump(target.name, target2outFile[target]!!) + commonDump.addIndividualDump(target.configurableName, target2outFile[target]!!) } commonDump.retainCommonAbi() @@ -123,12 +123,12 @@ internal abstract class KotlinKlibInferAbiForUnsupportedTargetTask : DefaultTask ) } - private fun findMatchingTargets(supportedTargets: Set, unsupportedTarget: Target): Collection { - var currentGroup: String? = unsupportedTarget.canonicalName + private fun findMatchingTargets(supportedTargets: Set, unsupportedTarget: KlibTarget): Collection { + var currentGroup: String? = unsupportedTarget.targetName while (currentGroup != null) { // If a current group has some supported targets, use them. val groupTargets = TargetHierarchy.targets(currentGroup) - val matchingTargets = supportedTargets.filter { groupTargets.contains(it.canonicalName) } + val matchingTargets = supportedTargets.filter { groupTargets.contains(it.targetName) } if (matchingTargets.isNotEmpty()) { return matchingTargets } diff --git a/src/main/kotlin/api/klib/KlibTarget.kt b/src/main/kotlin/api/klib/KlibTarget.kt new file mode 100644 index 00000000..adf89ada --- /dev/null +++ b/src/main/kotlin/api/klib/KlibTarget.kt @@ -0,0 +1,61 @@ +/* + * Copyright 2016-2024 JetBrains s.r.o. + * Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file. + */ + +package kotlinx.validation.api.klib + + +/** + * Target name consisting of two parts: a [configurableName] that could be configured by a user, and an [targetName] + * that names a target platform and could not be configured by a user. + * + * When serialized, the target represented as a tuple `.`, like `ios.iosArm64`. + * If both names are the same (they are by default, unless a user decides to use a custom name), the serialized + * from is shortened to a single term. For example, `macosArm64.macosArm64` and `macosArm64` are a long and a short + * serialized forms of the same target. + */ +public class KlibTarget internal constructor( + public val configurableName: String, + public val targetName: String) +{ + public companion object { + public fun parse(line: String): KlibTarget { + require(line.isNotBlank()) { "Target name could not be blank." } + if (!line.contains('.')) { + return KlibTarget(line) + } + val parts = line.split('.') + if (parts.size != 2 || parts.any { it.isBlank() }) { + throw IllegalArgumentException( + "Target has illegal name format: \"$line\", expected: ." + ) + } + return KlibTarget(parts[1], parts[0]) + } + } + + + override fun toString(): String = + if (configurableName == targetName) configurableName else "$targetName.$configurableName" + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as KlibTarget + + if (configurableName != other.configurableName) return false + if (targetName != other.targetName) return false + + return true + } + + override fun hashCode(): Int { + var result = configurableName.hashCode() + result = 31 * result + targetName.hashCode() + return result + } +} + +internal fun KlibTarget(name: String) = KlibTarget(name, name) diff --git a/src/main/kotlin/klib/KlibAbiDumpFileMerger.kt b/src/main/kotlin/klib/KlibAbiDumpFileMerger.kt index 6652abd7..cbee3706 100644 --- a/src/main/kotlin/klib/KlibAbiDumpFileMerger.kt +++ b/src/main/kotlin/klib/KlibAbiDumpFileMerger.kt @@ -5,40 +5,10 @@ package kotlinx.validation.klib +import kotlinx.validation.api.klib.KlibTarget import java.io.File import java.nio.file.Files -/** - * Target name consisting of two parts: a [name] that could be configured by a user, and an [canonicalName] - * that names a target platform and could not be configured by a user. - * - * When serialized, the target represented as a tuple `.`, like `ios.iosArm64`. - * If both names are the same (they are by default, unless a user decides to use a custom name), the serialized - * from is shortened to a single term. For example, `macosArm64.macosArm64` and `macosArm64` are a long and a short - * serialized forms of the same target. - */ -internal data class Target(val name: String, val canonicalName: String) { - companion object { - fun parse(line: String): Target { - require(line.isNotBlank()) { "Target name could not be blank." } - if (!line.contains('.')) { - return Target(line) - } - val parts = line.split('.') - if (parts.size != 2 || parts.any { it.isBlank() }) { - throw IllegalArgumentException( - "Target has illegal name format: \"$line\", expected: ." - ) - } - return Target(parts[0], parts[1]) - } - } - - override fun toString(): String = if (name == canonicalName) name else "$name.$canonicalName" -} - -internal fun Target(name: String) = Target(name, name) - internal class LinesProvider(private val lines: Iterator) : Iterator { private var nextLine: String? = null @@ -88,14 +58,14 @@ private fun String.depth(): Int { return indentation / INDENT_WIDTH } -private fun parseBcvTargetsLine(line: String): Set { +private fun parseBcvTargetsLine(line: String): Set { val trimmedLine = line.trimStart(' ') check(trimmedLine.startsWith(TARGETS_LIST_PREFIX) && trimmedLine.endsWith(TARGETS_LIST_SUFFIX)) { "Not a targets list line: \"$line\"" } return trimmedLine.substring(TARGETS_LIST_PREFIX.length, trimmedLine.length - 1) .split(TARGETS_DELIMITER) - .map { Target.parse(it) } + .map { KlibTarget.parse(it) } .toSet() } @@ -113,11 +83,11 @@ private class KlibAbiDumpHeader( val content: List, val underlyingTarget: String? ) { - fun extractTarget(targetName: String): Target { + fun extractTarget(targetName: String): KlibTarget { if (underlyingTarget == "wasm") { - return Target(targetName) + return KlibTarget(targetName) } - return Target(targetName, underlyingTarget ?: targetName) + return KlibTarget(targetName, underlyingTarget ?: targetName) } } @@ -125,14 +95,14 @@ private class KlibAbiDumpHeader( * A class representing a textual KLib ABI dump, either a regular one, or a merged. */ internal class KlibAbiDumpMerger { - private val targetsMut: MutableSet = mutableSetOf() + private val targetsMut: MutableSet = mutableSetOf() private val headerContent: MutableList = mutableListOf() private val topLevelDeclaration: DeclarationContainer = DeclarationContainer("") /** * All targets for which this dump contains declarations. */ - public val targets: Set = targetsMut + public val targets: Set = targetsMut public fun loadMergedDump(file: File) { require(file.exists()) { "File does not exist: $file" } @@ -159,8 +129,8 @@ internal class KlibAbiDumpMerger { if (isMergedFile) check(this.targetsMut.isEmpty()) { "Merged dump could only be loaded once." } lines.checkFileFormat(isMergedFile) - val aliases = mutableMapOf>() - val bcvTargets = mutableSetOf() + val aliases = mutableMapOf>() + val bcvTargets = mutableSetOf() if (isMergedFile) { bcvTargets.addAll(lines.parseTargets()) aliases.putAll(lines.parseAliases()) @@ -234,7 +204,7 @@ internal class KlibAbiDumpMerger { } } - private fun LinesProvider.parseTargets(): Set { + private fun LinesProvider.parseTargets(): Set { val line = peek() require(line != null) { "List of targets expected, but there are no more lines left." @@ -246,8 +216,8 @@ internal class KlibAbiDumpMerger { return parseBcvTargetsLine(line) } - private fun LinesProvider.parseAliases(): Map> { - val aliases = mutableMapOf>() + private fun LinesProvider.parseAliases(): Map> { + val aliases = mutableMapOf>() while (peek()?.startsWith(ALIAS_PREFIX) == true) { val line = next() val trimmedLine = line.substring(ALIAS_PREFIX.length) @@ -261,7 +231,7 @@ internal class KlibAbiDumpMerger { trimmedLine.length - 1 ) .split(",") - .map { Target(it.trim()) } + .map { KlibTarget(it.trim()) } .toSet() aliases[name] = targets } @@ -339,9 +309,9 @@ internal class KlibAbiDumpMerger { private fun LinesProvider.parseDeclaration( depth: Int, parent: DeclarationContainer, - allTargets: Set, + allTargets: Set, isMergedFile: Boolean, - aliases: Map> + aliases: Map> ): DeclarationContainer { val line = peek()!! return if (line.startsWith(" ".repeat(depth * INDENT_WIDTH) + TARGETS_LIST_PREFIX)) { @@ -354,7 +324,7 @@ internal class KlibAbiDumpMerger { // so we must use it. val targets = parseBcvTargetsLine(line) val expandedTargets = targets.flatMap { - aliases[it.name] ?: listOf(it) + aliases[it.configurableName] ?: listOf(it) }.toSet() parent.createOrUpdateChildren(next(), expandedTargets) } else { @@ -384,7 +354,7 @@ internal class KlibAbiDumpMerger { if (dumpFormat.singleTargetDump) { // Reconstruct a manifest partially for a single target dump, // so the dump could be later merged correctly. - val canonicalName = targets.single().canonicalName + val canonicalName = targets.single().targetName val platform = platformByCanonicalName(canonicalName) appendable.append(PLATFORM_PREFIX).append(platform).append('\n') if (platform == "NATIVE") { @@ -400,7 +370,7 @@ internal class KlibAbiDumpMerger { private fun createFormatter(dumpFormat: KlibAbiDumpFormat): KLibsTargetsFormatter { return if (dumpFormat.useGroupAliases) { for (target in targets) { - val node = TargetHierarchy.hierarchyIndex[target.canonicalName] + val node = TargetHierarchy.hierarchyIndex[target.targetName] if (node != null && node.allLeafs.size == 1 && node.allLeafs.first() != node.node.name) { throw IllegalStateException( "Can't use target aliases as one of the this dump's targets" + @@ -418,7 +388,7 @@ internal class KlibAbiDumpMerger { * Remove the [target] from this dump. * If some declaration was declared only for [target], it will be removed from the dump. */ - fun remove(target: Target) { + fun remove(target: KlibTarget) { if (!targetsMut.contains(target)) { return } @@ -433,7 +403,7 @@ internal class KlibAbiDumpMerger { * 1) it defined for some [targets] subset including [target], but not for all [targets]; * 2) it defined for all [targets], but contains target-specific child declaration. */ - fun retainTargetSpecificAbi(target: Target) { + fun retainTargetSpecificAbi(target: KlibTarget) { if (!targetsMut.contains(target)) { targetsMut.clear() topLevelDeclaration.children.clear() @@ -475,7 +445,7 @@ internal class KlibAbiDumpMerger { /** * For each declaration change targets to a specified [targets] set. */ - fun overrideTargets(targets: Set) { + fun overrideTargets(targets: Set) { targetsMut.clear() targetsMut.addAll(targets) @@ -494,11 +464,11 @@ internal class KlibAbiDumpMerger { * declarations. */ internal class DeclarationContainer(val text: String, val parent: DeclarationContainer? = null) { - val targets: MutableSet = mutableSetOf() + val targets: MutableSet = mutableSetOf() val children: MutableMap = mutableMapOf() var delimiter: String? = null - fun createOrUpdateChildren(text: String, targets: Set): DeclarationContainer { + fun createOrUpdateChildren(text: String, targets: Set): DeclarationContainer { val child = children.computeIfAbsent(text) { val newChild = DeclarationContainer(it, this) newChild @@ -508,7 +478,7 @@ internal class DeclarationContainer(val text: String, val parent: DeclarationCon } fun dump( - appendable: Appendable, allTargets: Set, + appendable: Appendable, allTargets: Set, dumpFormat: KlibAbiDumpFormat, formatter: KLibsTargetsFormatter ) { if (targets != allTargets && !dumpFormat.singleTargetDump) { @@ -526,7 +496,7 @@ internal class DeclarationContainer(val text: String, val parent: DeclarationCon } } - fun remove(target: Target) { + fun remove(target: KlibTarget) { if (parent != null && !targets.contains(target)) { return } @@ -534,7 +504,7 @@ internal class DeclarationContainer(val text: String, val parent: DeclarationCon mutateChildrenAndRemoveTargetless { it.remove(target) } } - fun retainSpecific(target: Target, allTargets: Set) { + fun retainSpecific(target: KlibTarget, allTargets: Set) { if (parent != null && !targets.contains(target)) { children.clear() targets.clear() @@ -554,7 +524,7 @@ internal class DeclarationContainer(val text: String, val parent: DeclarationCon } } - fun retainCommon(commonTargets: Set) { + fun retainCommon(commonTargets: Set) { if (parent != null && targets != commonTargets) { children.clear() targets.clear() @@ -578,12 +548,12 @@ internal class DeclarationContainer(val text: String, val parent: DeclarationCon } } - private fun addTargetRecursively(first: Target) { + private fun addTargetRecursively(first: KlibTarget) { targets.add(first) children.forEach { it.value.addTargetRecursively(first) } } - fun overrideTargets(targets: Set) { + fun overrideTargets(targets: Set) { this.targets.clear() this.targets.addAll(targets) children.forEach { it.value.overrideTargets(targets) } @@ -630,24 +600,24 @@ private object DeclarationsComparator : Comparator { } internal interface KLibsTargetsFormatter { - fun formatHeader(targets: Set): String + fun formatHeader(targets: Set): String - fun formatDeclarationTargets(targets: Set): String + fun formatDeclarationTargets(targets: Set): String } private object DefaultFormatter : KLibsTargetsFormatter { - override fun formatHeader(targets: Set): String { + override fun formatHeader(targets: Set): String { return formatDeclarationTargets(targets) } - override fun formatDeclarationTargets(targets: Set): String { + override fun formatDeclarationTargets(targets: Set): String { return targets.map { it.toString() }.sorted() .joinToString(TARGETS_DELIMITER, TARGETS_LIST_PREFIX, TARGETS_LIST_SUFFIX) { it } } } private class GroupingFormatter(klibDump: KlibAbiDumpMerger) : KLibsTargetsFormatter { - private data class Alias(val name: String, val targets: Set) + private data class Alias(val name: String, val targets: Set) private val aliases: List @@ -661,7 +631,7 @@ private class GroupingFormatter(klibDump: KlibAbiDumpMerger) : KLibsTargetsForma // intersect with all targets to use only enabled targets in aliases // intersection is based on underlying target name as a set of such names is fixed val leafs = it.value.allLeafs - val availableTargets = allTargets.asSequence().filter { leafs.contains(it.canonicalName) }.toSet() + val availableTargets = allTargets.asSequence().filter { leafs.contains(it.targetName) }.toSet() if (availableTargets.isNotEmpty()) { aliasesBuilder.add(Alias(it.key, availableTargets)) } @@ -671,7 +641,7 @@ private class GroupingFormatter(klibDump: KlibAbiDumpMerger) : KLibsTargetsForma aliasesBuilder.removeIf { it.targets.size < 2 } aliasesBuilder.removeIf { it.targets == allTargets } // collect all actually used target groups and remove all unused aliases - val usedAliases = mutableSetOf>() + val usedAliases = mutableSetOf>() fun visitor(decl: DeclarationContainer) { usedAliases.add(decl.targets) decl.visit(::visitor) @@ -697,7 +667,7 @@ private class GroupingFormatter(klibDump: KlibAbiDumpMerger) : KLibsTargetsForma aliases = aliasesBuilder.reversed() } - override fun formatHeader(targets: Set): String { + override fun formatHeader(targets: Set): String { return buildString { append( targets.asSequence().map { it.toString() }.sorted().joinToString( @@ -714,7 +684,7 @@ private class GroupingFormatter(klibDump: KlibAbiDumpMerger) : KLibsTargetsForma } } - override fun formatDeclarationTargets(targets: Set): String { + override fun formatDeclarationTargets(targets: Set): String { val targetsMut = targets.toMutableSet() val resultingTargets = mutableListOf() for (alias in aliases) { diff --git a/src/test/kotlin/tests/KlibAbiMergingTest.kt b/src/test/kotlin/tests/KlibAbiMergingTest.kt index 771df9cb..2e06d4b5 100644 --- a/src/test/kotlin/tests/KlibAbiMergingTest.kt +++ b/src/test/kotlin/tests/KlibAbiMergingTest.kt @@ -7,7 +7,7 @@ package tests import kotlinx.validation.klib.KlibAbiDumpFormat import kotlinx.validation.klib.KlibAbiDumpMerger -import kotlinx.validation.klib.Target +import kotlinx.validation.api.klib.KlibTarget import org.junit.Rule import org.junit.Test import org.junit.rules.TemporaryFolder @@ -52,29 +52,29 @@ class KlibAbiMergingTest { @Test fun testTargetNames() { - assertEquals("a.b", Target("a", "b").toString()) - assertEquals("a", Target("a").toString()) - assertEquals("a", Target("a", "a").toString()) - - assertFailsWith { Target.parse("") } - assertFailsWith { Target.parse(" ") } - assertFailsWith { Target.parse("a.b.c") } - assertFailsWith { Target.parse("a.") } - assertFailsWith { Target.parse(".a") } - - Target.parse("a.b").also { - assertEquals("a", it.name) - assertEquals("b", it.canonicalName) + assertEquals("a.b", KlibTarget("b", "a").toString()) + assertEquals("a", KlibTarget("a").toString()) + assertEquals("a", KlibTarget("a", "a").toString()) + + assertFailsWith { KlibTarget.parse("") } + assertFailsWith { KlibTarget.parse(" ") } + assertFailsWith { KlibTarget.parse("a.b.c") } + assertFailsWith { KlibTarget.parse("a.") } + assertFailsWith { KlibTarget.parse(".a") } + + KlibTarget.parse("a.b").also { + assertEquals("b", it.configurableName) + assertEquals("a", it.targetName) } - Target.parse("a.a").also { - assertEquals("a", it.name) - assertEquals("a", it.canonicalName) + KlibTarget.parse("a.a").also { + assertEquals("a", it.configurableName) + assertEquals("a", it.targetName) } - Target.parse("a").also { - assertEquals("a", it.name) - assertEquals("a", it.canonicalName) + KlibTarget.parse("a").also { + assertEquals("a", it.configurableName) + assertEquals("a", it.targetName) } } @@ -177,7 +177,7 @@ class KlibAbiMergingTest { val targets = listOf("androidNativeArm64", "linuxArm64", "linuxX64", "tvOsX64") targets.forEach { target -> - klib.remove(Target(target)) + klib.remove(KlibTarget(target)) klib.addIndividualDump(file("/merge/diverging/$target.api")) } @@ -206,14 +206,14 @@ class KlibAbiMergingTest { val klib = KlibAbiDumpMerger() klib.loadMergedDump(file("/merge/parseNarrowChildrenDecls/merged.abi")) - klib.remove(Target("linuxArm64")) + klib.remove(KlibTarget("linuxArm64")) val written1 = dumpToFile(klib) assertContentEquals( lines("/merge/parseNarrowChildrenDecls/withoutLinuxArm64.abi"), Files.readAllLines(written1.toPath()).asSequence() ) - klib.remove(Target("linuxX64")) + klib.remove(KlibTarget("linuxX64")) val written2 = dumpToFile(klib) assertContentEquals( lines("/merge/parseNarrowChildrenDecls/withoutLinuxAll.abi"), @@ -225,7 +225,7 @@ class KlibAbiMergingTest { fun guessAbi() { val klib = KlibAbiDumpMerger() klib.loadMergedDump(file("/merge/guess/merged.api")) - klib.retainTargetSpecificAbi(Target("linuxArm64")) + klib.retainTargetSpecificAbi(KlibTarget("linuxArm64")) val retainedLinuxAbiDump = dumpToFile(klib) assertContentEquals( @@ -235,7 +235,7 @@ class KlibAbiMergingTest { val commonAbi = KlibAbiDumpMerger() commonAbi.loadMergedDump(file("/merge/guess/merged.api")) - commonAbi.remove(Target("linuxArm64")) + commonAbi.remove(KlibTarget("linuxArm64")) commonAbi.retainCommonAbi() val commonAbiDump = dumpToFile(commonAbi) @@ -245,7 +245,7 @@ class KlibAbiMergingTest { ) commonAbi.mergeTargetSpecific(klib) - commonAbi.overrideTargets(setOf(Target("linuxArm64"))) + commonAbi.overrideTargets(setOf(KlibTarget("linuxArm64"))) val guessedAbiDump = dumpToFile(commonAbi, true) assertContentEquals( @@ -333,7 +333,7 @@ class KlibAbiMergingTest { val lib = KlibAbiDumpMerger().apply { loadMergedDump(file("/merge/diverging/merged_with_aliases_and_custom_names.abi")) } - val targets = lib.targets.filter { it.canonicalName != "linuxArm64" } + val targets = lib.targets.filter { it.targetName != "linuxArm64" } targets.forEach { lib.remove(it) } val extracted = dumpToFile(lib, singleTargetDump = true) assertContentEquals( @@ -349,7 +349,7 @@ class KlibAbiMergingTest { fun checkExtracted(targetName: String, expectedFile: String) { val lib = KlibAbiDumpMerger().apply { loadMergedDump(file(mergedPath)) } val targets = lib.targets - targets.filter { it.name != targetName }.forEach { lib.remove(it) } + targets.filter { it.configurableName != targetName }.forEach { lib.remove(it) } val dump = dumpToFile(lib, singleTargetDump = true) assertContentEquals( lines(expectedFile), diff --git a/src/test/kotlin/tests/TargetHierarchyTest.kt b/src/test/kotlin/tests/KlibTargetHierarchyTest.kt similarity index 98% rename from src/test/kotlin/tests/TargetHierarchyTest.kt rename to src/test/kotlin/tests/KlibTargetHierarchyTest.kt index a18b3a4b..f6447c87 100644 --- a/src/test/kotlin/tests/TargetHierarchyTest.kt +++ b/src/test/kotlin/tests/KlibTargetHierarchyTest.kt @@ -12,7 +12,7 @@ import kotlin.test.assertEquals import kotlin.test.assertNotNull import kotlin.test.assertTrue -class TargetHierarchyTest { +class KlibTargetHierarchyTest { @Test fun testHierarchy() { assertContentEquals(listOf("linuxArm64", "linux", "native", "all"), diff --git a/src/test/resources/merge/diverging/merged_with_aliases_and_custom_names.abi b/src/test/resources/merge/diverging/merged_with_aliases_and_custom_names.abi index 3c338b15..9db0ee51 100644 --- a/src/test/resources/merge/diverging/merged_with_aliases_and_custom_names.abi +++ b/src/test/resources/merge/diverging/merged_with_aliases_and_custom_names.abi @@ -1,6 +1,6 @@ // Merged KLib ABI Dump -// Targets: [android.androidNativeArm64, linux.linuxArm64, linuxX64, tvosX64] -// Alias: linux => [linux.linuxArm64, linuxX64] +// Targets: [androidNativeArm64.android, linuxArm64.linux, linuxX64, tvosX64] +// Alias: linux => [linuxArm64.linux, linuxX64] // Rendering settings: // - Signature version: 2 // - Show manifest properties: false @@ -18,13 +18,13 @@ final fun org.example/ShardedClass(kotlin/Int, kotlin/Float, kotlin/Long): org.e final fun (kotlin/Long).org.example/add(kotlin/Long): kotlin/Long // org.example/add|add@kotlin.Long(kotlin.Long){}[0] // Targets: [linux] final fun org.example/add(kotlin/Long, kotlin/Long): kotlin/Long // org.example/add|add(kotlin.Long;kotlin.Long){}[0] -// Targets: [android.androidNativeArm64] +// Targets: [androidNativeArm64.android] final class org.example/X { // org.example/X|null[0] constructor (kotlin/Int) // org.example/X.|(kotlin.Int){}[0] } -// Targets: [android.androidNativeArm64] +// Targets: [androidNativeArm64.android] final fun (org.example/X).org.example/add(org.example/X): org.example/X // org.example/add|add@org.example.X(org.example.X){}[0] -// Targets: [android.androidNativeArm64] +// Targets: [androidNativeArm64.android] final fun org.example/add(org.example/X, org.example/X): org.example/X // org.example/add|add(org.example.X;org.example.X){}[0] // Targets: [tvosX64] final fun (kotlin/Int).org.example/add(kotlin/Int): kotlin/Int // org.example/add|add@kotlin.Int(kotlin.Int){}[0] From a4768c6acfb8e287d11f40c40810ca0334979db7 Mon Sep 17 00:00:00 2001 From: Filipp Zhinkin Date: Wed, 6 Mar 2024 17:07:01 +0100 Subject: [PATCH 31/53] Use KlibSignatureVersion instead of int to represent a signature version --- api/binary-compatibility-validator.api | 16 +++++++- .../signatures/invalid.gradle.kts | 2 +- .../configuration/signatures/v1.gradle.kts | 2 +- src/main/kotlin/ApiValidationExtension.kt | 4 +- .../BinaryCompatibilityValidatorPlugin.kt | 2 +- src/main/kotlin/KotlinKlibAbiBuildTask.kt | 20 +++++++--- .../kotlin/api/klib/KlibSignatureVersion.kt | 37 +++++++++++++++++++ 7 files changed, 71 insertions(+), 12 deletions(-) create mode 100644 src/main/kotlin/api/klib/KlibSignatureVersion.kt diff --git a/api/binary-compatibility-validator.api b/api/binary-compatibility-validator.api index ba74d668..b75aef67 100644 --- a/api/binary-compatibility-validator.api +++ b/api/binary-compatibility-validator.api @@ -58,11 +58,11 @@ public abstract interface annotation class kotlinx/validation/ExternalApi : java public class kotlinx/validation/KlibValidationSettings { public fun ()V public final fun getEnabled ()Z - public final fun getSignatureVersion ()Ljava/lang/Integer; + public final fun getSignatureVersion ()Lkotlinx/validation/api/klib/KlibSignatureVersion; public final fun getStrictValidation ()Z public final fun getUseTargetGroupAliases ()Z public final fun setEnabled (Z)V - public final fun setSignatureVersion (Ljava/lang/Integer;)V + public final fun setSignatureVersion (Lkotlinx/validation/api/klib/KlibSignatureVersion;)V public final fun setStrictValidation (Z)V public final fun setUseTargetGroupAliases (Z)V } @@ -110,6 +110,18 @@ public final class kotlinx/validation/api/KotlinSignaturesLoadingKt { public static synthetic fun retainExplicitlyIncludedIfDeclared$default (Ljava/util/List;Ljava/util/Collection;Ljava/util/Collection;Ljava/util/Collection;ILjava/lang/Object;)Ljava/util/List; } +public final class kotlinx/validation/api/klib/KlibSignatureVersion { + public static final field Companion Lkotlinx/validation/api/klib/KlibSignatureVersion$Companion; + public fun equals (Ljava/lang/Object;)Z + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class kotlinx/validation/api/klib/KlibSignatureVersion$Companion { + public final fun getLATEST ()Lkotlinx/validation/api/klib/KlibSignatureVersion; + public final fun of (I)Lkotlinx/validation/api/klib/KlibSignatureVersion; +} + public final class kotlinx/validation/api/klib/KlibTarget { public static final field Companion Lkotlinx/validation/api/klib/KlibTarget$Companion; public fun equals (Ljava/lang/Object;)Z diff --git a/src/functionalTest/resources/examples/gradle/configuration/signatures/invalid.gradle.kts b/src/functionalTest/resources/examples/gradle/configuration/signatures/invalid.gradle.kts index eafc6ccc..ddb2f310 100644 --- a/src/functionalTest/resources/examples/gradle/configuration/signatures/invalid.gradle.kts +++ b/src/functionalTest/resources/examples/gradle/configuration/signatures/invalid.gradle.kts @@ -5,6 +5,6 @@ configure { klib { - signatureVersion = 100500 + signatureVersion = kotlinx.validation.api.klib.KlibSignatureVersion.of(100500) } } diff --git a/src/functionalTest/resources/examples/gradle/configuration/signatures/v1.gradle.kts b/src/functionalTest/resources/examples/gradle/configuration/signatures/v1.gradle.kts index f7277c58..28f0322f 100644 --- a/src/functionalTest/resources/examples/gradle/configuration/signatures/v1.gradle.kts +++ b/src/functionalTest/resources/examples/gradle/configuration/signatures/v1.gradle.kts @@ -4,5 +4,5 @@ */ configure { - klib.signatureVersion = 1 + klib.signatureVersion = kotlinx.validation.api.klib.KlibSignatureVersion.of(1) } diff --git a/src/main/kotlin/ApiValidationExtension.kt b/src/main/kotlin/ApiValidationExtension.kt index 61773ab7..d3451dd9 100644 --- a/src/main/kotlin/ApiValidationExtension.kt +++ b/src/main/kotlin/ApiValidationExtension.kt @@ -5,6 +5,8 @@ package kotlinx.validation +import kotlinx.validation.api.klib.KlibSignatureVersion + public open class ApiValidationExtension { /** @@ -111,7 +113,7 @@ public open class KlibValidationSettings { * However, if a klib supports multiple signature versions simultaneously, one my explicitly specify the version * that will be dumped to prevent changes in a dump file. */ - public var signatureVersion: Int? = null + public var signatureVersion: KlibSignatureVersion = KlibSignatureVersion.LATEST /** * Fail validation if some build targets are not supported by the host compiler. * By default, ABI dumped only for supported files will be validated. This option makes validation behavior diff --git a/src/main/kotlin/BinaryCompatibilityValidatorPlugin.kt b/src/main/kotlin/BinaryCompatibilityValidatorPlugin.kt index dbb7acbe..90ed2e75 100644 --- a/src/main/kotlin/BinaryCompatibilityValidatorPlugin.kt +++ b/src/main/kotlin/BinaryCompatibilityValidatorPlugin.kt @@ -603,7 +603,7 @@ private class KlibValidationPipelineBuilder( "Complementary task and shouldn't be called manually" klibFile = project.files(project.provider { compilation.output.classesDirs }) compilationDependencies = project.files(project.provider { compilation.compileDependencyFiles }) - signatureVersion = extension.klib.signatureVersion + signatureVersion = SerializableSignatureVersion(extension.klib.signatureVersion) outputApiFile = apiBuildDir.resolve(klibDumpFileName) } return buildTask diff --git a/src/main/kotlin/KotlinKlibAbiBuildTask.kt b/src/main/kotlin/KotlinKlibAbiBuildTask.kt index 55b10c3d..c2e17276 100644 --- a/src/main/kotlin/KotlinKlibAbiBuildTask.kt +++ b/src/main/kotlin/KotlinKlibAbiBuildTask.kt @@ -5,12 +5,20 @@ package kotlinx.validation +import kotlinx.validation.api.klib.KlibSignatureVersion import org.gradle.api.file.FileCollection import org.gradle.api.tasks.Input import org.gradle.api.tasks.InputFiles import org.gradle.api.tasks.Optional import org.gradle.api.tasks.TaskAction import org.jetbrains.kotlin.library.abi.* +import java.io.Serializable + +internal class SerializableSignatureVersion(val version: Int) : Serializable { + constructor(version: KlibSignatureVersion) : this(version.version) + + fun toKlibSignatureVersion(): KlibSignatureVersion = KlibSignatureVersion(version) +} /** * Generates a text file with a KLib ABI dump for a single klib. @@ -34,7 +42,7 @@ internal abstract class KotlinKlibAbiBuildTask : BuildTaskBase() { */ @Optional @get:Input - var signatureVersion: Int? = null + var signatureVersion: SerializableSignatureVersion = SerializableSignatureVersion(KlibSignatureVersion.LATEST) /** * Name of a target [klibFile] was compiled for. @@ -72,17 +80,17 @@ internal abstract class KotlinKlibAbiBuildTask : BuildTaskBase() { } val supportedVersions = parsedAbi.signatureVersions.asSequence().filter { it.isSupportedByAbiReader } - val sigVersion = if (signatureVersion != null) { + val sigVersion = if (signatureVersion.toKlibSignatureVersion() != KlibSignatureVersion.LATEST) { val versionNumbers = supportedVersions.map { it.versionNumber }.toSortedSet() - if (signatureVersion !in versionNumbers) { + if (signatureVersion.version !in versionNumbers) { throw IllegalArgumentException( - "Unsupported KLib signature version '$signatureVersion'. " + + "Unsupported KLib signature version '${signatureVersion.version}'. " + "Supported versions are: $versionNumbers" ) } - AbiSignatureVersion.resolveByVersionNumber(signatureVersion!!) + AbiSignatureVersion.resolveByVersionNumber(signatureVersion.version) } else { - supportedVersions.maxByOrNull(AbiSignatureVersion::versionNumber) + supportedVersions.filter { it.isSupportedByAbiReader }.maxByOrNull(AbiSignatureVersion::versionNumber) ?: throw IllegalStateException("Can't choose signatureVersion") } diff --git a/src/main/kotlin/api/klib/KlibSignatureVersion.kt b/src/main/kotlin/api/klib/KlibSignatureVersion.kt new file mode 100644 index 00000000..d7b63ad3 --- /dev/null +++ b/src/main/kotlin/api/klib/KlibSignatureVersion.kt @@ -0,0 +1,37 @@ +/* + * Copyright 2016-2024 JetBrains s.r.o. + * Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file. + */ + +package kotlinx.validation.api.klib + +public class KlibSignatureVersion internal constructor(internal val version: Int) { + + public companion object { + public fun of(value: Int): KlibSignatureVersion { + require(value >= 1) { + "Invalid version value, expected positive value: $value" + } + return KlibSignatureVersion(value) + } + + public val LATEST: KlibSignatureVersion = KlibSignatureVersion(Int.MIN_VALUE) + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as KlibSignatureVersion + + return version == other.version + } + + override fun hashCode(): Int { + return version.hashCode() + } + + override fun toString(): String { + return "KlibSignatureVersion($version)" + } +} From f3cf046e7c28fd5ec5bcb4f15b86845ba32d6f79 Mon Sep 17 00:00:00 2001 From: Filipp Zhinkin Date: Wed, 6 Mar 2024 17:21:11 +0100 Subject: [PATCH 32/53] Remove an option disabling target name grouping --- api/binary-compatibility-validator.api | 2 - src/main/kotlin/ApiValidationExtension.kt | 8 --- .../BinaryCompatibilityValidatorPlugin.kt | 3 -- ...otlinKlibExtractSupportedTargetsAbiTask.kt | 12 +---- src/main/kotlin/KotlinKlibMergeAbiTask.kt | 12 +---- src/main/kotlin/klib/KlibAbiDumpFileMerger.kt | 50 ++++++------------- src/test/kotlin/tests/KlibAbiMergingTest.kt | 27 +++------- src/test/resources/merge/diverging/merged.abi | 5 +- .../merge/idempotent/bcv-klib-test.abi | 17 ++++--- 9 files changed, 35 insertions(+), 101 deletions(-) diff --git a/api/binary-compatibility-validator.api b/api/binary-compatibility-validator.api index b75aef67..c71bba5d 100644 --- a/api/binary-compatibility-validator.api +++ b/api/binary-compatibility-validator.api @@ -60,11 +60,9 @@ public class kotlinx/validation/KlibValidationSettings { public final fun getEnabled ()Z public final fun getSignatureVersion ()Lkotlinx/validation/api/klib/KlibSignatureVersion; public final fun getStrictValidation ()Z - public final fun getUseTargetGroupAliases ()Z public final fun setEnabled (Z)V public final fun setSignatureVersion (Lkotlinx/validation/api/klib/KlibSignatureVersion;)V public final fun setStrictValidation (Z)V - public final fun setUseTargetGroupAliases (Z)V } public class kotlinx/validation/KotlinApiBuildTask : kotlinx/validation/BuildTaskBase { diff --git a/src/main/kotlin/ApiValidationExtension.kt b/src/main/kotlin/ApiValidationExtension.kt index d3451dd9..7a879775 100644 --- a/src/main/kotlin/ApiValidationExtension.kt +++ b/src/main/kotlin/ApiValidationExtension.kt @@ -120,12 +120,4 @@ public open class KlibValidationSettings { * stricter and treats having unsupported targets as an error. */ public var strictValidation: Boolean = false - /** - * For declarations available only on some targets, replace a comment listing these targets - * with a short alias. - * - * Group aliases are based on [the default hierarchy template](https://kotlinlang.org/docs/multiplatform-hierarchy.html#see-the-full-hierarchy-template) - * and enabled by default. - */ - public var useTargetGroupAliases: Boolean = true } diff --git a/src/main/kotlin/BinaryCompatibilityValidatorPlugin.kt b/src/main/kotlin/BinaryCompatibilityValidatorPlugin.kt index 90ed2e75..d189aca4 100644 --- a/src/main/kotlin/BinaryCompatibilityValidatorPlugin.kt +++ b/src/main/kotlin/BinaryCompatibilityValidatorPlugin.kt @@ -446,7 +446,6 @@ private class KlibValidationPipelineBuilder( "the golden file stored in the project" group = "other" strictValidation = extension.klib.strictValidation - groupTargetNames = extension.klib.useTargetGroupAliases supportedTargets = supportedTargets() inputAbiFile = klibApiDir.get().resolve(klibDumpFileName) outputAbiFile = klibOutputDir.resolve(klibDumpFileName) @@ -465,7 +464,6 @@ private class KlibValidationPipelineBuilder( "into a single merged KLib ABI dump" dumpFileName = klibDumpFileName mergedFile = klibMergeDir.resolve(klibDumpFileName) - groupTargetNames = extension.klib.useTargetGroupAliases } private fun Project.mergeKlibsUmbrellaTask( @@ -477,7 +475,6 @@ private class KlibValidationPipelineBuilder( "different targets into a single merged KLib ABI dump" dumpFileName = klibDumpFileName mergedFile = klibMergeDir.resolve(klibDumpFileName) - groupTargetNames = extension.klib.useTargetGroupAliases } fun Project.bannedTargets(): Set { diff --git a/src/main/kotlin/KotlinKlibExtractSupportedTargetsAbiTask.kt b/src/main/kotlin/KotlinKlibExtractSupportedTargetsAbiTask.kt index 9f1acda5..2f6837e1 100644 --- a/src/main/kotlin/KotlinKlibExtractSupportedTargetsAbiTask.kt +++ b/src/main/kotlin/KotlinKlibExtractSupportedTargetsAbiTask.kt @@ -44,12 +44,6 @@ internal abstract class KotlinKlibExtractSupportedTargetsAbiTask : DefaultTask() @Input var strictValidation: Boolean = false - /** - * Refer to [KlibValidationSettings.useTargetGroupAliases] for details. - */ - @Input - var groupTargetNames: Boolean = true - @TaskAction internal fun generate() { if (inputAbiFile.length() == 0L) { @@ -67,10 +61,6 @@ internal abstract class KotlinKlibExtractSupportedTargetsAbiTask : DefaultTask() for (target in targetsToRemove) { dump.remove(target) } - outputAbiFile.bufferedWriter().use { dump.dump(it, KlibAbiDumpFormat(useGroupAliases = canUseGroupAliases())) } - } - - private fun canUseGroupAliases(): Boolean { - return groupTargetNames + outputAbiFile.bufferedWriter().use { dump.dump(it) } } } diff --git a/src/main/kotlin/KotlinKlibMergeAbiTask.kt b/src/main/kotlin/KotlinKlibMergeAbiTask.kt index 810a7281..c98fcd52 100644 --- a/src/main/kotlin/KotlinKlibMergeAbiTask.kt +++ b/src/main/kotlin/KotlinKlibMergeAbiTask.kt @@ -45,12 +45,6 @@ internal abstract class KotlinKlibMergeAbiTask : DefaultTask() { @Input lateinit var dumpFileName: String - /** - * Refer to [KlibValidationSettings.useTargetGroupAliases] for details. - */ - @Input - var groupTargetNames: Boolean = true - internal fun addInput(target: String, file: File) { targetToFile[target] = file } @@ -61,10 +55,6 @@ internal abstract class KotlinKlibMergeAbiTask : DefaultTask() { targets.forEach { targetName -> builder.addIndividualDump(targetName, targetToFile[targetName]!!.resolve(dumpFileName)) } - mergedFile.bufferedWriter().use { builder.dump(it, KlibAbiDumpFormat(useGroupAliases = canUseGroupAliases())) } - } - - private fun canUseGroupAliases(): Boolean { - return groupTargetNames + mergedFile.bufferedWriter().use { builder.dump(it) } } } diff --git a/src/main/kotlin/klib/KlibAbiDumpFileMerger.kt b/src/main/kotlin/klib/KlibAbiDumpFileMerger.kt index cbee3706..6ae2298d 100644 --- a/src/main/kotlin/klib/KlibAbiDumpFileMerger.kt +++ b/src/main/kotlin/klib/KlibAbiDumpFileMerger.kt @@ -75,8 +75,7 @@ internal data class KlibAbiDumpFormat( * This flag is mostly intended for dumping an inferred ABI and * requires a [KlibAbiDumpMerger] containing only a single target. */ - val singleTargetDump: Boolean = false, - val useGroupAliases: Boolean = false + val singleTargetDump: Boolean = false ) private class KlibAbiDumpHeader( @@ -339,7 +338,7 @@ internal class KlibAbiDumpMerger { } fun dump(appendable: Appendable, dumpFormat: KlibAbiDumpFormat = KlibAbiDumpFormat()) { - val formatter = createFormatter(dumpFormat) + val formatter = createFormatter() if (dumpFormat.singleTargetDump) { require(targets.size == 1) { "Can skip target inclusion only if the dump contains a single target, but it contains: $targets" @@ -367,21 +366,17 @@ internal class KlibAbiDumpMerger { } } - private fun createFormatter(dumpFormat: KlibAbiDumpFormat): KLibsTargetsFormatter { - return if (dumpFormat.useGroupAliases) { - for (target in targets) { - val node = TargetHierarchy.hierarchyIndex[target.targetName] - if (node != null && node.allLeafs.size == 1 && node.allLeafs.first() != node.node.name) { - throw IllegalStateException( - "Can't use target aliases as one of the this dump's targets" + - " has the same name as a group in the default targets hierarchy: $target" - ) - } + private fun createFormatter(): KLibsTargetsFormatter { + for (target in targets) { + val node = TargetHierarchy.hierarchyIndex[target.targetName] + if (node != null && node.allLeafs.size == 1 && node.allLeafs.first() != node.node.name) { + throw IllegalStateException( + "Can't use target aliases as one of the this dump's targets" + + " has the same name as a group in the default targets hierarchy: $target" + ) } - return GroupingFormatter(this) - } else { - DefaultFormatter } + return KLibsTargetsFormatter(this) } /** @@ -599,24 +594,7 @@ private object DeclarationsComparator : Comparator { } } -internal interface KLibsTargetsFormatter { - fun formatHeader(targets: Set): String - - fun formatDeclarationTargets(targets: Set): String -} - -private object DefaultFormatter : KLibsTargetsFormatter { - override fun formatHeader(targets: Set): String { - return formatDeclarationTargets(targets) - } - - override fun formatDeclarationTargets(targets: Set): String { - return targets.map { it.toString() }.sorted() - .joinToString(TARGETS_DELIMITER, TARGETS_LIST_PREFIX, TARGETS_LIST_SUFFIX) { it } - } -} - -private class GroupingFormatter(klibDump: KlibAbiDumpMerger) : KLibsTargetsFormatter { +internal class KLibsTargetsFormatter(klibDump: KlibAbiDumpMerger) { private data class Alias(val name: String, val targets: Set) private val aliases: List @@ -667,7 +645,7 @@ private class GroupingFormatter(klibDump: KlibAbiDumpMerger) : KLibsTargetsForma aliases = aliasesBuilder.reversed() } - override fun formatHeader(targets: Set): String { + fun formatHeader(targets: Set): String { return buildString { append( targets.asSequence().map { it.toString() }.sorted().joinToString( @@ -684,7 +662,7 @@ private class GroupingFormatter(klibDump: KlibAbiDumpMerger) : KLibsTargetsForma } } - override fun formatDeclarationTargets(targets: Set): String { + fun formatDeclarationTargets(targets: Set): String { val targetsMut = targets.toMutableSet() val resultingTargets = mutableListOf() for (alias in aliases) { diff --git a/src/test/kotlin/tests/KlibAbiMergingTest.kt b/src/test/kotlin/tests/KlibAbiMergingTest.kt index 2e06d4b5..2f8d7aa2 100644 --- a/src/test/kotlin/tests/KlibAbiMergingTest.kt +++ b/src/test/kotlin/tests/KlibAbiMergingTest.kt @@ -1,5 +1,5 @@ /* - * Copyright 2016-2023 JetBrains s.r.o. + * Copyright 2016-2024 JetBrains s.r.o. * Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file. */ @@ -37,14 +37,11 @@ class KlibAbiMergingTest { return res.bufferedReader().lineSequence() } - private fun dumpToFile(klib: KlibAbiDumpMerger, - singleTargetDump: Boolean = false, - useAliases: Boolean = false): File { + private fun dumpToFile(klib: KlibAbiDumpMerger, singleTargetDump: Boolean = false): File { val file = tempDir.newFile() FileWriter(file).use { klib.dump(it, KlibAbiDumpFormat( - singleTargetDump = singleTargetDump, - useGroupAliases = useAliases + singleTargetDump = singleTargetDump )) } return file @@ -96,7 +93,7 @@ class KlibAbiMergingTest { val klib = KlibAbiDumpMerger() klib.addIndividualDump(file("/merge/identical/dump_macos_arm64.abi")) klib.addIndividualDump(file("/merge/identical/dump_linux_x64.abi")) - val merged = dumpToFile(klib, useAliases = true) + val merged = dumpToFile(klib) // there are no groups other than "all", so no aliases will be added assertContentEquals( @@ -134,7 +131,7 @@ class KlibAbiMergingTest { targets.forEach { klib.addIndividualDump(file("/merge/diverging/$it.api")) } - val merged = dumpToFile(klib, useAliases = true) + val merged = dumpToFile(klib) assertContentEquals( lines("/merge/diverging/merged_with_aliases.abi"), Files.readAllLines(merged.toPath()).asSequence() @@ -142,18 +139,6 @@ class KlibAbiMergingTest { } } - @Test - fun aliasedDumpParsing() { - val klib = KlibAbiDumpMerger() - klib.loadMergedDump(file("/merge/diverging/merged_with_aliases.abi")) - - val withoutAliases = dumpToFile(klib, useAliases = false) - assertContentEquals( - lines("/merge/diverging/merged.abi"), - Files.readAllLines(withoutAliases.toPath()).asSequence() - ) - } - @Test fun mergeDumpsWithDivergedHeaders() { val klib = KlibAbiDumpMerger() @@ -321,7 +306,7 @@ class KlibAbiMergingTest { addIndividualDump(file("/merge/diverging/tvOsX64.api")) } - val dump = dumpToFile(lib, useAliases = true) + val dump = dumpToFile(lib) assertContentEquals( lines("/merge/diverging/merged_with_aliases_and_custom_names.abi"), Files.readAllLines(dump.toPath()).asSequence() diff --git a/src/test/resources/merge/diverging/merged.abi b/src/test/resources/merge/diverging/merged.abi index 96ced25f..dedae58f 100644 --- a/src/test/resources/merge/diverging/merged.abi +++ b/src/test/resources/merge/diverging/merged.abi @@ -1,5 +1,6 @@ // Merged KLib ABI Dump // Targets: [androidNativeArm64, linuxArm64, linuxX64, tvosX64] +// Alias: linux => [linuxArm64, linuxX64] // Rendering settings: // - Signature version: 2 // - Show manifest properties: false @@ -13,9 +14,9 @@ final class org.example/ShardedClass { // org.example/ShardedClass|null[0] final fun (): kotlin/Int // org.example/ShardedClass.value.|(){}[0] } final fun org.example/ShardedClass(kotlin/Int, kotlin/Float, kotlin/Long): org.example/ShardedClass // org.example/ShardedClass|ShardedClass(kotlin.Int;kotlin.Float;kotlin.Long){}[0] -// Targets: [linuxArm64, linuxX64] +// Targets: [linux] final fun (kotlin/Long).org.example/add(kotlin/Long): kotlin/Long // org.example/add|add@kotlin.Long(kotlin.Long){}[0] -// Targets: [linuxArm64, linuxX64] +// Targets: [linux] final fun org.example/add(kotlin/Long, kotlin/Long): kotlin/Long // org.example/add|add(kotlin.Long;kotlin.Long){}[0] // Targets: [androidNativeArm64] final class org.example/X { // org.example/X|null[0] diff --git a/src/test/resources/merge/idempotent/bcv-klib-test.abi b/src/test/resources/merge/idempotent/bcv-klib-test.abi index 8a4abf9a..959708b3 100644 --- a/src/test/resources/merge/idempotent/bcv-klib-test.abi +++ b/src/test/resources/merge/idempotent/bcv-klib-test.abi @@ -1,5 +1,8 @@ // Merged KLib ABI Dump // Targets: [androidNativeArm32, androidNativeArm64, iosSimulatorArm64, linuxArm64, linuxX64, macosArm64, tvosArm64, tvosSimulatorArm64, tvosX64] +// Alias: apple => [iosSimulatorArm64, macosArm64, tvosArm64, tvosSimulatorArm64, tvosX64] +// Alias: linux => [linuxArm64, linuxX64] +// Alias: androidNative => [androidNativeArm32, androidNativeArm64] // Rendering settings: // - Signature version: 2 // - Show manifest properties: false @@ -13,19 +16,19 @@ final class org.example/ShardedClass { // org.example/ShardedClass|null[0] final fun (): kotlin/Int // org.example/ShardedClass.value.|(){}[0] } final fun org.example/ShardedClass(kotlin/Int, kotlin/Float, kotlin/Long): org.example/ShardedClass // org.example/ShardedClass|ShardedClass(kotlin.Int;kotlin.Float;kotlin.Long){}[0] -// Targets: [iosSimulatorArm64, macosArm64, tvosArm64, tvosSimulatorArm64, tvosX64] +// Targets: [apple] final fun (kotlin/Int).org.example/add(kotlin/Int): kotlin/Int // org.example/add|add@kotlin.Int(kotlin.Int){}[0] -// Targets: [iosSimulatorArm64, macosArm64, tvosArm64, tvosSimulatorArm64, tvosX64] +// Targets: [apple] final fun org.example/add(kotlin/Int, kotlin/Int): kotlin/Int // org.example/add|add(kotlin.Int;kotlin.Int){}[0] -// Targets: [androidNativeArm32, androidNativeArm64] +// Targets: [androidNative] final class org.example/X { // org.example/X|null[0] constructor (kotlin/Int) // org.example/X.|(kotlin.Int){}[0] } -// Targets: [androidNativeArm32, androidNativeArm64] +// Targets: [androidNative] final fun (org.example/X).org.example/add(org.example/X): org.example/X // org.example/add|add@org.example.X(org.example.X){}[0] -// Targets: [androidNativeArm32, androidNativeArm64] +// Targets: [androidNative] final fun org.example/add(org.example/X, org.example/X): org.example/X // org.example/add|add(org.example.X;org.example.X){}[0] -// Targets: [linuxArm64, linuxX64] +// Targets: [linux] final fun (kotlin/Long).org.example/add(kotlin/Long): kotlin/Long // org.example/add|add@kotlin.Long(kotlin.Long){}[0] -// Targets: [linuxArm64, linuxX64] +// Targets: [linux] final fun org.example/add(kotlin/Long, kotlin/Long): kotlin/Long // org.example/add|add(kotlin.Long;kotlin.Long){}[0] From 75363a909c2293add6d8a809d0a0e5f5e9cc7595 Mon Sep 17 00:00:00 2001 From: Filipp Zhinkin Date: Thu, 7 Mar 2024 09:12:44 +0100 Subject: [PATCH 33/53] Fixed java class name to AbiQualifiedName transformation --- .../ignoreSubclasses/ignore.gradle.kts | 4 +- src/main/kotlin/KotlinKlibAbiBuildTask.kt | 67 ++++++++++++++----- src/test/kotlin/tests/AbiTest.kt | 30 --------- .../kotlin/tests/ClassNameConvertionTest.kt | 42 ++++++++++++ 4 files changed, 94 insertions(+), 49 deletions(-) delete mode 100644 src/test/kotlin/tests/AbiTest.kt create mode 100644 src/test/kotlin/tests/ClassNameConvertionTest.kt diff --git a/src/functionalTest/resources/examples/gradle/configuration/ignoreSubclasses/ignore.gradle.kts b/src/functionalTest/resources/examples/gradle/configuration/ignoreSubclasses/ignore.gradle.kts index da82967c..776e96fd 100644 --- a/src/functionalTest/resources/examples/gradle/configuration/ignoreSubclasses/ignore.gradle.kts +++ b/src/functionalTest/resources/examples/gradle/configuration/ignoreSubclasses/ignore.gradle.kts @@ -4,6 +4,6 @@ */ configure { - ignoredClasses.add("subclasses.A.B") - //ignoredClasses.add("subclasses.A\$B") + // ignoredClasses.add("subclasses.A.B") + ignoredClasses.add("subclasses.A\$B") } diff --git a/src/main/kotlin/KotlinKlibAbiBuildTask.kt b/src/main/kotlin/KotlinKlibAbiBuildTask.kt index c2e17276..0216e815 100644 --- a/src/main/kotlin/KotlinKlibAbiBuildTask.kt +++ b/src/main/kotlin/KotlinKlibAbiBuildTask.kt @@ -62,14 +62,10 @@ internal abstract class KotlinKlibAbiBuildTask : BuildTaskBase() { add(AbiReadingFilter.ExcludedPackages(ignoredPackages.map { AbiCompoundName(it) })) } if (ignoredClasses.isNotEmpty()) { - add(AbiReadingFilter.ExcludedClasses(ignoredClasses.flatMap { - generateQualifiedNames(it) - })) + add(AbiReadingFilter.ExcludedClasses(ignoredClasses.toKlibNames())) } if (nonPublicMarkers.isNotEmpty()) { - add(AbiReadingFilter.NonPublicMarkerAnnotations(nonPublicMarkers.flatMap { - generateQualifiedNames(it) - })) + add(AbiReadingFilter.NonPublicMarkerAnnotations(nonPublicMarkers.toKlibNames())) } } @@ -100,18 +96,55 @@ internal abstract class KotlinKlibAbiBuildTask : BuildTaskBase() { } } -@ExperimentalStdlibApi -@ExperimentalLibraryAbiReader -internal fun generateQualifiedNames(name: String): List { - if (!name.contains('.')) { - return listOf(AbiQualifiedName(AbiCompoundName(""), AbiCompoundName(name))) +// We're assuming that all names are in valid binary form as it's described in JVMLS §13.1: +// https://docs.oracle.com/javase/specs/jls/se21/html/jls-13.html#jls-13.1 +@OptIn(ExperimentalLibraryAbiReader::class) +private fun Collection.toKlibNames(): List = + this.map(String::toAbiQualifiedName).filterNotNull() + +@OptIn(ExperimentalLibraryAbiReader::class) +internal fun String.toAbiQualifiedName(): AbiQualifiedName? { + if (this.isBlank() || this.contains('/')) return null + // Easiest part: dissect package name from the class name + val idx = this.lastIndexOf('.') + if (idx == -1) { + return AbiQualifiedName(AbiCompoundName(""), this.classNameToCompoundName()) + } else { + val packageName = this.substring(0, idx) + val className = this.substring(idx + 1) + return AbiQualifiedName(AbiCompoundName(packageName), className.classNameToCompoundName()) } - val parts = name.split('.') - return buildList { - for (packageLength in parts.indices) { - val packageName = AbiCompoundName(parts.subList(0, packageLength).joinToString(".")) - val className = AbiCompoundName(parts.subList(packageLength, parts.size).joinToString(".")) - add(AbiQualifiedName(packageName, className)) +} + +@OptIn(ExperimentalLibraryAbiReader::class) +private fun String.classNameToCompoundName(): AbiCompoundName { + if (this.isEmpty()) return AbiCompoundName(this) + + val segments = mutableListOf() + val builder = StringBuilder() + + for (idx in this.indices) { + val c = this[idx] + // Don't treat a character as a separator if: + // - it's not a '$' + // - it's at the beginning of the segment + // - it's the last character of the string + if ( c != '$' || builder.isEmpty() || idx == this.length - 1) { + builder.append(c) + continue + } + check(c == '$') + // class$$$susbclass -> class.$$subclass, were at second $ here. + if (builder.last() == '$') { + builder.append(c) + continue } + + segments.add(builder.toString()) + builder.clear() + } + if (builder.isNotEmpty()) { + segments.add(builder.toString()) } + return AbiCompoundName(segments.joinToString(separator = ".")) } diff --git a/src/test/kotlin/tests/AbiTest.kt b/src/test/kotlin/tests/AbiTest.kt deleted file mode 100644 index aadacf1b..00000000 --- a/src/test/kotlin/tests/AbiTest.kt +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright 2016-2023 JetBrains s.r.o. - * Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file. - */ - -package kotlinx.validation.api.tests - -import kotlinx.validation.generateQualifiedNames -import org.jetbrains.kotlin.library.abi.* -import org.junit.Test -import kotlin.test.assertEquals - -class AbiTest { - @OptIn(ExperimentalLibraryAbiReader::class, ExperimentalStdlibApi::class) - @Test - fun generateAbiNames() { - assertEquals( - listOf( - AbiQualifiedName(AbiCompoundName(""), AbiCompoundName("foo.bar.Baz")), - AbiQualifiedName(AbiCompoundName("foo"), AbiCompoundName("bar.Baz")), - AbiQualifiedName(AbiCompoundName("foo.bar"), AbiCompoundName("Baz")) - ), - generateQualifiedNames("foo.bar.Baz") - ) - assertEquals( - listOf(AbiQualifiedName(AbiCompoundName(""), AbiCompoundName("Class"))), - generateQualifiedNames("Class") - ) - } -} diff --git a/src/test/kotlin/tests/ClassNameConvertionTest.kt b/src/test/kotlin/tests/ClassNameConvertionTest.kt new file mode 100644 index 00000000..fe556762 --- /dev/null +++ b/src/test/kotlin/tests/ClassNameConvertionTest.kt @@ -0,0 +1,42 @@ +/* + * Copyright 2016-2023 JetBrains s.r.o. + * Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file. + */ + +@file:OptIn(ExperimentalLibraryAbiReader::class) + +package kotlinx.validation.api.tests + +import kotlinx.validation.toAbiQualifiedName +import org.jetbrains.kotlin.library.abi.* +import org.junit.Test +import kotlin.test.assertEquals +import kotlin.test.assertNull + +class ClassNameConvertionTest { + @Test + fun testConvertBinaryName() { + assertNull("".toAbiQualifiedName()) + assertNull(" ".toAbiQualifiedName()) + assertNull("a/b/c/d".toAbiQualifiedName()) + assertNull("a.b.c/d.e".toAbiQualifiedName()) + + checkNames("Hello", AbiQualifiedName("", "Hello")) + checkNames("a.b.c", AbiQualifiedName("a.b", "c")) + checkNames("a\$b\$c", AbiQualifiedName("", "a.b.c")) + checkNames("p.a\$b\$c", AbiQualifiedName("p", "a.b.c")) + checkNames("org.example.Outer\$Inner\$\$serializer", + AbiQualifiedName("org.example", "Outer.Inner.\$serializer")) + checkNames("a.b.e.s.c.MapStream\$Stream\$", + AbiQualifiedName("a.b.e.s.c", "MapStream.Stream\$")) + } + + private fun checkNames(binaryClassName: String, qualifiedName: AbiQualifiedName) { + val converted = binaryClassName.toAbiQualifiedName()!! + assertEquals(qualifiedName.packageName, converted.packageName) + assertEquals(qualifiedName.relativeName, converted.relativeName) + } +} + +private fun AbiQualifiedName(packageName: String, className: String) = + AbiQualifiedName(AbiCompoundName(packageName), AbiCompoundName(className)) From a69f2041df60f916c4432055f0b65e62be7c13f5 Mon Sep 17 00:00:00 2001 From: Filipp Zhinkin Date: Thu, 7 Mar 2024 09:41:16 +0100 Subject: [PATCH 34/53] Get rid of dump setting --- ...otlinKlibExtractSupportedTargetsAbiTask.kt | 1 - ...linKlibInferAbiForUnsupportedTargetTask.kt | 3 +- src/main/kotlin/KotlinKlibMergeAbiTask.kt | 1 - src/main/kotlin/api/klib/KlibTarget.kt | 8 +++ src/main/kotlin/klib/KlibAbiDumpFileMerger.kt | 64 +++++++------------ src/test/kotlin/tests/KlibAbiMergingTest.kt | 45 ++----------- src/test/kotlin/tests/KlibTargetNameTest.kt | 52 +++++++++++++++ .../merge/diverging/linuxArm64.extracted.api | 6 +- src/test/resources/merge/guess/guessed.api | 4 +- .../resources/merge/webTargets/js.ext.abi | 3 +- .../{wasm.ext.abi => wasmJs.ext.abi} | 3 +- .../merge/webTargets/wasmWasi.ext.abi | 15 +++++ 12 files changed, 115 insertions(+), 90 deletions(-) create mode 100644 src/test/kotlin/tests/KlibTargetNameTest.kt rename src/test/resources/merge/webTargets/{wasm.ext.abi => wasmJs.ext.abi} (94%) create mode 100644 src/test/resources/merge/webTargets/wasmWasi.ext.abi diff --git a/src/main/kotlin/KotlinKlibExtractSupportedTargetsAbiTask.kt b/src/main/kotlin/KotlinKlibExtractSupportedTargetsAbiTask.kt index 2f6837e1..0a552162 100644 --- a/src/main/kotlin/KotlinKlibExtractSupportedTargetsAbiTask.kt +++ b/src/main/kotlin/KotlinKlibExtractSupportedTargetsAbiTask.kt @@ -5,7 +5,6 @@ package kotlinx.validation -import kotlinx.validation.klib.KlibAbiDumpFormat import kotlinx.validation.klib.KlibAbiDumpMerger import kotlinx.validation.api.klib.KlibTarget import org.gradle.api.DefaultTask diff --git a/src/main/kotlin/KotlinKlibInferAbiForUnsupportedTargetTask.kt b/src/main/kotlin/KotlinKlibInferAbiForUnsupportedTargetTask.kt index 3a3a43e2..4cace233 100644 --- a/src/main/kotlin/KotlinKlibInferAbiForUnsupportedTargetTask.kt +++ b/src/main/kotlin/KotlinKlibInferAbiForUnsupportedTargetTask.kt @@ -5,7 +5,6 @@ package kotlinx.validation -import kotlinx.validation.klib.KlibAbiDumpFormat import kotlinx.validation.klib.KlibAbiDumpMerger import kotlinx.validation.api.klib.KlibTarget import kotlinx.validation.klib.TargetHierarchy @@ -111,7 +110,7 @@ internal abstract class KotlinKlibInferAbiForUnsupportedTargetTask : DefaultTask commonDump.overrideTargets(setOf(unsupportedTarget)) outputFile.bufferedWriter().use { - commonDump.dump(it, KlibAbiDumpFormat(singleTargetDump = true)) + commonDump.dump(it) } logger.warn( diff --git a/src/main/kotlin/KotlinKlibMergeAbiTask.kt b/src/main/kotlin/KotlinKlibMergeAbiTask.kt index c98fcd52..3b7395c6 100644 --- a/src/main/kotlin/KotlinKlibMergeAbiTask.kt +++ b/src/main/kotlin/KotlinKlibMergeAbiTask.kt @@ -5,7 +5,6 @@ package kotlinx.validation -import kotlinx.validation.klib.KlibAbiDumpFormat import kotlinx.validation.klib.KlibAbiDumpMerger import org.gradle.api.DefaultTask import org.gradle.api.tasks.* diff --git a/src/main/kotlin/api/klib/KlibTarget.kt b/src/main/kotlin/api/klib/KlibTarget.kt index adf89ada..404b51f6 100644 --- a/src/main/kotlin/api/klib/KlibTarget.kt +++ b/src/main/kotlin/api/klib/KlibTarget.kt @@ -19,6 +19,14 @@ public class KlibTarget internal constructor( public val configurableName: String, public val targetName: String) { + init { + require(!configurableName.contains(".")) { + "Configurable name can't contain the '.' character: $configurableName" + } + require(!targetName.contains(".")) { + "Target name can't contain the '.' character: $targetName" + } + } public companion object { public fun parse(line: String): KlibTarget { require(line.isNotBlank()) { "Target name could not be blank." } diff --git a/src/main/kotlin/klib/KlibAbiDumpFileMerger.kt b/src/main/kotlin/klib/KlibAbiDumpFileMerger.kt index 6ae2298d..7aa49b51 100644 --- a/src/main/kotlin/klib/KlibAbiDumpFileMerger.kt +++ b/src/main/kotlin/klib/KlibAbiDumpFileMerger.kt @@ -69,15 +69,6 @@ private fun parseBcvTargetsLine(line: String): Set { .toSet() } -internal data class KlibAbiDumpFormat( - /** - * Reconstruct a dump as it would look like after dumping a klib. - * This flag is mostly intended for dumping an inferred ABI and - * requires a [KlibAbiDumpMerger] containing only a single target. - */ - val singleTargetDump: Boolean = false -) - private class KlibAbiDumpHeader( val content: List, val underlyingTarget: String? @@ -112,8 +103,14 @@ internal class KlibAbiDumpMerger { public fun addIndividualDump(customTargetName: String, file: File) { require(file.exists()) { "File does not exist: $file" } + Files.lines(file.toPath()).use { - mergeFile(false, customTargetName, LinesProvider(it.iterator())) + val lp = LinesProvider(it.iterator()) + if (lp.peek()?.startsWith(MERGED_DUMP_FILE_HEADER) == true) { + mergeFile(true, null, lp) + } else { + mergeFile(false, customTargetName, lp) + } } } @@ -125,7 +122,7 @@ internal class KlibAbiDumpMerger { } private fun mergeFile(isMergedFile: Boolean, targetName: String?, lines: LinesProvider) { - if (isMergedFile) check(this.targetsMut.isEmpty()) { "Merged dump could only be loaded once." } + //if (isMergedFile) check(this.targetsMut.isEmpty()) { "Merged dump could only be loaded once." } lines.checkFileFormat(isMergedFile) val aliases = mutableMapOf>() @@ -145,10 +142,15 @@ internal class KlibAbiDumpMerger { } bcvTargets.add(header.extractTarget(targetName ?: header.underlyingTarget!!)) } - if (isMergedFile || this.targetsMut.isEmpty()) { + // TODO + if (this.targetsMut.isEmpty()) { headerContent.addAll(header.content) } else if (headerContent != header.content) { - throw IllegalStateException("File header doesn't match the header of other files") + // TODO + throw IllegalStateException( + "File header doesn't match the header of other files\n" + + headerContent.toString() + "\n\n\n" + header.content.toString() + ) } this.targetsMut.addAll(bcvTargets) topLevelDeclaration.targets.addAll(bcvTargets) @@ -230,7 +232,7 @@ internal class KlibAbiDumpMerger { trimmedLine.length - 1 ) .split(",") - .map { KlibTarget(it.trim()) } + .map { KlibTarget.parse(it.trim()) } .toSet() aliases[name] = targets } @@ -337,32 +339,15 @@ internal class KlibAbiDumpMerger { } } - fun dump(appendable: Appendable, dumpFormat: KlibAbiDumpFormat = KlibAbiDumpFormat()) { + fun dump(appendable: Appendable) { val formatter = createFormatter() - if (dumpFormat.singleTargetDump) { - require(targets.size == 1) { - "Can skip target inclusion only if the dump contains a single target, but it contains: $targets" - } - } else { - appendable.append(MERGED_DUMP_FILE_HEADER).append('\n') - appendable.append(formatter.formatHeader(targets)).append('\n') - } + appendable.append(MERGED_DUMP_FILE_HEADER).append('\n') + appendable.append(formatter.formatHeader(targets)).append('\n') headerContent.forEach { appendable.append(it).append('\n') } - if (dumpFormat.singleTargetDump) { - // Reconstruct a manifest partially for a single target dump, - // so the dump could be later merged correctly. - val canonicalName = targets.single().targetName - val platform = platformByCanonicalName(canonicalName) - appendable.append(PLATFORM_PREFIX).append(platform).append('\n') - if (platform == "NATIVE") { - val nativeTarget = konanTargetNameReverseMapping[canonicalName]!! - appendable.append(NATIVE_TARGETS_PREFIX).append(nativeTarget).append('\n') - } - } topLevelDeclaration.children.values.sortedWith(DeclarationsComparator).forEach { - it.dump(appendable, targetsMut, dumpFormat, formatter) + it.dump(appendable, targetsMut, formatter) } } @@ -472,11 +457,8 @@ internal class DeclarationContainer(val text: String, val parent: DeclarationCon return child } - fun dump( - appendable: Appendable, allTargets: Set, - dumpFormat: KlibAbiDumpFormat, formatter: KLibsTargetsFormatter - ) { - if (targets != allTargets && !dumpFormat.singleTargetDump) { + fun dump(appendable: Appendable, allTargets: Set, formatter: KLibsTargetsFormatter) { + if (targets != allTargets/* && !dumpFormat.singleTargetDump*/) { // Use the same indentation for target list as for the declaration itself appendable.append(" ".repeat(text.depth() * INDENT_WIDTH)) .append(formatter.formatDeclarationTargets(targets)) @@ -484,7 +466,7 @@ internal class DeclarationContainer(val text: String, val parent: DeclarationCon } appendable.append(text).append('\n') children.values.sortedWith(DeclarationsComparator).forEach { - it.dump(appendable, this.targets, dumpFormat, formatter) + it.dump(appendable, this.targets, formatter) } if (delimiter != null) { appendable.append(delimiter).append('\n') diff --git a/src/test/kotlin/tests/KlibAbiMergingTest.kt b/src/test/kotlin/tests/KlibAbiMergingTest.kt index 2f8d7aa2..1fe14980 100644 --- a/src/test/kotlin/tests/KlibAbiMergingTest.kt +++ b/src/test/kotlin/tests/KlibAbiMergingTest.kt @@ -5,7 +5,6 @@ package tests -import kotlinx.validation.klib.KlibAbiDumpFormat import kotlinx.validation.klib.KlibAbiDumpMerger import kotlinx.validation.api.klib.KlibTarget import org.junit.Rule @@ -37,44 +36,14 @@ class KlibAbiMergingTest { return res.bufferedReader().lineSequence() } - private fun dumpToFile(klib: KlibAbiDumpMerger, singleTargetDump: Boolean = false): File { + private fun dumpToFile(klib: KlibAbiDumpMerger): File { val file = tempDir.newFile() FileWriter(file).use { - klib.dump(it, KlibAbiDumpFormat( - singleTargetDump = singleTargetDump - )) + klib.dump(it) } return file } - @Test - fun testTargetNames() { - assertEquals("a.b", KlibTarget("b", "a").toString()) - assertEquals("a", KlibTarget("a").toString()) - assertEquals("a", KlibTarget("a", "a").toString()) - - assertFailsWith { KlibTarget.parse("") } - assertFailsWith { KlibTarget.parse(" ") } - assertFailsWith { KlibTarget.parse("a.b.c") } - assertFailsWith { KlibTarget.parse("a.") } - assertFailsWith { KlibTarget.parse(".a") } - - KlibTarget.parse("a.b").also { - assertEquals("b", it.configurableName) - assertEquals("a", it.targetName) - } - - KlibTarget.parse("a.a").also { - assertEquals("a", it.configurableName) - assertEquals("a", it.targetName) - } - - KlibTarget.parse("a").also { - assertEquals("a", it.configurableName) - assertEquals("a", it.targetName) - } - } - @Test fun identicalDumpFiles() { val klib = KlibAbiDumpMerger() @@ -232,7 +201,7 @@ class KlibAbiMergingTest { commonAbi.mergeTargetSpecific(klib) commonAbi.overrideTargets(setOf(KlibTarget("linuxArm64"))) - val guessedAbiDump = dumpToFile(commonAbi, true) + val guessedAbiDump = dumpToFile(commonAbi) assertContentEquals( lines("/merge/guess/guessed.api"), Files.readAllLines(guessedAbiDump.toPath()).asSequence() @@ -320,7 +289,7 @@ class KlibAbiMergingTest { } val targets = lib.targets.filter { it.targetName != "linuxArm64" } targets.forEach { lib.remove(it) } - val extracted = dumpToFile(lib, singleTargetDump = true) + val extracted = dumpToFile(lib) assertContentEquals( lines("/merge/diverging/linuxArm64.extracted.api"), Files.readAllLines(extracted.toPath()).asSequence() @@ -335,7 +304,7 @@ class KlibAbiMergingTest { val lib = KlibAbiDumpMerger().apply { loadMergedDump(file(mergedPath)) } val targets = lib.targets targets.filter { it.configurableName != targetName }.forEach { lib.remove(it) } - val dump = dumpToFile(lib, singleTargetDump = true) + val dump = dumpToFile(lib) assertContentEquals( lines(expectedFile), Files.readAllLines(dump.toPath()).asSequence(), @@ -344,7 +313,7 @@ class KlibAbiMergingTest { } checkExtracted("js", "/merge/webTargets/js.ext.abi") - checkExtracted("wasmWasi", "/merge/webTargets/wasm.ext.abi") - checkExtracted("wasmJs", "/merge/webTargets/wasm.ext.abi") + checkExtracted("wasmWasi", "/merge/webTargets/wasmWasi.ext.abi") + checkExtracted("wasmJs", "/merge/webTargets/wasmJs.ext.abi") } } diff --git a/src/test/kotlin/tests/KlibTargetNameTest.kt b/src/test/kotlin/tests/KlibTargetNameTest.kt new file mode 100644 index 00000000..ced22847 --- /dev/null +++ b/src/test/kotlin/tests/KlibTargetNameTest.kt @@ -0,0 +1,52 @@ +/* + * Copyright 2016-2024 JetBrains s.r.o. + * Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file. + */ + +package tests + +import kotlinx.validation.api.klib.KlibTarget +import org.junit.Test +import kotlin.test.assertEquals +import kotlin.test.assertFails +import kotlin.test.assertFailsWith + +class KlibTargetNameTest { + @Test + fun parse() { + assertEquals("a.b", KlibTarget("b", "a").toString()) + assertEquals("a", KlibTarget("a").toString()) + assertEquals("a", KlibTarget("a", "a").toString()) + + assertFailsWith { KlibTarget.parse("") } + assertFailsWith { KlibTarget.parse(" ") } + assertFailsWith { KlibTarget.parse("a.b.c") } + assertFailsWith { KlibTarget.parse("a.") } + assertFailsWith { KlibTarget.parse(".a") } + + KlibTarget.parse("a.b").also { + assertEquals("b", it.configurableName) + assertEquals("a", it.targetName) + } + + KlibTarget.parse("a.a").also { + assertEquals("a", it.configurableName) + assertEquals("a", it.targetName) + } + + KlibTarget.parse("a").also { + assertEquals("a", it.configurableName) + assertEquals("a", it.targetName) + } + } + + @Test + fun validate() { + assertFailsWith { + KlibTarget("a.b", "c") + } + assertFailsWith { + KlibTarget("a", "b.c") + } + } +} diff --git a/src/test/resources/merge/diverging/linuxArm64.extracted.api b/src/test/resources/merge/diverging/linuxArm64.extracted.api index 7174bad5..965b7849 100644 --- a/src/test/resources/merge/diverging/linuxArm64.extracted.api +++ b/src/test/resources/merge/diverging/linuxArm64.extracted.api @@ -1,17 +1,17 @@ +// Merged KLib ABI Dump +// Targets: [linuxArm64.linux] // Rendering settings: // - Signature version: 2 // - Show manifest properties: false // - Show declarations: true // Library unique name: -// Platform: NATIVE -// Native targets: linux_arm64 final class org.example/ShardedClass { // org.example/ShardedClass|null[0] constructor (kotlin/Int) // org.example/ShardedClass.|(kotlin.Int){}[0] final fun add(kotlin/Int): kotlin/Int // org.example/ShardedClass.add|add(kotlin.Int){}[0] final val value // org.example/ShardedClass.value|{}value[0] final fun (): kotlin/Int // org.example/ShardedClass.value.|(){}[0] } -final fun org.example/ShardedClass(kotlin/Int, kotlin/Float, kotlin/Long): org.example/ShardedClass // org.example/ShardedClass|ShardedClass(kotlin.Int;kotlin.Float;kotlin.Long){}[0] final fun (kotlin/Long).org.example/add(kotlin/Long): kotlin/Long // org.example/add|add@kotlin.Long(kotlin.Long){}[0] +final fun org.example/ShardedClass(kotlin/Int, kotlin/Float, kotlin/Long): org.example/ShardedClass // org.example/ShardedClass|ShardedClass(kotlin.Int;kotlin.Float;kotlin.Long){}[0] final fun org.example/add(kotlin/Long, kotlin/Long): kotlin/Long // org.example/add|add(kotlin.Long;kotlin.Long){}[0] diff --git a/src/test/resources/merge/guess/guessed.api b/src/test/resources/merge/guess/guessed.api index 4e1e5f6f..f723898a 100644 --- a/src/test/resources/merge/guess/guessed.api +++ b/src/test/resources/merge/guess/guessed.api @@ -1,11 +1,11 @@ +// Merged KLib ABI Dump +// Targets: [linuxArm64] // Rendering settings: // - Signature version: 2 // - Show manifest properties: false // - Show declarations: true // Library unique name: -// Platform: NATIVE -// Native targets: linux_arm64 final class org.example/ShardedClass { // org.example/ShardedClass|null[0] constructor (kotlin/Int) // org.example/ShardedClass.|(kotlin.Int){}[0] final fun add(kotlin/Int): kotlin/Int // org.example/ShardedClass.add|add(kotlin.Int){}[0] diff --git a/src/test/resources/merge/webTargets/js.ext.abi b/src/test/resources/merge/webTargets/js.ext.abi index 6cbca640..e3b46200 100644 --- a/src/test/resources/merge/webTargets/js.ext.abi +++ b/src/test/resources/merge/webTargets/js.ext.abi @@ -1,10 +1,11 @@ +// Merged KLib ABI Dump +// Targets: [js] // Rendering settings: // - Signature version: 2 // - Show manifest properties: false // - Show declarations: true // Library unique name: -// Platform: JS final class org.example/ShardedClass { // org.example/ShardedClass|null[0] constructor (kotlin/Int) // org.example/ShardedClass.|(kotlin.Int){}[0] final fun add(kotlin/Int): kotlin/Int // org.example/ShardedClass.add|add(kotlin.Int){}[0] diff --git a/src/test/resources/merge/webTargets/wasm.ext.abi b/src/test/resources/merge/webTargets/wasmJs.ext.abi similarity index 94% rename from src/test/resources/merge/webTargets/wasm.ext.abi rename to src/test/resources/merge/webTargets/wasmJs.ext.abi index 4de57b09..3959e180 100644 --- a/src/test/resources/merge/webTargets/wasm.ext.abi +++ b/src/test/resources/merge/webTargets/wasmJs.ext.abi @@ -1,10 +1,11 @@ +// Merged KLib ABI Dump +// Targets: [wasmJs] // Rendering settings: // - Signature version: 2 // - Show manifest properties: false // - Show declarations: true // Library unique name: -// Platform: WASM final class org.example/ShardedClass { // org.example/ShardedClass|null[0] constructor (kotlin/Int) // org.example/ShardedClass.|(kotlin.Int){}[0] final fun add(kotlin/Int): kotlin/Int // org.example/ShardedClass.add|add(kotlin.Int){}[0] diff --git a/src/test/resources/merge/webTargets/wasmWasi.ext.abi b/src/test/resources/merge/webTargets/wasmWasi.ext.abi new file mode 100644 index 00000000..94c81355 --- /dev/null +++ b/src/test/resources/merge/webTargets/wasmWasi.ext.abi @@ -0,0 +1,15 @@ +// Merged KLib ABI Dump +// Targets: [wasmWasi] +// Rendering settings: +// - Signature version: 2 +// - Show manifest properties: false +// - Show declarations: true + +// Library unique name: +final class org.example/ShardedClass { // org.example/ShardedClass|null[0] + constructor (kotlin/Int) // org.example/ShardedClass.|(kotlin.Int){}[0] + final fun add(kotlin/Int): kotlin/Int // org.example/ShardedClass.add|add(kotlin.Int){}[0] + final val value // org.example/ShardedClass.value|{}value[0] + final fun (): kotlin/Int // org.example/ShardedClass.value.|(){}[0] +} +final fun org.example/ShardedClass(kotlin/Int, kotlin/Float, kotlin/Long): org.example/ShardedClass // org.example/ShardedClass|ShardedClass(kotlin.Int;kotlin.Float;kotlin.Long){}[0] From a21f4a9c4c72637a5102e1c2ae10a63e9ea54076 Mon Sep 17 00:00:00 2001 From: Filipp Zhinkin Date: Thu, 7 Mar 2024 11:06:19 +0100 Subject: [PATCH 35/53] Left a single method to load all types of dumps, supported multi-target dumps --- ...otlinKlibExtractSupportedTargetsAbiTask.kt | 2 +- ...linKlibInferAbiForUnsupportedTargetTask.kt | 4 +- src/main/kotlin/KotlinKlibMergeAbiTask.kt | 2 +- src/main/kotlin/klib/KlibAbiDumpFileMerger.kt | 144 +++++++++--------- src/test/kotlin/tests/KlibAbiMergingTest.kt | 112 ++++++++------ .../diverging/{tvOsX64.api => tvosX64.api} | 0 .../resources/merge/header-mismatch/v1.abi | 6 +- .../resources/merge/header-mismatch/v2.abi | 4 + .../resources/merge/stdlib_native_common.abi | 17 +++ 9 files changed, 168 insertions(+), 123 deletions(-) rename src/test/resources/merge/diverging/{tvOsX64.api => tvosX64.api} (100%) create mode 100644 src/test/resources/merge/stdlib_native_common.abi diff --git a/src/main/kotlin/KotlinKlibExtractSupportedTargetsAbiTask.kt b/src/main/kotlin/KotlinKlibExtractSupportedTargetsAbiTask.kt index 0a552162..8a97eefc 100644 --- a/src/main/kotlin/KotlinKlibExtractSupportedTargetsAbiTask.kt +++ b/src/main/kotlin/KotlinKlibExtractSupportedTargetsAbiTask.kt @@ -48,7 +48,7 @@ internal abstract class KotlinKlibExtractSupportedTargetsAbiTask : DefaultTask() if (inputAbiFile.length() == 0L) { error("Project ABI file $inputAbiFile is empty.") } - val dump = KlibAbiDumpMerger().apply { loadMergedDump(inputAbiFile) } + val dump = KlibAbiDumpMerger().apply { load(inputAbiFile) } val enabledTargets = supportedTargets.get().map { KlibTarget.parse(it).targetName } val targetsToRemove = dump.targets.filter { it.targetName !in enabledTargets } if (targetsToRemove.isNotEmpty() && strictValidation) { diff --git a/src/main/kotlin/KotlinKlibInferAbiForUnsupportedTargetTask.kt b/src/main/kotlin/KotlinKlibInferAbiForUnsupportedTargetTask.kt index 4cace233..bb3b2e2a 100644 --- a/src/main/kotlin/KotlinKlibInferAbiForUnsupportedTargetTask.kt +++ b/src/main/kotlin/KotlinKlibInferAbiForUnsupportedTargetTask.kt @@ -86,7 +86,7 @@ internal abstract class KotlinKlibInferAbiForUnsupportedTargetTask : DefaultTask // a common ABI that should be shared by the unsupported target as well val commonDump = KlibAbiDumpMerger() for (target in matchingTargets) { - commonDump.addIndividualDump(target.configurableName, target2outFile[target]!!) + commonDump.load(target2outFile[target]!!, target.configurableName) } commonDump.retainCommonAbi() @@ -95,7 +95,7 @@ internal abstract class KotlinKlibInferAbiForUnsupportedTargetTask : DefaultTask val image = KlibAbiDumpMerger() if (inputImageFile.exists()) { if (inputImageFile.length() > 0L) { - image.loadMergedDump(inputImageFile) + image.load(inputImageFile) image.retainTargetSpecificAbi(unsupportedTarget) // merge common ABI with target-specific ABI commonDump.mergeTargetSpecific(image) diff --git a/src/main/kotlin/KotlinKlibMergeAbiTask.kt b/src/main/kotlin/KotlinKlibMergeAbiTask.kt index 3b7395c6..26d99946 100644 --- a/src/main/kotlin/KotlinKlibMergeAbiTask.kt +++ b/src/main/kotlin/KotlinKlibMergeAbiTask.kt @@ -52,7 +52,7 @@ internal abstract class KotlinKlibMergeAbiTask : DefaultTask() { internal fun merge() { val builder = KlibAbiDumpMerger() targets.forEach { targetName -> - builder.addIndividualDump(targetName, targetToFile[targetName]!!.resolve(dumpFileName)) + builder.load(targetToFile[targetName]!!.resolve(dumpFileName), targetName) } mergedFile.bufferedWriter().use { builder.dump(it) } } diff --git a/src/main/kotlin/klib/KlibAbiDumpFileMerger.kt b/src/main/kotlin/klib/KlibAbiDumpFileMerger.kt index 7aa49b51..4993d28e 100644 --- a/src/main/kotlin/klib/KlibAbiDumpFileMerger.kt +++ b/src/main/kotlin/klib/KlibAbiDumpFileMerger.kt @@ -71,14 +71,9 @@ private fun parseBcvTargetsLine(line: String): Set { private class KlibAbiDumpHeader( val content: List, - val underlyingTarget: String? + val underlyingTargets: Set ) { - fun extractTarget(targetName: String): KlibTarget { - if (underlyingTarget == "wasm") { - return KlibTarget(targetName) - } - return KlibTarget(targetName, underlyingTarget ?: targetName) - } + constructor(content: List, underlyingTarget: KlibTarget) : this(content, setOf(underlyingTarget)) } /** @@ -94,59 +89,36 @@ internal class KlibAbiDumpMerger { */ public val targets: Set = targetsMut - public fun loadMergedDump(file: File) { - require(file.exists()) { "File does not exist: $file" } - Files.lines(file.toPath()).use { - mergeFile(true, null, LinesProvider(it.iterator())) - } - } - - public fun addIndividualDump(customTargetName: String, file: File) { - require(file.exists()) { "File does not exist: $file" } - - Files.lines(file.toPath()).use { - val lp = LinesProvider(it.iterator()) - if (lp.peek()?.startsWith(MERGED_DUMP_FILE_HEADER) == true) { - mergeFile(true, null, lp) - } else { - mergeFile(false, customTargetName, lp) - } - } - } - - public fun addIndividualDump(file: File) { + public fun load(file: File, targetName: String? = null) { require(file.exists()) { "File does not exist: $file" } Files.lines(file.toPath()).use { - mergeFile(false, null, LinesProvider(it.iterator())) + mergeFile(targetName, LinesProvider(it.iterator())) } } - private fun mergeFile(isMergedFile: Boolean, targetName: String?, lines: LinesProvider) { - //if (isMergedFile) check(this.targetsMut.isEmpty()) { "Merged dump could only be loaded once." } - lines.checkFileFormat(isMergedFile) + private fun mergeFile(targetName: String?, lines: LinesProvider) { + val isMergedFile = lines.determineFileType() val aliases = mutableMapOf>() val bcvTargets = mutableSetOf() if (isMergedFile) { + lines.next() // skip the heading line bcvTargets.addAll(lines.parseTargets()) + check(bcvTargets.size == 1 || targetName == null) { + "Can't use an explicit target name with a multi-target dump. " + + "targetName: $targetName, dump targets: $bcvTargets" + } aliases.putAll(lines.parseAliases()) } - val header = lines.parseFileHeader() - if (!isMergedFile) { - if (targetName == null && header.underlyingTarget == "wasm") { - throw IllegalStateException( - "Currently, there is no way to distinguish dumps generated for " + - "different Wasm targets (wasmJs and wasmWasi), " + - "please specify the actual target name explicitly" - ) - } - bcvTargets.add(header.extractTarget(targetName ?: header.underlyingTarget!!)) + val header = lines.parseFileHeader(isMergedFile, targetName) + bcvTargets.addAll(header.underlyingTargets) + bcvTargets.intersect(targets).also { + check(it.isEmpty()) { "This dump and a file to merge share some targets: $it" } } - // TODO + if (this.targetsMut.isEmpty()) { headerContent.addAll(header.content) } else if (headerContent != header.content) { - // TODO throw IllegalStateException( "File header doesn't match the header of other files\n" + headerContent.toString() + "\n\n\n" + header.content.toString() @@ -239,7 +211,10 @@ internal class KlibAbiDumpMerger { return aliases } - private fun LinesProvider.parseFileHeader(): KlibAbiDumpHeader { + private fun LinesProvider.parseFileHeader( + isMergedFile: Boolean, + configurableTargetName: String? + ): KlibAbiDumpHeader { val header = mutableListOf() var targets: String? = null var platform: String? = null @@ -261,6 +236,8 @@ internal class KlibAbiDumpMerger { val next = peek()!! if (!next.startsWith(COMMENT_PREFIX)) break next() + // There's no manifest in merged files + check(!isMergedFile) { "Unexpected header line: $next" } when { next.startsWith(PLATFORM_PREFIX) -> { platform = next.split(": ")[1].trim() @@ -271,40 +248,67 @@ internal class KlibAbiDumpMerger { } } } - if (platform == null) { - return KlibAbiDumpHeader(header, null) + if (isMergedFile) { + return KlibAbiDumpHeader(header, emptySet()) } - // TODO - if (targets?.contains(",") == true) throw IllegalStateException("Multi-target klibs are not supported.") - val underlyingTarget = when (platform) { - "NATIVE" -> konanTargetNameMapping[targets] - ?: throw IllegalStateException("The manifest is missing targets for native platform") - else -> platform.toLowerCase() - } - return KlibAbiDumpHeader(header, underlyingTarget) + // transform a combination of platform name and targets list to a set of KlibTargets + return KlibAbiDumpHeader(header, extractTargets(platform, targets, configurableTargetName)) } - private fun LinesProvider.checkFileFormat(isMergedFile: Boolean) { - val headerLine = if (isMergedFile) { - next() - } else { - peek()!! - } - val expectedHeader = if (isMergedFile) { - MERGED_DUMP_FILE_HEADER - } else { - REGULAR_DUMP_FILE_HEADER + private fun extractTargets( + platformString: String?, + targetsString: String?, + configurableTargetName: String? + ): Set { + check(platformString != null) { + "The dump does not contain platform name. Please make sure that the manifest was included in the dump" } - check(headerLine == expectedHeader) { - val headerStart = if (headerLine.length > 32) { - headerLine.substring(0, 32) + "..." + if (platformString == "WASM") { + // Currently, there's no way to distinguish Wasm targets without explicitly specifying a target name + check(configurableTargetName != null) { "targetName has to be specified for a Wasm target" } + return setOf(KlibTarget(configurableTargetName)) + } + if (platformString != "NATIVE") { + return if (configurableTargetName == null) { + setOf(KlibTarget(platformString.toLowerCase())) } else { - headerLine + setOf(KlibTarget(configurableTargetName, platformString.toLowerCase())) } - "Expected a file staring with \"$expectedHeader\", but the file stats with \"$headerStart\"" } + + check(targetsString != null) { "Dump for a native platform missing targets list." } + + val targetsList = targetsString.split(TARGETS_DELIMITER).map { + konanTargetNameMapping[it.trim()] ?: throw IllegalStateException("Unknown native target: $it") + } + check(targetsList.size == 1 || configurableTargetName == null) { + "Can't use configurableTargetName ($configurableTargetName) for a multi-target dump: $targetsList" + } + if (targetsList.size == 1 && configurableTargetName != null) { + return setOf(KlibTarget(configurableTargetName, targetsList.first())) + } + return targetsList.asSequence().map { KlibTarget(it) }.toSet() + } + + private fun LinesProvider.determineFileType(): Boolean { + val headerLine = peek() ?: throw IllegalStateException("File is empty") + if (headerLine.trimEnd() == MERGED_DUMP_FILE_HEADER) { + return true + } + if (headerLine.trimEnd() == REGULAR_DUMP_FILE_HEADER) { + return false + } + val headerStart = if (headerLine.length > 32) { + headerLine.substring(0, 32) + "..." + } else { + headerLine + } + throw IllegalStateException( + "Expected a file staring with \"$REGULAR_DUMP_FILE_HEADER\" " + + "or \"$MERGED_DUMP_FILE_HEADER\", but the file stats with \"$headerStart\"" + ) } private fun LinesProvider.parseDeclaration( diff --git a/src/test/kotlin/tests/KlibAbiMergingTest.kt b/src/test/kotlin/tests/KlibAbiMergingTest.kt index 1fe14980..d1594f2f 100644 --- a/src/test/kotlin/tests/KlibAbiMergingTest.kt +++ b/src/test/kotlin/tests/KlibAbiMergingTest.kt @@ -8,7 +8,6 @@ package tests import kotlinx.validation.klib.KlibAbiDumpMerger import kotlinx.validation.api.klib.KlibTarget import org.junit.Rule -import org.junit.Test import org.junit.rules.TemporaryFolder import java.io.File import java.io.FileWriter @@ -16,6 +15,7 @@ import java.nio.file.Files import java.util.UUID import kotlin.random.Random import kotlin.test.* +import kotlin.test.Test class KlibAbiMergingTest { @JvmField @@ -47,8 +47,8 @@ class KlibAbiMergingTest { @Test fun identicalDumpFiles() { val klib = KlibAbiDumpMerger() - klib.addIndividualDump(file("/merge/identical/dump_macos_arm64.abi")) - klib.addIndividualDump(file("/merge/identical/dump_linux_x64.abi")) + klib.load(file("/merge/identical/dump_macos_arm64.abi")) + klib.load(file("/merge/identical/dump_linux_x64.abi")) val merged = dumpToFile(klib) assertContentEquals( @@ -60,8 +60,8 @@ class KlibAbiMergingTest { @Test fun identicalDumpFilesWithAliases() { val klib = KlibAbiDumpMerger() - klib.addIndividualDump(file("/merge/identical/dump_macos_arm64.abi")) - klib.addIndividualDump(file("/merge/identical/dump_linux_x64.abi")) + klib.load(file("/merge/identical/dump_macos_arm64.abi")) + klib.load(file("/merge/identical/dump_linux_x64.abi")) val merged = dumpToFile(klib) // there are no groups other than "all", so no aliases will be added @@ -79,7 +79,7 @@ class KlibAbiMergingTest { val klib = KlibAbiDumpMerger() targets.shuffle(random) targets.forEach { - klib.addIndividualDump(file("/merge/diverging/$it.api")) + klib.load(file("/merge/diverging/$it.api")) } val merged = dumpToFile(klib) assertContentEquals( @@ -92,13 +92,13 @@ class KlibAbiMergingTest { @Test fun divergingDumpFilesWithAliases() { - val klib = KlibAbiDumpMerger() val random = Random(42) for (i in 0 until 10) { - val targets = mutableListOf("androidNativeArm64", "linuxArm64", "linuxX64", "tvOsX64") + val klib = KlibAbiDumpMerger() + val targets = mutableListOf("androidNativeArm64", "linuxArm64", "linuxX64", "tvosX64") targets.shuffle(random) targets.forEach { - klib.addIndividualDump(file("/merge/diverging/$it.api")) + klib.load(file("/merge/diverging/$it.api")) } val merged = dumpToFile(klib) assertContentEquals( @@ -111,28 +111,22 @@ class KlibAbiMergingTest { @Test fun mergeDumpsWithDivergedHeaders() { val klib = KlibAbiDumpMerger() - klib.addIndividualDump( - "linuxArm64", - file("/merge/header-mismatch/v1.abi") - ) + klib.load(file("/merge/header-mismatch/v1.abi"), "linuxArm64") assertFailsWith { - klib.addIndividualDump( - "linuxX64", - file("/merge/header-mismatch/v2.abi") - ) + klib.load(file("/merge/header-mismatch/v2.abi"), "linuxX64") } } @Test fun overwriteAll() { val klib = KlibAbiDumpMerger() - klib.loadMergedDump(file("/merge/diverging/merged.abi")) + klib.load(file("/merge/diverging/merged.abi")) - val targets = listOf("androidNativeArm64", "linuxArm64", "linuxX64", "tvOsX64") + val targets = listOf("androidNativeArm64", "linuxArm64", "linuxX64", "tvosX64") targets.forEach { target -> klib.remove(KlibTarget(target)) - klib.addIndividualDump(file("/merge/diverging/$target.api")) + klib.load(file("/merge/diverging/$target.api")) } val merged = dumpToFile(klib) @@ -146,7 +140,7 @@ class KlibAbiMergingTest { @Test fun read() { val klib = KlibAbiDumpMerger() - klib.loadMergedDump(file("/merge/idempotent/bcv-klib-test.abi")) + klib.load(file("/merge/idempotent/bcv-klib-test.abi")) val written = dumpToFile(klib) assertContentEquals( @@ -158,7 +152,7 @@ class KlibAbiMergingTest { @Test fun readDeclarationWithNarrowerChildrenDeclarations() { val klib = KlibAbiDumpMerger() - klib.loadMergedDump(file("/merge/parseNarrowChildrenDecls/merged.abi")) + klib.load(file("/merge/parseNarrowChildrenDecls/merged.abi")) klib.remove(KlibTarget("linuxArm64")) val written1 = dumpToFile(klib) @@ -178,7 +172,7 @@ class KlibAbiMergingTest { @Test fun guessAbi() { val klib = KlibAbiDumpMerger() - klib.loadMergedDump(file("/merge/guess/merged.api")) + klib.load(file("/merge/guess/merged.api")) klib.retainTargetSpecificAbi(KlibTarget("linuxArm64")) val retainedLinuxAbiDump = dumpToFile(klib) @@ -188,7 +182,7 @@ class KlibAbiMergingTest { ) val commonAbi = KlibAbiDumpMerger() - commonAbi.loadMergedDump(file("/merge/guess/merged.api")) + commonAbi.load(file("/merge/guess/merged.api")) commonAbi.remove(KlibTarget("linuxArm64")) commonAbi.retainCommonAbi() @@ -211,44 +205,39 @@ class KlibAbiMergingTest { @Test fun loadInvalidFile() { assertFails { - KlibAbiDumpMerger().loadMergedDump(file("/merge/illegalFiles/emptyFile.txt")) + KlibAbiDumpMerger().load(file("/merge/illegalFiles/emptyFile.txt")) } assertFails { - KlibAbiDumpMerger().loadMergedDump(file("/merge/illegalFiles/nonDumpFile.txt")) + KlibAbiDumpMerger().load(file("/merge/illegalFiles/nonDumpFile.txt")) } assertFails { - // Not a merged dump - KlibAbiDumpMerger().loadMergedDump(file("/merge/diverging/linuxArm64.api")) + KlibAbiDumpMerger().load(file("/merge/illegalFiles/emptyFile.txt"), "linuxX64") } assertFails { - KlibAbiDumpMerger().addIndividualDump( - "linuxX64", file("/merge/illegalFiles/emptyFile.txt") - ) + KlibAbiDumpMerger().load(file("/merge/illegalFiles/nonDumpFile.txt"), "linuxX64") } assertFails { - KlibAbiDumpMerger().addIndividualDump( - "linuxX64", file("/merge/illegalFiles/nonDumpFile.txt") - ) + // Not a single-target dump + KlibAbiDumpMerger().load(file("/merge/diverging/merged.api"), "linuxX64") } assertFails { - // Not a single-target dump - KlibAbiDumpMerger().addIndividualDump( - "linuxX64", file("/merge/diverging/merged.api") - ) + KlibAbiDumpMerger().apply { + load(file("/merge/stdlib_native_common.abi"), "linuxArm64") + } } } @Test fun webTargets() { val klib = KlibAbiDumpMerger() - klib.addIndividualDump(file("/merge/webTargets/js.abi")) - klib.addIndividualDump("wasmWasi", file("/merge/webTargets/wasmWasi.abi")) - klib.addIndividualDump("wasmJs", file("/merge/webTargets/wasmJs.abi")) + klib.load(file("/merge/webTargets/js.abi")) + klib.load(file("/merge/webTargets/wasmWasi.abi"), "wasmWasi") + klib.load(file("/merge/webTargets/wasmJs.abi"), "wasmJs") val merged = dumpToFile(klib) @@ -262,17 +251,29 @@ class KlibAbiMergingTest { fun unqualifiedWasmTarget() { // currently, there's no way to distinguish wasmWasi from wasmJs assertFailsWith { - KlibAbiDumpMerger().addIndividualDump(file("/merge/webTargets/wasmWasi.abi")) + KlibAbiDumpMerger().load(file("/merge/webTargets/wasmWasi.abi")) + } + } + + @Test + fun intersectingTargets() { + val dump = KlibAbiDumpMerger().apply { + load(file("/merge/diverging/merged.abi")) + } + assertFailsWith { + dump.load(file("/merge/diverging/linuxArm64.api")) } + // but here, we're loading a dump for different target (configuredName changed) + dump.load(file("/merge/diverging/linuxArm64.api"), "custom") } @Test fun customTargetNames() { val lib = KlibAbiDumpMerger().apply { - addIndividualDump("android", file("/merge/diverging/androidNativeArm64.api")) - addIndividualDump("linux", file("/merge/diverging/linuxArm64.api")) - addIndividualDump(file("/merge/diverging/linuxX64.api")) - addIndividualDump(file("/merge/diverging/tvOsX64.api")) + load(file("/merge/diverging/androidNativeArm64.api"), "android") + load(file("/merge/diverging/linuxArm64.api"), "linux") + load(file("/merge/diverging/linuxX64.api")) + load(file("/merge/diverging/tvosX64.api")) } val dump = dumpToFile(lib) @@ -285,7 +286,7 @@ class KlibAbiMergingTest { @Test fun customTargetExtraction() { val lib = KlibAbiDumpMerger().apply { - loadMergedDump(file("/merge/diverging/merged_with_aliases_and_custom_names.abi")) + load(file("/merge/diverging/merged_with_aliases_and_custom_names.abi")) } val targets = lib.targets.filter { it.targetName != "linuxArm64" } targets.forEach { lib.remove(it) } @@ -301,7 +302,7 @@ class KlibAbiMergingTest { val mergedPath = "/merge/webTargets/merged.abi" fun checkExtracted(targetName: String, expectedFile: String) { - val lib = KlibAbiDumpMerger().apply { loadMergedDump(file(mergedPath)) } + val lib = KlibAbiDumpMerger().apply { load(file(mergedPath)) } val targets = lib.targets targets.filter { it.configurableName != targetName }.forEach { lib.remove(it) } val dump = dumpToFile(lib) @@ -316,4 +317,19 @@ class KlibAbiMergingTest { checkExtracted("wasmWasi", "/merge/webTargets/wasmWasi.ext.abi") checkExtracted("wasmJs", "/merge/webTargets/wasmJs.ext.abi") } + + @Test + fun loadMultiTargetDump() { + val lib = KlibAbiDumpMerger().apply { + load(file("/merge/stdlib_native_common.abi")) + } + val expectedTargetNames = listOf( + "androidNativeArm32", "androidNativeArm64", "androidNativeX64", "androidNativeX86", + "iosArm64", "iosSimulatorArm64", "iosX64", "linuxArm32Hfp", "linuxArm64", "linuxX64", + "macosArm64", "macosX64", "mingwX64", "tvosArm64", "tvosSimulatorArm64", "tvosX64", + "watchosArm32", "watchosArm64", "watchosDeviceArm64", "watchosSimulatorArm64", "watchosX64" + ) + val expectedTargets = expectedTargetNames.asSequence().map(KlibTarget::parse).toSet() + assertEquals(expectedTargets, lib.targets) + } } diff --git a/src/test/resources/merge/diverging/tvOsX64.api b/src/test/resources/merge/diverging/tvosX64.api similarity index 100% rename from src/test/resources/merge/diverging/tvOsX64.api rename to src/test/resources/merge/diverging/tvosX64.api diff --git a/src/test/resources/merge/header-mismatch/v1.abi b/src/test/resources/merge/header-mismatch/v1.abi index cbc8741a..34fdd006 100644 --- a/src/test/resources/merge/header-mismatch/v1.abi +++ b/src/test/resources/merge/header-mismatch/v1.abi @@ -1,9 +1,13 @@ // Rendering settings: // - Signature version: 1 -// - Show manifest properties: false +// - Show manifest properties: true // - Show declarations: true // Library unique name: +// Platform: NATIVE +// Native targets: linux_arm64 +// Compiler version: 1.9.22 +// ABI version: 1.8.0 final class org.example/ShardedClass { // org.example/ShardedClass|null[0] final val value // org.example/ShardedClass.value|1987073854177347439[0] final fun (): kotlin/Int // org.example/ShardedClass.value.|3260093555963109437[0] diff --git a/src/test/resources/merge/header-mismatch/v2.abi b/src/test/resources/merge/header-mismatch/v2.abi index 086d9048..ca9ca7cf 100644 --- a/src/test/resources/merge/header-mismatch/v2.abi +++ b/src/test/resources/merge/header-mismatch/v2.abi @@ -4,6 +4,10 @@ // - Show declarations: true // Library unique name: +// Platform: NATIVE +// Native targets: linux_x64 +// Compiler version: 1.9.22 +// ABI version: 1.8.0 final class org.example/ShardedClass { // org.example/ShardedClass|null[0] final val value // org.example/ShardedClass.value|{}value[0] final fun (): kotlin/Int // org.example/ShardedClass.value.|(){}[0] diff --git a/src/test/resources/merge/stdlib_native_common.abi b/src/test/resources/merge/stdlib_native_common.abi new file mode 100644 index 00000000..a475212a --- /dev/null +++ b/src/test/resources/merge/stdlib_native_common.abi @@ -0,0 +1,17 @@ +// Rendering settings: +// - Signature version: 2 +// - Show manifest properties: true +// - Show declarations: true + +// Library unique name: +// Platform: NATIVE +// Native targets: android_arm32, android_arm64, android_x64, android_x86, ios_arm64, ios_simulator_arm64, ios_x64, linux_arm32_hfp, linux_arm64, linux_x64, macos_arm64, macos_x64, mingw_x64, tvos_arm64, tvos_simulator_arm64, tvos_x64, watchos_arm32, watchos_arm64, watchos_device_arm64, watchos_simulator_arm64, watchos_x64 +// Compiler version: 2.0.255-SNAPSHOT +// ABI version: 1.8.0 +abstract interface kotlin/Annotation // kotlin/Annotation|null[0] +open class kotlin/Any { // kotlin/Any|null[0] + constructor () // kotlin/Any.|(){}[0] + open fun equals(kotlin/Any?): kotlin/Boolean // kotlin/Any.equals|equals(kotlin.Any?){}[0] + open fun hashCode(): kotlin/Int // kotlin/Any.hashCode|hashCode(){}[0] + open fun toString(): kotlin/String // kotlin/Any.toString|toString(){}[0] +} From f51611dd7025f52509a586c092d5ee32f9526d21 Mon Sep 17 00:00:00 2001 From: Filipp Zhinkin Date: Thu, 7 Mar 2024 12:11:36 +0100 Subject: [PATCH 36/53] Renaming and doc update --- docs/design/KLibSupport.md | 10 +++++----- ...ibVerificationTests.kt => KlibVerificationTests.kt} | 4 ++-- .../classes/AnotherBuildConfig.klib.clash.dump | 2 +- .../classes/AnotherBuildConfig.klib.custom.dump | 2 +- .../examples/classes/AnotherBuildConfig.klib.dump | 2 +- .../classes/AnotherBuildConfig.klib.renamedTarget.dump | 2 +- .../examples/classes/AnotherBuildConfig.klib.web.dump | 2 +- .../classes/AnotherBuildConfigLinux.klib.grouping.dump | 2 +- .../AnotherBuildConfigLinuxArm64Extra.klib.dump | 2 +- .../classes/AnotherBuildConfigModified.klib.dump | 2 +- .../examples/classes/ClassWithPublicMarkers.klib.dump | 2 +- .../resources/examples/classes/Empty.klib.dump | 2 +- .../examples/classes/HiddenDeclarations.klib.dump | 2 +- .../resources/examples/classes/Properties.klib.dump | 2 +- .../resources/examples/classes/Subclasses.klib.dump | 2 +- .../classes/TopLevelDeclarations.klib.all.dump | 2 +- .../examples/classes/TopLevelDeclarations.klib.dump | 2 +- .../classes/TopLevelDeclarations.klib.unsup.dump | 2 +- .../examples/classes/TopLevelDeclarations.klib.v1.dump | 2 +- .../TopLevelDeclarations.klib.with.guessed.linux.dump | 2 +- .../classes/TopLevelDeclarations.klib.with.linux.dump | 2 +- src/main/kotlin/klib/KlibAbiDumpFileMerger.kt | 2 +- .../resources/merge/diverging/linuxArm64.extracted.api | 2 +- src/test/resources/merge/diverging/merged.abi | 2 +- .../resources/merge/diverging/merged_with_aliases.abi | 2 +- .../diverging/merged_with_aliases_and_custom_names.abi | 2 +- src/test/resources/merge/guess/common.api | 2 +- src/test/resources/merge/guess/guessed.api | 2 +- src/test/resources/merge/guess/linuxArm64Specific.api | 2 +- src/test/resources/merge/guess/merged.api | 2 +- src/test/resources/merge/idempotent/bcv-klib-test.abi | 2 +- src/test/resources/merge/identical/merged.abi | 2 +- .../merge/parseNarrowChildrenDecls/merged.abi | 2 +- .../merge/parseNarrowChildrenDecls/withoutLinuxAll.abi | 2 +- .../parseNarrowChildrenDecls/withoutLinuxArm64.abi | 2 +- src/test/resources/merge/webTargets/js.ext.abi | 2 +- src/test/resources/merge/webTargets/merged.abi | 2 +- src/test/resources/merge/webTargets/wasmJs.ext.abi | 2 +- src/test/resources/merge/webTargets/wasmWasi.ext.abi | 2 +- 39 files changed, 44 insertions(+), 44 deletions(-) rename src/functionalTest/kotlin/kotlinx/validation/test/{KLibVerificationTests.kt => KlibVerificationTests.kt} (99%) diff --git a/docs/design/KLibSupport.md b/docs/design/KLibSupport.md index 2c82bd7b..a196e304 100644 --- a/docs/design/KLibSupport.md +++ b/docs/design/KLibSupport.md @@ -53,7 +53,7 @@ in a file's header. Here's a brief example of such a merged dump file: ``` -// Merged KLib ABI Dump +// KLib ABI Dump // Targets: [androidNativeArm32, androidNativeArm64, androidNativeX64, androidNativeX86, linuxArm64, linuxX64, mingwX64] // Alias: linux => [linuxArm64, linuxX64] // Rendering settings: @@ -189,12 +189,12 @@ There are two main issues related to the renaming: However, a klib's manifest contains all the information required to find an actual underlying target (but it does not contain a custom name, though). And the same info could be included in a textual klib dump. -To overcome the issue, it is proposed to represent target name as a tuple consisting of "visible" name -and a canonical underlying target name: `name.canonicalName`. +To overcome the issue, it is proposed to represent target name as a tuple consisting of a "visible" +configurable target name and an underlying target name: `targetName.canfigurableName`. For the example mentioned above, target such fully qualified target names are: -- `macos.macosArm64` for `macosArm64("macos")`; -- `linux.linuxArm64` for `linuxArm64("linux")`; +- `macosArm64.macos` for `macosArm64("macos")`; +- `linuxArm64.linux` for `linuxArm64("linux")`; - `iosArm64.iosArm64` for `iosArm64()`. By default, when the visible and canonical names are the same, only one of them could be specified, so diff --git a/src/functionalTest/kotlin/kotlinx/validation/test/KLibVerificationTests.kt b/src/functionalTest/kotlin/kotlinx/validation/test/KlibVerificationTests.kt similarity index 99% rename from src/functionalTest/kotlin/kotlinx/validation/test/KLibVerificationTests.kt rename to src/functionalTest/kotlin/kotlinx/validation/test/KlibVerificationTests.kt index 4f774819..63198ed7 100644 --- a/src/functionalTest/kotlin/kotlinx/validation/test/KLibVerificationTests.kt +++ b/src/functionalTest/kotlin/kotlinx/validation/test/KlibVerificationTests.kt @@ -22,7 +22,7 @@ import kotlin.test.assertTrue internal const val BANNED_TARGETS_PROPERTY_NAME = "binary.compatibility.validator.klib.targets.disabled.for.testing" -private fun KLibVerificationTests.checkKlibDump( +private fun KlibVerificationTests.checkKlibDump( buildResult: BuildResult, expectedDumpFileName: String, projectName: String = "testproject", @@ -38,7 +38,7 @@ private fun KLibVerificationTests.checkKlibDump( Assertions.assertThat(generatedDump.readText()).isEqualToIgnoringNewLines(expected) } -internal class KLibVerificationTests : BaseKotlinGradleTest() { +internal class KlibVerificationTests : BaseKotlinGradleTest() { private fun BaseKotlinScope.baseProjectSetting() { settingsGradleKts { resolve("/examples/gradle/settings/settings-name-testproject.gradle.kts") diff --git a/src/functionalTest/resources/examples/classes/AnotherBuildConfig.klib.clash.dump b/src/functionalTest/resources/examples/classes/AnotherBuildConfig.klib.clash.dump index c3493903..0aba4248 100644 --- a/src/functionalTest/resources/examples/classes/AnotherBuildConfig.klib.clash.dump +++ b/src/functionalTest/resources/examples/classes/AnotherBuildConfig.klib.clash.dump @@ -1,4 +1,4 @@ -// Merged KLib ABI Dump +// KLib ABI Dump // Targets: [androidNativeArm32, androidNativeArm64, androidNativeX64, androidNativeX86, linuxArm64, linuxX64.linux, mingwX64] // Alias: linux => [linuxArm64, linuxX64.linux] // Rendering settings: diff --git a/src/functionalTest/resources/examples/classes/AnotherBuildConfig.klib.custom.dump b/src/functionalTest/resources/examples/classes/AnotherBuildConfig.klib.custom.dump index d09de432..8633ae30 100644 --- a/src/functionalTest/resources/examples/classes/AnotherBuildConfig.klib.custom.dump +++ b/src/functionalTest/resources/examples/classes/AnotherBuildConfig.klib.custom.dump @@ -1,4 +1,4 @@ -// Merged KLib ABI Dump +// KLib ABI Dump // Targets: [linuxX64.linuxA, linuxX64.linuxB] // Rendering settings: // - Signature version: 2 diff --git a/src/functionalTest/resources/examples/classes/AnotherBuildConfig.klib.dump b/src/functionalTest/resources/examples/classes/AnotherBuildConfig.klib.dump index 29d43e40..c1884a35 100644 --- a/src/functionalTest/resources/examples/classes/AnotherBuildConfig.klib.dump +++ b/src/functionalTest/resources/examples/classes/AnotherBuildConfig.klib.dump @@ -1,4 +1,4 @@ -// Merged KLib ABI Dump +// KLib ABI Dump // Targets: [androidNativeArm32, androidNativeArm64, androidNativeX64, androidNativeX86, linuxArm64, linuxX64, mingwX64] // Rendering settings: // - Signature version: 2 diff --git a/src/functionalTest/resources/examples/classes/AnotherBuildConfig.klib.renamedTarget.dump b/src/functionalTest/resources/examples/classes/AnotherBuildConfig.klib.renamedTarget.dump index ede29f43..7a63e873 100644 --- a/src/functionalTest/resources/examples/classes/AnotherBuildConfig.klib.renamedTarget.dump +++ b/src/functionalTest/resources/examples/classes/AnotherBuildConfig.klib.renamedTarget.dump @@ -1,4 +1,4 @@ -// Merged KLib ABI Dump +// KLib ABI Dump // Targets: [androidNativeArm32, androidNativeArm64, androidNativeX64, androidNativeX86, linuxArm64.linux, linuxX64, mingwX64] // Rendering settings: // - Signature version: 2 diff --git a/src/functionalTest/resources/examples/classes/AnotherBuildConfig.klib.web.dump b/src/functionalTest/resources/examples/classes/AnotherBuildConfig.klib.web.dump index 226addf8..cb7fa733 100644 --- a/src/functionalTest/resources/examples/classes/AnotherBuildConfig.klib.web.dump +++ b/src/functionalTest/resources/examples/classes/AnotherBuildConfig.klib.web.dump @@ -1,4 +1,4 @@ -// Merged KLib ABI Dump +// KLib ABI Dump // Targets: [androidNativeArm32, androidNativeArm64, androidNativeX64, androidNativeX86, js, linuxArm64, linuxX64, mingwX64, wasmJs, wasmWasi] // Rendering settings: // - Signature version: 2 diff --git a/src/functionalTest/resources/examples/classes/AnotherBuildConfigLinux.klib.grouping.dump b/src/functionalTest/resources/examples/classes/AnotherBuildConfigLinux.klib.grouping.dump index 32aeceb3..41092e34 100644 --- a/src/functionalTest/resources/examples/classes/AnotherBuildConfigLinux.klib.grouping.dump +++ b/src/functionalTest/resources/examples/classes/AnotherBuildConfigLinux.klib.grouping.dump @@ -1,4 +1,4 @@ -// Merged KLib ABI Dump +// KLib ABI Dump // Targets: [androidNativeArm32, androidNativeArm64, androidNativeX64, androidNativeX86, linuxArm64, linuxX64, mingwX64] // Alias: linux => [linuxArm64, linuxX64] // Rendering settings: diff --git a/src/functionalTest/resources/examples/classes/AnotherBuildConfigLinuxArm64Extra.klib.dump b/src/functionalTest/resources/examples/classes/AnotherBuildConfigLinuxArm64Extra.klib.dump index 5386f860..c4f9abfc 100644 --- a/src/functionalTest/resources/examples/classes/AnotherBuildConfigLinuxArm64Extra.klib.dump +++ b/src/functionalTest/resources/examples/classes/AnotherBuildConfigLinuxArm64Extra.klib.dump @@ -1,4 +1,4 @@ -// Merged KLib ABI Dump +// KLib ABI Dump // Targets: [androidNativeArm32, androidNativeArm64, androidNativeX64, androidNativeX86, linuxArm64, linuxX64, mingwX64] // Rendering settings: // - Signature version: 2 diff --git a/src/functionalTest/resources/examples/classes/AnotherBuildConfigModified.klib.dump b/src/functionalTest/resources/examples/classes/AnotherBuildConfigModified.klib.dump index d498c0c1..d24e55c2 100644 --- a/src/functionalTest/resources/examples/classes/AnotherBuildConfigModified.klib.dump +++ b/src/functionalTest/resources/examples/classes/AnotherBuildConfigModified.klib.dump @@ -1,4 +1,4 @@ -// Merged KLib ABI Dump +// KLib ABI Dump // Targets: [androidNativeArm32, androidNativeArm64, androidNativeX64, androidNativeX86, linuxArm64, linuxX64, mingwX64] // Rendering settings: // - Signature version: 2 diff --git a/src/functionalTest/resources/examples/classes/ClassWithPublicMarkers.klib.dump b/src/functionalTest/resources/examples/classes/ClassWithPublicMarkers.klib.dump index 4884ae1f..7e91492e 100644 --- a/src/functionalTest/resources/examples/classes/ClassWithPublicMarkers.klib.dump +++ b/src/functionalTest/resources/examples/classes/ClassWithPublicMarkers.klib.dump @@ -1,4 +1,4 @@ -// Merged KLib ABI Dump +// KLib ABI Dump // Targets: [androidNativeArm32, androidNativeArm64, androidNativeX64, androidNativeX86, linuxArm64, linuxX64, mingwX64] // Alias: androidNative => [androidNativeArm32, androidNativeArm64, androidNativeX64, androidNativeX86] // Alias: linux => [linuxArm64, linuxX64] diff --git a/src/functionalTest/resources/examples/classes/Empty.klib.dump b/src/functionalTest/resources/examples/classes/Empty.klib.dump index cc4d1356..03380850 100644 --- a/src/functionalTest/resources/examples/classes/Empty.klib.dump +++ b/src/functionalTest/resources/examples/classes/Empty.klib.dump @@ -1,4 +1,4 @@ -// Merged KLib ABI Dump +// KLib ABI Dump // Targets: [mingwX64] // Rendering settings: // - Signature version: 2 diff --git a/src/functionalTest/resources/examples/classes/HiddenDeclarations.klib.dump b/src/functionalTest/resources/examples/classes/HiddenDeclarations.klib.dump index 030dc49d..337ef7a3 100644 --- a/src/functionalTest/resources/examples/classes/HiddenDeclarations.klib.dump +++ b/src/functionalTest/resources/examples/classes/HiddenDeclarations.klib.dump @@ -1,4 +1,4 @@ -// Merged KLib ABI Dump +// KLib ABI Dump // Targets: [androidNativeArm32, androidNativeArm64, androidNativeX64, androidNativeX86, linuxArm64, linuxX64, mingwX64] // Rendering settings: // - Signature version: 2 diff --git a/src/functionalTest/resources/examples/classes/Properties.klib.dump b/src/functionalTest/resources/examples/classes/Properties.klib.dump index f69dad8d..d4ae3a68 100644 --- a/src/functionalTest/resources/examples/classes/Properties.klib.dump +++ b/src/functionalTest/resources/examples/classes/Properties.klib.dump @@ -1,4 +1,4 @@ -// Merged KLib ABI Dump +// KLib ABI Dump // Targets: [androidNativeArm32, androidNativeArm64, androidNativeX64, androidNativeX86, linuxArm64, linuxX64, mingwX64] // Rendering settings: // - Signature version: 2 diff --git a/src/functionalTest/resources/examples/classes/Subclasses.klib.dump b/src/functionalTest/resources/examples/classes/Subclasses.klib.dump index 854fac55..e06df81c 100644 --- a/src/functionalTest/resources/examples/classes/Subclasses.klib.dump +++ b/src/functionalTest/resources/examples/classes/Subclasses.klib.dump @@ -1,4 +1,4 @@ -// Merged KLib ABI Dump +// KLib ABI Dump // Targets: [androidNativeArm32, androidNativeArm64, androidNativeX64, androidNativeX86, linuxArm64, linuxX64, mingwX64] // Rendering settings: // - Signature version: 2 diff --git a/src/functionalTest/resources/examples/classes/TopLevelDeclarations.klib.all.dump b/src/functionalTest/resources/examples/classes/TopLevelDeclarations.klib.all.dump index a10615fd..5505d583 100644 --- a/src/functionalTest/resources/examples/classes/TopLevelDeclarations.klib.all.dump +++ b/src/functionalTest/resources/examples/classes/TopLevelDeclarations.klib.all.dump @@ -1,4 +1,4 @@ -// Merged KLib ABI Dump +// KLib ABI Dump // Targets: [androidNativeArm32, androidNativeArm64, androidNativeX64, androidNativeX86, iosArm64, iosSimulatorArm64, iosX64, linuxArm64, linuxX64, macosArm64, macosX64, mingwX64, tvosArm64, tvosSimulatorArm64, tvosX64, watchosArm32, watchosArm64, watchosDeviceArm64, watchosSimulatorArm64, watchosX64] // Rendering settings: // - Signature version: 2 diff --git a/src/functionalTest/resources/examples/classes/TopLevelDeclarations.klib.dump b/src/functionalTest/resources/examples/classes/TopLevelDeclarations.klib.dump index 91d444fe..38deb94f 100644 --- a/src/functionalTest/resources/examples/classes/TopLevelDeclarations.klib.dump +++ b/src/functionalTest/resources/examples/classes/TopLevelDeclarations.klib.dump @@ -1,4 +1,4 @@ -// Merged KLib ABI Dump +// KLib ABI Dump // Targets: [androidNativeArm32, androidNativeArm64, androidNativeX64, androidNativeX86, linuxArm64, linuxX64, mingwX64] // Alias: androidNative => [androidNativeArm32, androidNativeArm64, androidNativeX64, androidNativeX86] // Rendering settings: diff --git a/src/functionalTest/resources/examples/classes/TopLevelDeclarations.klib.unsup.dump b/src/functionalTest/resources/examples/classes/TopLevelDeclarations.klib.unsup.dump index d9a4ac78..01682364 100644 --- a/src/functionalTest/resources/examples/classes/TopLevelDeclarations.klib.unsup.dump +++ b/src/functionalTest/resources/examples/classes/TopLevelDeclarations.klib.unsup.dump @@ -1,4 +1,4 @@ -// Merged KLib ABI Dump +// KLib ABI Dump // Targets: [androidNativeArm32, androidNativeArm64, androidNativeX64, androidNativeX86, linuxX64, mingwX64] // Rendering settings: // - Signature version: 2 diff --git a/src/functionalTest/resources/examples/classes/TopLevelDeclarations.klib.v1.dump b/src/functionalTest/resources/examples/classes/TopLevelDeclarations.klib.v1.dump index 7e40214b..23824600 100644 --- a/src/functionalTest/resources/examples/classes/TopLevelDeclarations.klib.v1.dump +++ b/src/functionalTest/resources/examples/classes/TopLevelDeclarations.klib.v1.dump @@ -1,4 +1,4 @@ -// Merged KLib ABI Dump +// KLib ABI Dump // Targets: [androidNativeArm32, androidNativeArm64, androidNativeX64, androidNativeX86, linuxArm64, linuxX64, mingwX64] // Rendering settings: // - Signature version: 1 diff --git a/src/functionalTest/resources/examples/classes/TopLevelDeclarations.klib.with.guessed.linux.dump b/src/functionalTest/resources/examples/classes/TopLevelDeclarations.klib.with.guessed.linux.dump index a485b92e..5da647fd 100644 --- a/src/functionalTest/resources/examples/classes/TopLevelDeclarations.klib.with.guessed.linux.dump +++ b/src/functionalTest/resources/examples/classes/TopLevelDeclarations.klib.with.guessed.linux.dump @@ -1,4 +1,4 @@ -// Merged KLib ABI Dump +// KLib ABI Dump // Targets: [androidNativeArm32, androidNativeArm64, androidNativeX64, androidNativeX86, linuxArm64, linuxX64.linux, mingwX64] // Rendering settings: // - Signature version: 2 diff --git a/src/functionalTest/resources/examples/classes/TopLevelDeclarations.klib.with.linux.dump b/src/functionalTest/resources/examples/classes/TopLevelDeclarations.klib.with.linux.dump index 27642861..35f9ccd5 100644 --- a/src/functionalTest/resources/examples/classes/TopLevelDeclarations.klib.with.linux.dump +++ b/src/functionalTest/resources/examples/classes/TopLevelDeclarations.klib.with.linux.dump @@ -1,4 +1,4 @@ -// Merged KLib ABI Dump +// KLib ABI Dump // Targets: [androidNativeArm32, androidNativeArm64, androidNativeX64, androidNativeX86, linuxArm64, linuxX64, mingwX64] // Rendering settings: // - Signature version: 2 diff --git a/src/main/kotlin/klib/KlibAbiDumpFileMerger.kt b/src/main/kotlin/klib/KlibAbiDumpFileMerger.kt index 4993d28e..1a005ec4 100644 --- a/src/main/kotlin/klib/KlibAbiDumpFileMerger.kt +++ b/src/main/kotlin/klib/KlibAbiDumpFileMerger.kt @@ -37,7 +37,7 @@ internal class LinesProvider(private val lines: Iterator) : Iterator [linuxArm64, linuxX64] // Rendering settings: diff --git a/src/test/resources/merge/diverging/merged_with_aliases.abi b/src/test/resources/merge/diverging/merged_with_aliases.abi index dedae58f..26761ebc 100644 --- a/src/test/resources/merge/diverging/merged_with_aliases.abi +++ b/src/test/resources/merge/diverging/merged_with_aliases.abi @@ -1,4 +1,4 @@ -// Merged KLib ABI Dump +// KLib ABI Dump // Targets: [androidNativeArm64, linuxArm64, linuxX64, tvosX64] // Alias: linux => [linuxArm64, linuxX64] // Rendering settings: diff --git a/src/test/resources/merge/diverging/merged_with_aliases_and_custom_names.abi b/src/test/resources/merge/diverging/merged_with_aliases_and_custom_names.abi index 9db0ee51..32e8bdbf 100644 --- a/src/test/resources/merge/diverging/merged_with_aliases_and_custom_names.abi +++ b/src/test/resources/merge/diverging/merged_with_aliases_and_custom_names.abi @@ -1,4 +1,4 @@ -// Merged KLib ABI Dump +// KLib ABI Dump // Targets: [androidNativeArm64.android, linuxArm64.linux, linuxX64, tvosX64] // Alias: linux => [linuxArm64.linux, linuxX64] // Rendering settings: diff --git a/src/test/resources/merge/guess/common.api b/src/test/resources/merge/guess/common.api index 75a14693..d90c8cf5 100644 --- a/src/test/resources/merge/guess/common.api +++ b/src/test/resources/merge/guess/common.api @@ -1,4 +1,4 @@ -// Merged KLib ABI Dump +// KLib ABI Dump // Targets: [androidNativeArm64, linuxX64, tvOsX64] // Rendering settings: // - Signature version: 2 diff --git a/src/test/resources/merge/guess/guessed.api b/src/test/resources/merge/guess/guessed.api index f723898a..6dbdad42 100644 --- a/src/test/resources/merge/guess/guessed.api +++ b/src/test/resources/merge/guess/guessed.api @@ -1,4 +1,4 @@ -// Merged KLib ABI Dump +// KLib ABI Dump // Targets: [linuxArm64] // Rendering settings: // - Signature version: 2 diff --git a/src/test/resources/merge/guess/linuxArm64Specific.api b/src/test/resources/merge/guess/linuxArm64Specific.api index 51e56fbe..2ece7ab5 100644 --- a/src/test/resources/merge/guess/linuxArm64Specific.api +++ b/src/test/resources/merge/guess/linuxArm64Specific.api @@ -1,4 +1,4 @@ -// Merged KLib ABI Dump +// KLib ABI Dump // Targets: [linuxArm64] // Rendering settings: // - Signature version: 2 diff --git a/src/test/resources/merge/guess/merged.api b/src/test/resources/merge/guess/merged.api index 9279929a..12322ba5 100644 --- a/src/test/resources/merge/guess/merged.api +++ b/src/test/resources/merge/guess/merged.api @@ -1,4 +1,4 @@ -// Merged KLib ABI Dump +// KLib ABI Dump // Targets: [androidNativeArm64, linuxArm64, linuxX64, tvOsX64] // Rendering settings: // - Signature version: 2 diff --git a/src/test/resources/merge/idempotent/bcv-klib-test.abi b/src/test/resources/merge/idempotent/bcv-klib-test.abi index 959708b3..12c1d64b 100644 --- a/src/test/resources/merge/idempotent/bcv-klib-test.abi +++ b/src/test/resources/merge/idempotent/bcv-klib-test.abi @@ -1,4 +1,4 @@ -// Merged KLib ABI Dump +// KLib ABI Dump // Targets: [androidNativeArm32, androidNativeArm64, iosSimulatorArm64, linuxArm64, linuxX64, macosArm64, tvosArm64, tvosSimulatorArm64, tvosX64] // Alias: apple => [iosSimulatorArm64, macosArm64, tvosArm64, tvosSimulatorArm64, tvosX64] // Alias: linux => [linuxArm64, linuxX64] diff --git a/src/test/resources/merge/identical/merged.abi b/src/test/resources/merge/identical/merged.abi index 6a72f1c6..087ab730 100644 --- a/src/test/resources/merge/identical/merged.abi +++ b/src/test/resources/merge/identical/merged.abi @@ -1,4 +1,4 @@ -// Merged KLib ABI Dump +// KLib ABI Dump // Targets: [linuxX64, macosArm64] // Rendering settings: // - Signature version: 2 diff --git a/src/test/resources/merge/parseNarrowChildrenDecls/merged.abi b/src/test/resources/merge/parseNarrowChildrenDecls/merged.abi index 25f9df1e..5d24a140 100644 --- a/src/test/resources/merge/parseNarrowChildrenDecls/merged.abi +++ b/src/test/resources/merge/parseNarrowChildrenDecls/merged.abi @@ -1,4 +1,4 @@ -// Merged KLib ABI Dump +// KLib ABI Dump // Targets: [androidNativeArm64, linuxArm64, linuxX64, tvOsX64] // Rendering settings: // - Signature version: 2 diff --git a/src/test/resources/merge/parseNarrowChildrenDecls/withoutLinuxAll.abi b/src/test/resources/merge/parseNarrowChildrenDecls/withoutLinuxAll.abi index 6ff8e005..31b540d9 100644 --- a/src/test/resources/merge/parseNarrowChildrenDecls/withoutLinuxAll.abi +++ b/src/test/resources/merge/parseNarrowChildrenDecls/withoutLinuxAll.abi @@ -1,4 +1,4 @@ -// Merged KLib ABI Dump +// KLib ABI Dump // Targets: [androidNativeArm64, tvOsX64] // Rendering settings: // - Signature version: 2 diff --git a/src/test/resources/merge/parseNarrowChildrenDecls/withoutLinuxArm64.abi b/src/test/resources/merge/parseNarrowChildrenDecls/withoutLinuxArm64.abi index 812f852d..d0da23f4 100644 --- a/src/test/resources/merge/parseNarrowChildrenDecls/withoutLinuxArm64.abi +++ b/src/test/resources/merge/parseNarrowChildrenDecls/withoutLinuxArm64.abi @@ -1,4 +1,4 @@ -// Merged KLib ABI Dump +// KLib ABI Dump // Targets: [androidNativeArm64, linuxX64, tvOsX64] // Rendering settings: // - Signature version: 2 diff --git a/src/test/resources/merge/webTargets/js.ext.abi b/src/test/resources/merge/webTargets/js.ext.abi index e3b46200..61cad6c7 100644 --- a/src/test/resources/merge/webTargets/js.ext.abi +++ b/src/test/resources/merge/webTargets/js.ext.abi @@ -1,4 +1,4 @@ -// Merged KLib ABI Dump +// KLib ABI Dump // Targets: [js] // Rendering settings: // - Signature version: 2 diff --git a/src/test/resources/merge/webTargets/merged.abi b/src/test/resources/merge/webTargets/merged.abi index 451047a7..8bb24824 100644 --- a/src/test/resources/merge/webTargets/merged.abi +++ b/src/test/resources/merge/webTargets/merged.abi @@ -1,4 +1,4 @@ -// Merged KLib ABI Dump +// KLib ABI Dump // Targets: [js, wasmJs, wasmWasi] // Rendering settings: // - Signature version: 2 diff --git a/src/test/resources/merge/webTargets/wasmJs.ext.abi b/src/test/resources/merge/webTargets/wasmJs.ext.abi index 3959e180..af4f7f4b 100644 --- a/src/test/resources/merge/webTargets/wasmJs.ext.abi +++ b/src/test/resources/merge/webTargets/wasmJs.ext.abi @@ -1,4 +1,4 @@ -// Merged KLib ABI Dump +// KLib ABI Dump // Targets: [wasmJs] // Rendering settings: // - Signature version: 2 diff --git a/src/test/resources/merge/webTargets/wasmWasi.ext.abi b/src/test/resources/merge/webTargets/wasmWasi.ext.abi index 94c81355..1451d373 100644 --- a/src/test/resources/merge/webTargets/wasmWasi.ext.abi +++ b/src/test/resources/merge/webTargets/wasmWasi.ext.abi @@ -1,4 +1,4 @@ -// Merged KLib ABI Dump +// KLib ABI Dump // Targets: [wasmWasi] // Rendering settings: // - Signature version: 2 From 9aa5be8736e00f11682a7d0bdc844ea312395112 Mon Sep 17 00:00:00 2001 From: Filipp Zhinkin Date: Thu, 7 Mar 2024 16:37:04 +0100 Subject: [PATCH 37/53] Added public API prototype and switched Gradle tasks to it, fixed the parser --- api/binary-compatibility-validator.api | 53 +++++ .../classes/TopLevelDeclarations.klib.dump | 1 - src/main/kotlin/KotlinKlibAbiBuildTask.kt | 99 +-------- ...otlinKlibExtractSupportedTargetsAbiTask.kt | 12 +- ...linKlibInferAbiForUnsupportedTargetTask.kt | 32 ++- src/main/kotlin/KotlinKlibMergeAbiTask.kt | 12 +- src/main/kotlin/api/klib/KlibDump.kt | 126 ++++++++++++ src/main/kotlin/api/klib/KlibDumpFilters.kt | 194 ++++++++++++++++++ src/main/kotlin/klib/KlibAbiDumpFileMerger.kt | 100 ++++++--- .../kotlin/tests/ClassNameConvertionTest.kt | 2 +- src/test/kotlin/tests/KlibAbiMergingTest.kt | 68 +++--- 11 files changed, 521 insertions(+), 178 deletions(-) create mode 100644 src/main/kotlin/api/klib/KlibDump.kt create mode 100644 src/main/kotlin/api/klib/KlibDumpFilters.kt diff --git a/api/binary-compatibility-validator.api b/api/binary-compatibility-validator.api index c71bba5d..b773d00e 100644 --- a/api/binary-compatibility-validator.api +++ b/api/binary-compatibility-validator.api @@ -108,6 +108,59 @@ public final class kotlinx/validation/api/KotlinSignaturesLoadingKt { public static synthetic fun retainExplicitlyIncludedIfDeclared$default (Ljava/util/List;Ljava/util/Collection;Ljava/util/Collection;Ljava/util/Collection;ILjava/lang/Object;)Ljava/util/List; } +public final class kotlinx/validation/api/klib/KLibDumpFilters { + public static final field Companion Lkotlinx/validation/api/klib/KLibDumpFilters$Companion; + public final fun getIgnoredClasses ()Ljava/util/Set; + public final fun getIgnoredPackages ()Ljava/util/Set; + public final fun getNonPublicMarkers ()Ljava/util/Set; + public final fun getSignatureVersion ()Lkotlinx/validation/api/klib/KlibSignatureVersion; +} + +public final class kotlinx/validation/api/klib/KLibDumpFilters$Builder { + public fun ()V + public final fun build ()Lkotlinx/validation/api/klib/KLibDumpFilters; + public final fun getIgnoredClasses ()Ljava/util/Set; + public final fun getIgnoredPackages ()Ljava/util/Set; + public final fun getNonPublicMarkers ()Ljava/util/Set; + public final fun getSignatureVersion ()Lkotlinx/validation/api/klib/KlibSignatureVersion; + public final fun setSignatureVersion (Lkotlinx/validation/api/klib/KlibSignatureVersion;)V +} + +public final class kotlinx/validation/api/klib/KLibDumpFilters$Companion { + public final fun getDEFAULT ()Lkotlinx/validation/api/klib/KLibDumpFilters; +} + +public final class kotlinx/validation/api/klib/KlibDump { + public static final field Companion Lkotlinx/validation/api/klib/KlibDump$Companion; + public fun ()V + public final fun copy ()Lkotlinx/validation/api/klib/KlibDump; + public final fun getTargets ()Ljava/util/Set; + public final fun merge (Ljava/io/File;Ljava/lang/String;)V + public final fun merge (Lkotlinx/validation/api/klib/KlibDump;)V + public static synthetic fun merge$default (Lkotlinx/validation/api/klib/KlibDump;Ljava/io/File;Ljava/lang/String;ILjava/lang/Object;)V + public final fun remove (Ljava/lang/Iterable;)V + public final fun retain (Ljava/lang/Iterable;)V + public final fun saveTo (Ljava/lang/Appendable;)V +} + +public final class kotlinx/validation/api/klib/KlibDump$Companion { + public final fun from (Ljava/io/File;Ljava/lang/String;)Lkotlinx/validation/api/klib/KlibDump; + public static synthetic fun from$default (Lkotlinx/validation/api/klib/KlibDump$Companion;Ljava/io/File;Ljava/lang/String;ILjava/lang/Object;)Lkotlinx/validation/api/klib/KlibDump; + public final fun fromKlib (Ljava/io/File;Ljava/lang/String;Lkotlinx/validation/api/klib/KLibDumpFilters;)Lkotlinx/validation/api/klib/KlibDump; + public static synthetic fun fromKlib$default (Lkotlinx/validation/api/klib/KlibDump$Companion;Ljava/io/File;Ljava/lang/String;Lkotlinx/validation/api/klib/KLibDumpFilters;ILjava/lang/Object;)Lkotlinx/validation/api/klib/KlibDump; +} + +public final class kotlinx/validation/api/klib/KlibDumpFiltersKt { + public static final fun KLibDumpFilters (Lkotlin/jvm/functions/Function1;)Lkotlinx/validation/api/klib/KLibDumpFilters; +} + +public final class kotlinx/validation/api/klib/KlibDumpKt { + public static final fun inferAbi (Lkotlinx/validation/api/klib/KlibTarget;Ljava/lang/Iterable;Lkotlinx/validation/api/klib/KlibDump;)Lkotlinx/validation/api/klib/KlibDump; + public static synthetic fun inferAbi$default (Lkotlinx/validation/api/klib/KlibTarget;Ljava/lang/Iterable;Lkotlinx/validation/api/klib/KlibDump;ILjava/lang/Object;)Lkotlinx/validation/api/klib/KlibDump; + public static final fun mergeKlib (Lkotlinx/validation/api/klib/KlibDump;Ljava/io/File;Ljava/lang/String;Lkotlinx/validation/api/klib/KLibDumpFilters;)V + public static synthetic fun mergeKlib$default (Lkotlinx/validation/api/klib/KlibDump;Ljava/io/File;Ljava/lang/String;Lkotlinx/validation/api/klib/KLibDumpFilters;ILjava/lang/Object;)V +} + public final class kotlinx/validation/api/klib/KlibSignatureVersion { public static final field Companion Lkotlinx/validation/api/klib/KlibSignatureVersion$Companion; public fun equals (Ljava/lang/Object;)Z diff --git a/src/functionalTest/resources/examples/classes/TopLevelDeclarations.klib.dump b/src/functionalTest/resources/examples/classes/TopLevelDeclarations.klib.dump index 38deb94f..35f9ccd5 100644 --- a/src/functionalTest/resources/examples/classes/TopLevelDeclarations.klib.dump +++ b/src/functionalTest/resources/examples/classes/TopLevelDeclarations.klib.dump @@ -1,6 +1,5 @@ // KLib ABI Dump // Targets: [androidNativeArm32, androidNativeArm64, androidNativeX64, androidNativeX86, linuxArm64, linuxX64, mingwX64] -// Alias: androidNative => [androidNativeArm32, androidNativeArm64, androidNativeX64, androidNativeX86] // Rendering settings: // - Signature version: 2 // - Show manifest properties: true diff --git a/src/main/kotlin/KotlinKlibAbiBuildTask.kt b/src/main/kotlin/KotlinKlibAbiBuildTask.kt index 0216e815..2b0fc09f 100644 --- a/src/main/kotlin/KotlinKlibAbiBuildTask.kt +++ b/src/main/kotlin/KotlinKlibAbiBuildTask.kt @@ -5,13 +5,14 @@ package kotlinx.validation +import kotlinx.validation.api.klib.KLibDumpFilters +import kotlinx.validation.api.klib.KlibDump import kotlinx.validation.api.klib.KlibSignatureVersion import org.gradle.api.file.FileCollection import org.gradle.api.tasks.Input import org.gradle.api.tasks.InputFiles import org.gradle.api.tasks.Optional import org.gradle.api.tasks.TaskAction -import org.jetbrains.kotlin.library.abi.* import java.io.Serializable internal class SerializableSignatureVersion(val version: Int) : Serializable { @@ -50,101 +51,21 @@ internal abstract class KotlinKlibAbiBuildTask : BuildTaskBase() { @Input lateinit var target: String - @ExperimentalStdlibApi - @ExperimentalLibraryAbiReader + @OptIn(ExperimentalBCVApi::class) @TaskAction internal fun generate() { outputApiFile.delete() outputApiFile.parentFile.mkdirs() - val filters = buildList { - if (ignoredPackages.isNotEmpty()) { - add(AbiReadingFilter.ExcludedPackages(ignoredPackages.map { AbiCompoundName(it) })) - } - if (ignoredClasses.isNotEmpty()) { - add(AbiReadingFilter.ExcludedClasses(ignoredClasses.toKlibNames())) - } - if (nonPublicMarkers.isNotEmpty()) { - add(AbiReadingFilter.NonPublicMarkerAnnotations(nonPublicMarkers.toKlibNames())) - } - } - - val parsedAbi = try { - LibraryAbiReader.readAbiInfo(klibFile.singleFile, filters) - } catch (e: Exception) { - throw IllegalStateException("Can't read a klib: ${klibFile.singleFile}", e) - } - - val supportedVersions = parsedAbi.signatureVersions.asSequence().filter { it.isSupportedByAbiReader } - val sigVersion = if (signatureVersion.toKlibSignatureVersion() != KlibSignatureVersion.LATEST) { - val versionNumbers = supportedVersions.map { it.versionNumber }.toSortedSet() - if (signatureVersion.version !in versionNumbers) { - throw IllegalArgumentException( - "Unsupported KLib signature version '${signatureVersion.version}'. " + - "Supported versions are: $versionNumbers" - ) - } - AbiSignatureVersion.resolveByVersionNumber(signatureVersion.version) - } else { - supportedVersions.filter { it.isSupportedByAbiReader }.maxByOrNull(AbiSignatureVersion::versionNumber) - ?: throw IllegalStateException("Can't choose signatureVersion") - } + val dump = KlibDump.fromKlib(klibFile.singleFile, target, KLibDumpFilters { + ignoredClasses.addAll(this@KotlinKlibAbiBuildTask.ignoredClasses) + ignoredPackages.addAll(this@KotlinKlibAbiBuildTask.ignoredPackages) + nonPublicMarkers.addAll(this@KotlinKlibAbiBuildTask.nonPublicMarkers) + signatureVersion = this@KotlinKlibAbiBuildTask.signatureVersion.toKlibSignatureVersion() + }) outputApiFile.bufferedWriter().use { - LibraryAbiRenderer.render(parsedAbi, it, AbiRenderingSettings(sigVersion, renderManifest = true)) - } - } -} - -// We're assuming that all names are in valid binary form as it's described in JVMLS §13.1: -// https://docs.oracle.com/javase/specs/jls/se21/html/jls-13.html#jls-13.1 -@OptIn(ExperimentalLibraryAbiReader::class) -private fun Collection.toKlibNames(): List = - this.map(String::toAbiQualifiedName).filterNotNull() - -@OptIn(ExperimentalLibraryAbiReader::class) -internal fun String.toAbiQualifiedName(): AbiQualifiedName? { - if (this.isBlank() || this.contains('/')) return null - // Easiest part: dissect package name from the class name - val idx = this.lastIndexOf('.') - if (idx == -1) { - return AbiQualifiedName(AbiCompoundName(""), this.classNameToCompoundName()) - } else { - val packageName = this.substring(0, idx) - val className = this.substring(idx + 1) - return AbiQualifiedName(AbiCompoundName(packageName), className.classNameToCompoundName()) - } -} - -@OptIn(ExperimentalLibraryAbiReader::class) -private fun String.classNameToCompoundName(): AbiCompoundName { - if (this.isEmpty()) return AbiCompoundName(this) - - val segments = mutableListOf() - val builder = StringBuilder() - - for (idx in this.indices) { - val c = this[idx] - // Don't treat a character as a separator if: - // - it's not a '$' - // - it's at the beginning of the segment - // - it's the last character of the string - if ( c != '$' || builder.isEmpty() || idx == this.length - 1) { - builder.append(c) - continue - } - check(c == '$') - // class$$$susbclass -> class.$$subclass, were at second $ here. - if (builder.last() == '$') { - builder.append(c) - continue + dump.saveTo(it) } - - segments.add(builder.toString()) - builder.clear() - } - if (builder.isNotEmpty()) { - segments.add(builder.toString()) } - return AbiCompoundName(segments.joinToString(separator = ".")) } diff --git a/src/main/kotlin/KotlinKlibExtractSupportedTargetsAbiTask.kt b/src/main/kotlin/KotlinKlibExtractSupportedTargetsAbiTask.kt index 8a97eefc..d719d318 100644 --- a/src/main/kotlin/KotlinKlibExtractSupportedTargetsAbiTask.kt +++ b/src/main/kotlin/KotlinKlibExtractSupportedTargetsAbiTask.kt @@ -5,6 +5,7 @@ package kotlinx.validation +import kotlinx.validation.api.klib.KlibDump import kotlinx.validation.klib.KlibAbiDumpMerger import kotlinx.validation.api.klib.KlibTarget import org.gradle.api.DefaultTask @@ -43,13 +44,16 @@ internal abstract class KotlinKlibExtractSupportedTargetsAbiTask : DefaultTask() @Input var strictValidation: Boolean = false + @OptIn(ExperimentalBCVApi::class) @TaskAction internal fun generate() { if (inputAbiFile.length() == 0L) { error("Project ABI file $inputAbiFile is empty.") } - val dump = KlibAbiDumpMerger().apply { load(inputAbiFile) } + val dump = KlibDump.from(inputAbiFile) val enabledTargets = supportedTargets.get().map { KlibTarget.parse(it).targetName } + // Filter out only unsupported files. + // That ensures that target renaming will be caught and reported as a change. val targetsToRemove = dump.targets.filter { it.targetName !in enabledTargets } if (targetsToRemove.isNotEmpty() && strictValidation) { throw IllegalStateException( @@ -57,9 +61,7 @@ internal abstract class KotlinKlibExtractSupportedTargetsAbiTask : DefaultTask() "and the strictValidation mode was enabled." ) } - for (target in targetsToRemove) { - dump.remove(target) - } - outputAbiFile.bufferedWriter().use { dump.dump(it) } + dump.remove(targetsToRemove) + outputAbiFile.bufferedWriter().use { dump.saveTo(it) } } } diff --git a/src/main/kotlin/KotlinKlibInferAbiForUnsupportedTargetTask.kt b/src/main/kotlin/KotlinKlibInferAbiForUnsupportedTargetTask.kt index bb3b2e2a..fee4aafa 100644 --- a/src/main/kotlin/KotlinKlibInferAbiForUnsupportedTargetTask.kt +++ b/src/main/kotlin/KotlinKlibInferAbiForUnsupportedTargetTask.kt @@ -5,8 +5,10 @@ package kotlinx.validation +import kotlinx.validation.api.klib.KlibDump import kotlinx.validation.klib.KlibAbiDumpMerger import kotlinx.validation.api.klib.KlibTarget +import kotlinx.validation.api.klib.inferAbi import kotlinx.validation.klib.TargetHierarchy import org.gradle.api.DefaultTask import org.gradle.api.provider.Provider @@ -71,6 +73,7 @@ internal abstract class KotlinKlibInferAbiForUnsupportedTargetTask : DefaultTask @OutputFile lateinit var outputFile: File + @OptIn(ExperimentalBCVApi::class) @TaskAction internal fun generate() { val unsupportedTarget = KlibTarget(unsupportedTargetName, unsupportedTargetCanonicalName) @@ -78,27 +81,19 @@ internal abstract class KotlinKlibInferAbiForUnsupportedTargetTask : DefaultTask // Find a set of supported targets that are closer to unsupported target in the hierarchy. // Note that dumps are stored using configurable name, but grouped by the canonical target name. val matchingTargets = findMatchingTargets(supportedTargetNames, unsupportedTarget) - val target2outFile = supportedTargetNames.keysToMap { - File(outputApiDir).parentFile.resolve(it.configurableName).resolve(dumpFileName) - } - - // given a set of similar targets, combine their ABI files into a single merged dump and consider it - // a common ABI that should be shared by the unsupported target as well - val commonDump = KlibAbiDumpMerger() - for (target in matchingTargets) { - commonDump.load(target2outFile[target]!!, target.configurableName) + // Load dumps that are a good fit for inference + val supportedTargetDumps = matchingTargets.map { target -> + val dumpFile = File(outputApiDir).parentFile.resolve(target.configurableName).resolve(dumpFileName) + KlibDump.from(dumpFile, target.configurableName).also { + check(it.targets.single() == target) + } } - commonDump.retainCommonAbi() - // load and old dump (that may contain the dump for the unsupported target) and remove all but the declarations - // specific to the unsupported target - val image = KlibAbiDumpMerger() + // Load an old dump, if any + var image: KlibDump? = null if (inputImageFile.exists()) { if (inputImageFile.length() > 0L) { - image.load(inputImageFile) - image.retainTargetSpecificAbi(unsupportedTarget) - // merge common ABI with target-specific ABI - commonDump.mergeTargetSpecific(image) + image = KlibDump.from(inputImageFile) } else { logger.warn( "Project's ABI file exists, but empty: $inputImageFile. " + @@ -107,10 +102,9 @@ internal abstract class KotlinKlibInferAbiForUnsupportedTargetTask : DefaultTask ) } } - commonDump.overrideTargets(setOf(unsupportedTarget)) outputFile.bufferedWriter().use { - commonDump.dump(it) + inferAbi(unsupportedTarget, supportedTargetDumps, image).saveTo(it) } logger.warn( diff --git a/src/main/kotlin/KotlinKlibMergeAbiTask.kt b/src/main/kotlin/KotlinKlibMergeAbiTask.kt index 26d99946..2840c206 100644 --- a/src/main/kotlin/KotlinKlibMergeAbiTask.kt +++ b/src/main/kotlin/KotlinKlibMergeAbiTask.kt @@ -5,7 +5,7 @@ package kotlinx.validation -import kotlinx.validation.klib.KlibAbiDumpMerger +import kotlinx.validation.api.klib.KlibDump import org.gradle.api.DefaultTask import org.gradle.api.tasks.* import java.io.File @@ -48,12 +48,14 @@ internal abstract class KotlinKlibMergeAbiTask : DefaultTask() { targetToFile[target] = file } + @OptIn(ExperimentalBCVApi::class) @TaskAction internal fun merge() { - val builder = KlibAbiDumpMerger() - targets.forEach { targetName -> - builder.load(targetToFile[targetName]!!.resolve(dumpFileName), targetName) + val mergedDump = KlibDump().apply { + targetToFile.forEach { (targetName, dumpDir) -> + merge(dumpDir.resolve(dumpFileName), targetName) + } } - mergedFile.bufferedWriter().use { builder.dump(it) } + mergedFile.bufferedWriter().use { mergedDump.saveTo(it) } } } diff --git a/src/main/kotlin/api/klib/KlibDump.kt b/src/main/kotlin/api/klib/KlibDump.kt new file mode 100644 index 00000000..22139725 --- /dev/null +++ b/src/main/kotlin/api/klib/KlibDump.kt @@ -0,0 +1,126 @@ +/* + * Copyright 2016-2024 JetBrains s.r.o. + * Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file. + */ + +package kotlinx.validation.api.klib + +import kotlinx.validation.ExperimentalBCVApi +import kotlinx.validation.klib.KlibAbiDumpMerger +import java.io.File +import java.io.FileNotFoundException + +@ExperimentalBCVApi +public class KlibDump { + internal var merger: KlibAbiDumpMerger = KlibAbiDumpMerger() + + /** + * Set of targets for which this dump contains declarations. + */ + public val targets: Set + get() = merger.targets + + + /** + * Load a text dump for a target and merge it into this dump. + * + * @throws IllegalArgumentException if the dump already has that target. + * @throws FileNotFoundException if [dump] does not exist. + */ + public fun merge(from: File, withTargetName: String? = null) { + if (!from.exists()) throw FileNotFoundException(from.absolutePath) + merger.merge(from, withTargetName) + } + + /** + * Merge [other] dump into this one. + */ + public fun merge(other: KlibDump) { + val intersection = targets.intersect(other.targets) + require(intersection.isEmpty()) { + "Cannot merge dump as this and other dumps share some targets: $intersection" + } + merger.merge(other.merger) + } + + /** + * Remove all declarations that do not belong to the specified targets. + */ + public fun retain(targets: Iterable) { + val toRemove = merger.targets.subtract(targets.toSet()) + remove(toRemove) + } + + /** + * Remove all declarations that do belong to the specified target. + */ + public fun remove(targets: Iterable) { + targets.forEach { + merger.remove(it) + } + } + + public fun copy(): KlibDump = KlibDump().also { it.merge(this) } + + /** + * Convert the dump back into a textual form. + */ + public fun saveTo(to: Appendable) { + merger.dump(to) + } + + public companion object { + public fun from(dumpFile: File, configurableTargetName: String? = null): KlibDump { + check(dumpFile.exists()) { "File does not exist: ${dumpFile.absolutePath}" } + return KlibDump().apply { merge(dumpFile, configurableTargetName) } + } + public fun fromKlib( + klibFile: File, + configurableTargetName: String? = null, + filters: KLibDumpFilters = KLibDumpFilters.DEFAULT + ): KlibDump { + val dump = buildString { + dumpTo(this, klibFile, filters) + } + return KlibDump().apply { + merger.merge(dump.splitToSequence('\n').iterator(), configurableTargetName) + } + } + } +} + +/** + * Infer a possible public ABI for [target] using old merged dump [oldDump] + * and set of [generatedDumps] generated for other targets. + */ +@ExperimentalBCVApi +public fun inferAbi( + unsupportedTarget: KlibTarget, + supportedTargetDumps: Iterable, + oldMergedDump: KlibDump? = null +): KlibDump { + + val retainedDump = KlibDump().apply { + if (oldMergedDump != null) { + merge(oldMergedDump) + merger.retainTargetSpecificAbi(unsupportedTarget) + } + } + val commonDump = KlibDump().apply { + supportedTargetDumps.forEach { + merge(it) + } + merger.retainCommonAbi() + } + commonDump.merge(retainedDump) + commonDump.merger.overrideTargets(setOf(unsupportedTarget)) + return commonDump +} + +@ExperimentalBCVApi +public fun KlibDump.mergeKlib( + klibFile: File, configurableTargetName: String? = null, + filters: KLibDumpFilters = KLibDumpFilters.DEFAULT +) { + this.merge(KlibDump.fromKlib(klibFile, configurableTargetName, filters)) +} diff --git a/src/main/kotlin/api/klib/KlibDumpFilters.kt b/src/main/kotlin/api/klib/KlibDumpFilters.kt new file mode 100644 index 00000000..d7fd9f72 --- /dev/null +++ b/src/main/kotlin/api/klib/KlibDumpFilters.kt @@ -0,0 +1,194 @@ +/* + * Copyright 2016-2024 JetBrains s.r.o. + * Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file. + */ + +package kotlinx.validation.api.klib + +import kotlinx.validation.ExperimentalBCVApi +import org.jetbrains.kotlin.gradle.utils.`is` +import org.jetbrains.kotlin.library.abi.* +import java.io.File + +/** + * Filters affecting how the klib ABI will be represented in a dump. + */ +@ExperimentalBCVApi +public class KLibDumpFilters internal constructor( + /** + * Names of packages that should be excluded from a dump. + * If a package is listed here, none of its declarations will be included in a dump. + */ + public val ignoredPackages: Set, + /** + * Names of classes that should be excluded from a dump. + */ + public val ignoredClasses: Set, + /** + * Names of annotations marking non-public declarations. + * Such declarations will be excluded from a dump. + */ + public val nonPublicMarkers: Set, + /** + * KLib ABI signature version to include in a dump. + */ + public val signatureVersion: KlibSignatureVersion +) { + + public class Builder @PublishedApi internal constructor() { + /** + * Names of packages that should be excluded from a dump. + * If a package is listed here, none of its declarations will be included in a dump. + * + * By default, there are no ignored packages. + */ + public val ignoredPackages: MutableSet = mutableSetOf() + + /** + * Names of classes that should be excluded from a dump. + * + * By default, there are no ignored classes. + */ + public val ignoredClasses: MutableSet = mutableSetOf() + + /** + * Names of annotations marking non-public declarations. + * Such declarations will be excluded from a dump. + * + * By default, a set of non-public markers is empty. + */ + public val nonPublicMarkers: MutableSet = mutableSetOf() + + /** + * KLib ABI signature version to include in a dump. + * + * By default, the latest ABI signature version provided by a klib + * and supported by a reader will be used. + */ + public var signatureVersion: KlibSignatureVersion = KlibSignatureVersion.LATEST + + @PublishedApi + internal fun build(): KLibDumpFilters { + return KLibDumpFilters(ignoredPackages, ignoredClasses, nonPublicMarkers, signatureVersion) + } + } + + public companion object { + /** + * Default KLib ABI dump filters which declares no filters + * and uses the latest KLib ABI signature version available. + */ + public val DEFAULT: KLibDumpFilters = KLibDumpFilters {} + } +} + +/** + * Builds a new [KLibDumpFilters] instance by invoking a [builderAction] on a temporary + * [KLibDumpFilters.Builder] instance and then converting it into filters. + * + * Supplied [KLibDumpFilters.Builder] is valid only during the scope of [builderAction] execution. + */ +@ExperimentalBCVApi +public fun KLibDumpFilters(builderAction: KLibDumpFilters.Builder.() -> Unit): KLibDumpFilters { + val builder = KLibDumpFilters.Builder() + builderAction(builder) + return builder.build() +} + +@ExperimentalBCVApi +@OptIn(ExperimentalLibraryAbiReader::class) +internal fun dumpTo(to: Appendable, klibFile: File, filters: KLibDumpFilters) { + require(klibFile.exists()) { "File does not exist: ${klibFile.absolutePath}" } + val abiFilters = mutableListOf() + filters.ignoredClasses.toKlibNames().also { + if (it.isNotEmpty()) { + abiFilters.add(AbiReadingFilter.ExcludedClasses(it)) + } + } + filters.nonPublicMarkers.toKlibNames().also { + if (it.isNotEmpty()) { + abiFilters.add(AbiReadingFilter.NonPublicMarkerAnnotations(it)) + } + } + if (filters.ignoredPackages.isNotEmpty()) { + abiFilters.add(AbiReadingFilter.ExcludedPackages(filters.ignoredPackages.map { AbiCompoundName(it) })) + } + + val library = LibraryAbiReader.readAbiInfo(klibFile, abiFilters) + + val supportedSignatureVersions = library.signatureVersions.asSequence().filter { it.isSupportedByAbiReader } + + val signatureVersion = if (filters.signatureVersion == KlibSignatureVersion.LATEST) { + supportedSignatureVersions.maxByOrNull { it.versionNumber } + ?: throw IllegalStateException("Can't choose signatureVersion") + } else { + supportedSignatureVersions.find { it.versionNumber == filters.signatureVersion.version } + ?: throw IllegalArgumentException( + "Unsupported KLib signature version '${filters.signatureVersion.version}'. " + + "Supported versions are: ${ + supportedSignatureVersions.map { it.versionNumber }.sorted().toList() + }" + ) + } + + LibraryAbiRenderer.render( + library, to, AbiRenderingSettings( + renderedSignatureVersion = signatureVersion, + renderManifest = true, + renderDeclarations = true + ) + ) +} + +// We're assuming that all names are in valid binary form as it's described in JVMLS §13.1: +// https://docs.oracle.com/javase/specs/jls/se21/html/jls-13.html#jls-13.1 +@OptIn(ExperimentalLibraryAbiReader::class) +private fun Collection.toKlibNames(): List = + this.map(String::toAbiQualifiedName).filterNotNull() + +@OptIn(ExperimentalLibraryAbiReader::class) +internal fun String.toAbiQualifiedName(): AbiQualifiedName? { + if (this.isBlank() || this.contains('/')) return null + // Easiest part: dissect package name from the class name + val idx = this.lastIndexOf('.') + if (idx == -1) { + return AbiQualifiedName(AbiCompoundName(""), this.classNameToCompoundName()) + } else { + val packageName = this.substring(0, idx) + val className = this.substring(idx + 1) + return AbiQualifiedName(AbiCompoundName(packageName), className.classNameToCompoundName()) + } +} + +@OptIn(ExperimentalLibraryAbiReader::class) +private fun String.classNameToCompoundName(): AbiCompoundName { + if (this.isEmpty()) return AbiCompoundName(this) + + val segments = mutableListOf() + val builder = StringBuilder() + + for (idx in this.indices) { + val c = this[idx] + // Don't treat a character as a separator if: + // - it's not a '$' + // - it's at the beginning of the segment + // - it's the last character of the string + if (c != '$' || builder.isEmpty() || idx == this.length - 1) { + builder.append(c) + continue + } + check(c == '$') + // class$$$susbclass -> class.$$subclass, were at second $ here. + if (builder.last() == '$') { + builder.append(c) + continue + } + + segments.add(builder.toString()) + builder.clear() + } + if (builder.isNotEmpty()) { + segments.add(builder.toString()) + } + return AbiCompoundName(segments.joinToString(separator = ".")) +} diff --git a/src/main/kotlin/klib/KlibAbiDumpFileMerger.kt b/src/main/kotlin/klib/KlibAbiDumpFileMerger.kt index 1a005ec4..947f807c 100644 --- a/src/main/kotlin/klib/KlibAbiDumpFileMerger.kt +++ b/src/main/kotlin/klib/KlibAbiDumpFileMerger.kt @@ -8,8 +8,10 @@ package kotlinx.validation.klib import kotlinx.validation.api.klib.KlibTarget import java.io.File import java.nio.file.Files +import java.util.* +import kotlin.Comparator -internal class LinesProvider(private val lines: Iterator) : Iterator { +private class LinesProvider(private val lines: Iterator) : Iterator { private var nextLine: String? = null public fun peek(): String? { @@ -87,16 +89,20 @@ internal class KlibAbiDumpMerger { /** * All targets for which this dump contains declarations. */ - public val targets: Set = targetsMut + internal val targets: Set = targetsMut - public fun load(file: File, targetName: String? = null) { + internal fun merge(file: File, configurableTargetName: String? = null) { require(file.exists()) { "File does not exist: $file" } Files.lines(file.toPath()).use { - mergeFile(targetName, LinesProvider(it.iterator())) + merge(it.iterator(), configurableTargetName) } } - private fun mergeFile(targetName: String?, lines: LinesProvider) { + internal fun merge(lines: Iterator, configurableTargetName: String? = null) { + merge(LinesProvider(lines), configurableTargetName) + } + + private fun merge(lines: LinesProvider, configurableTargetName: String?) { val isMergedFile = lines.determineFileType() val aliases = mutableMapOf>() @@ -104,13 +110,13 @@ internal class KlibAbiDumpMerger { if (isMergedFile) { lines.next() // skip the heading line bcvTargets.addAll(lines.parseTargets()) - check(bcvTargets.size == 1 || targetName == null) { + check(bcvTargets.size == 1 || configurableTargetName == null) { "Can't use an explicit target name with a multi-target dump. " + - "targetName: $targetName, dump targets: $bcvTargets" + "targetName: $configurableTargetName, dump targets: $bcvTargets" } aliases.putAll(lines.parseAliases()) } - val header = lines.parseFileHeader(isMergedFile, targetName) + val header = lines.parseFileHeader(isMergedFile, configurableTargetName) bcvTargets.addAll(header.underlyingTargets) bcvTargets.intersect(targets).also { check(it.isEmpty()) { "This dump and a file to merge share some targets: $it" } @@ -134,16 +140,21 @@ internal class KlibAbiDumpMerger { // and we must pop one or several declarations out of the parsing stack. var currentContainer = topLevelDeclaration var depth = -1 + val targetsStack = Stack>().apply { push(bcvTargets) } while (lines.hasNext()) { val line = lines.peek()!! + if (line.isEmpty()) { lines.next(); continue } // TODO: wrap the line and cache the depth inside that wrapper? val lineDepth = line.depth() when { // The depth is the same as before; we encountered a sibling depth == lineDepth -> { + // pop it off to swap previous value from the same depth, + // parseDeclaration will update it + targetsStack.pop() currentContainer = - lines.parseDeclaration(lineDepth, currentContainer.parent!!, bcvTargets, isMergedFile, aliases) + lines.parseDeclaration(lineDepth, currentContainer.parent!!, targetsStack, aliases) } // The depth is increasing; that means we encountered child declaration depth < lineDepth -> { @@ -152,7 +163,7 @@ internal class KlibAbiDumpMerger { "previous: ${currentContainer.text}" } currentContainer = - lines.parseDeclaration(lineDepth, currentContainer, bcvTargets, isMergedFile, aliases) + lines.parseDeclaration(lineDepth, currentContainer, targetsStack, aliases) depth = lineDepth } // Otherwise, we're finishing all the declaration with greater depth compared to the depth of @@ -162,6 +173,7 @@ internal class KlibAbiDumpMerger { else -> { while (currentContainer.text.depth() > lineDepth) { currentContainer = currentContainer.parent!! + targetsStack.pop() } // If the line is '}' - add it as a terminator to the corresponding declaration, it'll simplify // dumping the merged file back to text format. @@ -314,32 +326,24 @@ internal class KlibAbiDumpMerger { private fun LinesProvider.parseDeclaration( depth: Int, parent: DeclarationContainer, - allTargets: Set, - isMergedFile: Boolean, + parentTargetsStack: Stack>, aliases: Map> ): DeclarationContainer { val line = peek()!! return if (line.startsWith(" ".repeat(depth * INDENT_WIDTH) + TARGETS_LIST_PREFIX)) { - check(isMergedFile) { - "Targets declaration should only be a part of merged file, " + - "and the current file claimed to be a regular dump file:\n$line" - } next() // skip prefix - // Target list means that the declaration following it has a narrower set of targets then its parent, + // Target list means that the declaration following it has a narrower set of targets than its parent, // so we must use it. val targets = parseBcvTargetsLine(line) val expandedTargets = targets.flatMap { aliases[it.configurableName] ?: listOf(it) }.toSet() + parentTargetsStack.push(expandedTargets) parent.createOrUpdateChildren(next(), expandedTargets) } else { - // That's an ugly part: - // - for a merged file (isMergedFile==true) we need to use parent declaration targets: if we're in this - // branch, no explicit targets were specified, and new declaration targets should be the same as targets - // of its parent. We can't use allTargets here, as parent may have a more specific set of targets. - // - for a single klib dump file, we need to specify the exact target associated with this file and allTargets - // must contain exactly one value here. - parent.createOrUpdateChildren(next(), if (isMergedFile) parent.targets else allTargets) + // Inherit all targets from a parent + parentTargetsStack.push(parentTargetsStack.peek()) + parent.createOrUpdateChildren(next(), parentTargetsStack.peek()) } } @@ -426,6 +430,30 @@ internal class KlibAbiDumpMerger { topLevelDeclaration.mergeTargetSpecific(other.topLevelDeclaration) } + /** + * Merges other [KlibAbiDumpMerger] into this one. + */ + fun merge(other: KlibAbiDumpMerger) { + if (other.targets.isEmpty()) return + + targets.intersect(other.targets).also { + require(it.isEmpty()) { + "Targets of this dump and the dump to merge into it should not intersect. Common targets: $it" + } + } + if (headerContent != other.headerContent) { + // the dump was empty + if (headerContent.isEmpty() && targets.isEmpty()) { + headerContent.addAll(other.headerContent) + } else { + throw IllegalArgumentException("Dumps headers does not match") + } + } + + targetsMut.addAll(other.targetsMut) + topLevelDeclaration.merge(other.topLevelDeclaration) + } + /** * For each declaration change targets to a specified [targets] set. */ @@ -529,6 +557,30 @@ internal class DeclarationContainer(val text: String, val parent: DeclarationCon } } + fun merge(other: DeclarationContainer) { + targets.addAll(other.targets) + val parent = this + other.children.forEach { (line, decl) -> + children.compute(line) { _, thisDecl -> + if (thisDecl == null) { + decl.deepCopy(parent) + } else { + thisDecl.apply { merge(decl) } + } + } + } + } + + fun deepCopy(parent: DeclarationContainer): DeclarationContainer { + val copy = DeclarationContainer(this.text, parent) + copy.delimiter = delimiter + copy.targets.addAll(targets) + children.forEach { key, value -> + copy.children[key] = value.deepCopy(copy) + } + return copy + } + private fun addTargetRecursively(first: KlibTarget) { targets.add(first) children.forEach { it.value.addTargetRecursively(first) } diff --git a/src/test/kotlin/tests/ClassNameConvertionTest.kt b/src/test/kotlin/tests/ClassNameConvertionTest.kt index fe556762..3dcd28e1 100644 --- a/src/test/kotlin/tests/ClassNameConvertionTest.kt +++ b/src/test/kotlin/tests/ClassNameConvertionTest.kt @@ -7,7 +7,7 @@ package kotlinx.validation.api.tests -import kotlinx.validation.toAbiQualifiedName +import kotlinx.validation.api.klib.toAbiQualifiedName import org.jetbrains.kotlin.library.abi.* import org.junit.Test import kotlin.test.assertEquals diff --git a/src/test/kotlin/tests/KlibAbiMergingTest.kt b/src/test/kotlin/tests/KlibAbiMergingTest.kt index d1594f2f..16424b92 100644 --- a/src/test/kotlin/tests/KlibAbiMergingTest.kt +++ b/src/test/kotlin/tests/KlibAbiMergingTest.kt @@ -47,8 +47,8 @@ class KlibAbiMergingTest { @Test fun identicalDumpFiles() { val klib = KlibAbiDumpMerger() - klib.load(file("/merge/identical/dump_macos_arm64.abi")) - klib.load(file("/merge/identical/dump_linux_x64.abi")) + klib.merge(file("/merge/identical/dump_macos_arm64.abi")) + klib.merge(file("/merge/identical/dump_linux_x64.abi")) val merged = dumpToFile(klib) assertContentEquals( @@ -60,8 +60,8 @@ class KlibAbiMergingTest { @Test fun identicalDumpFilesWithAliases() { val klib = KlibAbiDumpMerger() - klib.load(file("/merge/identical/dump_macos_arm64.abi")) - klib.load(file("/merge/identical/dump_linux_x64.abi")) + klib.merge(file("/merge/identical/dump_macos_arm64.abi")) + klib.merge(file("/merge/identical/dump_linux_x64.abi")) val merged = dumpToFile(klib) // there are no groups other than "all", so no aliases will be added @@ -79,7 +79,7 @@ class KlibAbiMergingTest { val klib = KlibAbiDumpMerger() targets.shuffle(random) targets.forEach { - klib.load(file("/merge/diverging/$it.api")) + klib.merge(file("/merge/diverging/$it.api")) } val merged = dumpToFile(klib) assertContentEquals( @@ -98,7 +98,7 @@ class KlibAbiMergingTest { val targets = mutableListOf("androidNativeArm64", "linuxArm64", "linuxX64", "tvosX64") targets.shuffle(random) targets.forEach { - klib.load(file("/merge/diverging/$it.api")) + klib.merge(file("/merge/diverging/$it.api")) } val merged = dumpToFile(klib) assertContentEquals( @@ -111,22 +111,22 @@ class KlibAbiMergingTest { @Test fun mergeDumpsWithDivergedHeaders() { val klib = KlibAbiDumpMerger() - klib.load(file("/merge/header-mismatch/v1.abi"), "linuxArm64") + klib.merge(file("/merge/header-mismatch/v1.abi"), "linuxArm64") assertFailsWith { - klib.load(file("/merge/header-mismatch/v2.abi"), "linuxX64") + klib.merge(file("/merge/header-mismatch/v2.abi"), "linuxX64") } } @Test fun overwriteAll() { val klib = KlibAbiDumpMerger() - klib.load(file("/merge/diverging/merged.abi")) + klib.merge(file("/merge/diverging/merged.abi")) val targets = listOf("androidNativeArm64", "linuxArm64", "linuxX64", "tvosX64") targets.forEach { target -> klib.remove(KlibTarget(target)) - klib.load(file("/merge/diverging/$target.api")) + klib.merge(file("/merge/diverging/$target.api")) } val merged = dumpToFile(klib) @@ -140,7 +140,7 @@ class KlibAbiMergingTest { @Test fun read() { val klib = KlibAbiDumpMerger() - klib.load(file("/merge/idempotent/bcv-klib-test.abi")) + klib.merge(file("/merge/idempotent/bcv-klib-test.abi")) val written = dumpToFile(klib) assertContentEquals( @@ -152,7 +152,7 @@ class KlibAbiMergingTest { @Test fun readDeclarationWithNarrowerChildrenDeclarations() { val klib = KlibAbiDumpMerger() - klib.load(file("/merge/parseNarrowChildrenDecls/merged.abi")) + klib.merge(file("/merge/parseNarrowChildrenDecls/merged.abi")) klib.remove(KlibTarget("linuxArm64")) val written1 = dumpToFile(klib) @@ -172,7 +172,7 @@ class KlibAbiMergingTest { @Test fun guessAbi() { val klib = KlibAbiDumpMerger() - klib.load(file("/merge/guess/merged.api")) + klib.merge(file("/merge/guess/merged.api")) klib.retainTargetSpecificAbi(KlibTarget("linuxArm64")) val retainedLinuxAbiDump = dumpToFile(klib) @@ -182,7 +182,7 @@ class KlibAbiMergingTest { ) val commonAbi = KlibAbiDumpMerger() - commonAbi.load(file("/merge/guess/merged.api")) + commonAbi.merge(file("/merge/guess/merged.api")) commonAbi.remove(KlibTarget("linuxArm64")) commonAbi.retainCommonAbi() @@ -205,29 +205,29 @@ class KlibAbiMergingTest { @Test fun loadInvalidFile() { assertFails { - KlibAbiDumpMerger().load(file("/merge/illegalFiles/emptyFile.txt")) + KlibAbiDumpMerger().merge(file("/merge/illegalFiles/emptyFile.txt")) } assertFails { - KlibAbiDumpMerger().load(file("/merge/illegalFiles/nonDumpFile.txt")) + KlibAbiDumpMerger().merge(file("/merge/illegalFiles/nonDumpFile.txt")) } assertFails { - KlibAbiDumpMerger().load(file("/merge/illegalFiles/emptyFile.txt"), "linuxX64") + KlibAbiDumpMerger().merge(file("/merge/illegalFiles/emptyFile.txt"), "linuxX64") } assertFails { - KlibAbiDumpMerger().load(file("/merge/illegalFiles/nonDumpFile.txt"), "linuxX64") + KlibAbiDumpMerger().merge(file("/merge/illegalFiles/nonDumpFile.txt"), "linuxX64") } assertFails { // Not a single-target dump - KlibAbiDumpMerger().load(file("/merge/diverging/merged.api"), "linuxX64") + KlibAbiDumpMerger().merge(file("/merge/diverging/merged.api"), "linuxX64") } assertFails { KlibAbiDumpMerger().apply { - load(file("/merge/stdlib_native_common.abi"), "linuxArm64") + merge(file("/merge/stdlib_native_common.abi"), "linuxArm64") } } } @@ -235,9 +235,9 @@ class KlibAbiMergingTest { @Test fun webTargets() { val klib = KlibAbiDumpMerger() - klib.load(file("/merge/webTargets/js.abi")) - klib.load(file("/merge/webTargets/wasmWasi.abi"), "wasmWasi") - klib.load(file("/merge/webTargets/wasmJs.abi"), "wasmJs") + klib.merge(file("/merge/webTargets/js.abi")) + klib.merge(file("/merge/webTargets/wasmWasi.abi"), "wasmWasi") + klib.merge(file("/merge/webTargets/wasmJs.abi"), "wasmJs") val merged = dumpToFile(klib) @@ -251,29 +251,29 @@ class KlibAbiMergingTest { fun unqualifiedWasmTarget() { // currently, there's no way to distinguish wasmWasi from wasmJs assertFailsWith { - KlibAbiDumpMerger().load(file("/merge/webTargets/wasmWasi.abi")) + KlibAbiDumpMerger().merge(file("/merge/webTargets/wasmWasi.abi")) } } @Test fun intersectingTargets() { val dump = KlibAbiDumpMerger().apply { - load(file("/merge/diverging/merged.abi")) + merge(file("/merge/diverging/merged.abi")) } assertFailsWith { - dump.load(file("/merge/diverging/linuxArm64.api")) + dump.merge(file("/merge/diverging/linuxArm64.api")) } // but here, we're loading a dump for different target (configuredName changed) - dump.load(file("/merge/diverging/linuxArm64.api"), "custom") + dump.merge(file("/merge/diverging/linuxArm64.api"), "custom") } @Test fun customTargetNames() { val lib = KlibAbiDumpMerger().apply { - load(file("/merge/diverging/androidNativeArm64.api"), "android") - load(file("/merge/diverging/linuxArm64.api"), "linux") - load(file("/merge/diverging/linuxX64.api")) - load(file("/merge/diverging/tvosX64.api")) + merge(file("/merge/diverging/androidNativeArm64.api"), "android") + merge(file("/merge/diverging/linuxArm64.api"), "linux") + merge(file("/merge/diverging/linuxX64.api")) + merge(file("/merge/diverging/tvosX64.api")) } val dump = dumpToFile(lib) @@ -286,7 +286,7 @@ class KlibAbiMergingTest { @Test fun customTargetExtraction() { val lib = KlibAbiDumpMerger().apply { - load(file("/merge/diverging/merged_with_aliases_and_custom_names.abi")) + merge(file("/merge/diverging/merged_with_aliases_and_custom_names.abi")) } val targets = lib.targets.filter { it.targetName != "linuxArm64" } targets.forEach { lib.remove(it) } @@ -302,7 +302,7 @@ class KlibAbiMergingTest { val mergedPath = "/merge/webTargets/merged.abi" fun checkExtracted(targetName: String, expectedFile: String) { - val lib = KlibAbiDumpMerger().apply { load(file(mergedPath)) } + val lib = KlibAbiDumpMerger().apply { merge(file(mergedPath)) } val targets = lib.targets targets.filter { it.configurableName != targetName }.forEach { lib.remove(it) } val dump = dumpToFile(lib) @@ -321,7 +321,7 @@ class KlibAbiMergingTest { @Test fun loadMultiTargetDump() { val lib = KlibAbiDumpMerger().apply { - load(file("/merge/stdlib_native_common.abi")) + merge(file("/merge/stdlib_native_common.abi")) } val expectedTargetNames = listOf( "androidNativeArm32", "androidNativeArm64", "androidNativeX64", "androidNativeX86", From 28cc85a9af0a3a1b9a69ccbbbf1c76273a64a2bd Mon Sep 17 00:00:00 2001 From: Filipp Zhinkin Date: Thu, 7 Mar 2024 17:50:17 +0100 Subject: [PATCH 38/53] Improved kdoc and added samples --- src/main/kotlin/api/klib/KlibDump.kt | 129 ++++++++- src/test/kotlin/samples/KlibDumpSamples.kt | 290 +++++++++++++++++++++ 2 files changed, 406 insertions(+), 13 deletions(-) create mode 100644 src/test/kotlin/samples/KlibDumpSamples.kt diff --git a/src/main/kotlin/api/klib/KlibDump.kt b/src/main/kotlin/api/klib/KlibDump.kt index 22139725..1c587cc0 100644 --- a/src/main/kotlin/api/klib/KlibDump.kt +++ b/src/main/kotlin/api/klib/KlibDump.kt @@ -10,30 +10,56 @@ import kotlinx.validation.klib.KlibAbiDumpMerger import java.io.File import java.io.FileNotFoundException +/** + * Represents KLib ABI dump and allows manipulating it. + */ @ExperimentalBCVApi public class KlibDump { internal var merger: KlibAbiDumpMerger = KlibAbiDumpMerger() /** - * Set of targets for which this dump contains declarations. + * Set of all targets for which this dump contains declarations. + * + * @sample samples.KlibDumpSamples.extractTargets */ public val targets: Set get() = merger.targets /** - * Load a text dump for a target and merge it into this dump. + * Loads a textual KLib dump and merges it into this dump. + * + * If a dump contains only a single target, it's possible to specify a custom configurable target name. + * Please refer to [KlibTarget.configurableName] for more details on the meaning of that name. + * + * By default, [configurableTargetName] is null and information about a target will be taken directly from + * the loaded dump. + * + * It's an error to specify non-null [configurableTargetName] for a dump containing multiple targets. + * It's also an error to merge dumps having some targets in common. * - * @throws IllegalArgumentException if the dump already has that target. - * @throws FileNotFoundException if [dump] does not exist. + * @throws IllegalArgumentException if this dump and [dumpFile] shares same targets. + * @throws IllegalArgumentException if [dumpFile] contains multiple targets + * and [configurableTargetName] is not null. + * @throws FileNotFoundException if [dumpFile] does not exist. + * + * @sample samples.KlibDumpSamples.mergeDumps */ - public fun merge(from: File, withTargetName: String? = null) { - if (!from.exists()) throw FileNotFoundException(from.absolutePath) - merger.merge(from, withTargetName) + public fun merge(dumpFile: File, configurableTargetName: String? = null) { + if (!dumpFile.exists()) throw FileNotFoundException(dumpFile.absolutePath) + merger.merge(dumpFile, configurableTargetName) } /** - * Merge [other] dump into this one. + * Merges [other] dump with this one. + * + * It's also an error to merge dumps having some targets in common. + * + * The operation does not modify [other]. + * + * @throws IllegalArgumentException if this dump and [other] shares same targets. + * + * @sample samples.KlibDumpSamples.mergeDumpObjects */ public fun merge(other: KlibDump) { val intersection = targets.intersect(other.targets) @@ -44,7 +70,11 @@ public class KlibDump { } /** - * Remove all declarations that do not belong to the specified targets. + * Remove all declarations that do not belong to specified targets and remove these targets from the dump. + * + * All targets not stored within a dump will be ignored. + * + * @sample samples.KlibDumpSamples.extractTargets */ public fun retain(targets: Iterable) { val toRemove = merger.targets.subtract(targets.toSet()) @@ -52,7 +82,11 @@ public class KlibDump { } /** - * Remove all declarations that do belong to the specified target. + * Remove all declarations that do belong to specified targets and remove these targets from the dump. + * + * All targets not stored within a dump will be ignored. + * + * @sample samples.KlibDumpSamples.mergeDumpObjects */ public fun remove(targets: Iterable) { targets.forEach { @@ -60,20 +94,61 @@ public class KlibDump { } } + /** + * Creates a copy of this dump. + */ public fun copy(): KlibDump = KlibDump().also { it.merge(this) } /** - * Convert the dump back into a textual form. + * Serializes the dump and writes it to [to]. + * + * @sample samples.KlibDumpSamples.mergeDumps */ public fun saveTo(to: Appendable) { merger.dump(to) } public companion object { + /** + * Loads a dump from a textual form. + * + * If a dump contains only a single target, it's possible to specify a custom configurable target name. + * Please refer to [KlibTarget.configurableName] for more details on the meaning of that name. + * + * By default, [configurableTargetName] is null and information about a target will be taken directly from + * the loaded dump. + * + * It's an error to specify non-null [configurableTargetName] for a dump containing multiple targets. + * + * @throws IllegalArgumentException if [dumpFile] contains multiple targets + * and [configurableTargetName] is not null. + * @throws FileNotFoundException if [dumpFile] does not exist. + * + * @sample samples.KlibDumpSamples.mergeDumpObjects + */ public fun from(dumpFile: File, configurableTargetName: String? = null): KlibDump { check(dumpFile.exists()) { "File does not exist: ${dumpFile.absolutePath}" } return KlibDump().apply { merge(dumpFile, configurableTargetName) } } + + /** + * Dumps a public ABI of a klib represented by [klibFile] using [filters] + * and returns a [KlibDump] representing it. + * + * To control which declarations are dumped, [filters] could be used. By default, no filter will be applied. + * + * If a klib contains only a single target, it's possible to specify a custom configurable target name. + * Please refer to [KlibTarget.configurableName] for more details on the meaning of that name. + * + * By default, [configurableTargetName] is null and information about a target will be taken directly from + * the klib. + * + * It's an error to specify non-null [configurableTargetName] for a klib containing multiple targets. + * + * @throws IllegalArgumentException if [klibFile] contains multiple targets + * and [configurableTargetName] is not null. + * @throws FileNotFoundException if [klibFile] does not exist. + */ public fun fromKlib( klibFile: File, configurableTargetName: String? = null, @@ -90,8 +165,17 @@ public class KlibDump { } /** - * Infer a possible public ABI for [target] using old merged dump [oldDump] - * and set of [generatedDumps] generated for other targets. + * Infer a possible public ABI for [unsupportedTarget] as an ABI common across all [supportedTargetDumps]. + * If there's an [oldMergedDump] consisting of declarations of multiple targets, including [unsupportedTarget], + * a portion of that dump specific to the [unsupportedTarget] will be extracted and merged to the common ABI + * build from [supportedTargetDumps]. + * + * Returned dump contains only declarations for [unsupportedTarget]. + * + * The function aimed to facilitate ABI dumps generation for targets that are not supported by a host compiler. + * In practice, it means generating dumps for Apple targets on non-Apple hosts. + * + * @sample samples.KlibDumpSamples.inferDump */ @ExperimentalBCVApi public fun inferAbi( @@ -117,6 +201,25 @@ public fun inferAbi( return commonDump } +/** + * Dumps a public ABI of a klib represented by [klibFile] using [filters] and merges it into this dump. + * + * To control which declarations are dumped, [filters] could be used. By default, no filter will be applied. + * + * If a klib contains only a single target, it's possible to specify a custom configurable target name. + * Please refer to [KlibTarget.configurableName] for more details on the meaning of that name. + * + * By default, [configurableTargetName] is null and information about a target will be taken directly from + * the klib. + * + * It's an error to specify non-null [configurableTargetName] for a klib containing multiple targets. + * It's also an error to merge dumps having some targets in common. + * + * @throws IllegalArgumentException if this dump and [klibFile] shares same targets. + * @throws IllegalArgumentException if [klibFile] contains multiple targets + * and [configurableTargetName] is not null. + * @throws FileNotFoundException if [klibFile] does not exist. + */ @ExperimentalBCVApi public fun KlibDump.mergeKlib( klibFile: File, configurableTargetName: String? = null, diff --git a/src/test/kotlin/samples/KlibDumpSamples.kt b/src/test/kotlin/samples/KlibDumpSamples.kt new file mode 100644 index 00000000..74f23782 --- /dev/null +++ b/src/test/kotlin/samples/KlibDumpSamples.kt @@ -0,0 +1,290 @@ +/* + * Copyright 2016-2024 JetBrains s.r.o. + * Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file. + */ + +package samples + +import kotlinx.validation.ExperimentalBCVApi +import kotlinx.validation.api.klib.KlibDump +import kotlinx.validation.api.klib.KlibTarget +import kotlinx.validation.api.klib.inferAbi +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TemporaryFolder +import java.io.File +import kotlin.test.assertEquals + +class KlibDumpSamples { + @JvmField + @Rule + var tempFolder = TemporaryFolder() + + fun createDumpWithContent(content: String): File { + val file = tempFolder.newFile() + file.writer().use { + it.write(content) + } + return file + } + + @OptIn(ExperimentalBCVApi::class) + @Test + fun mergeDumps() { + val linuxX64Dump = createDumpWithContent(""" + // Rendering settings: + // - Signature version: 2 + // - Show manifest properties: false + // - Show declarations: true + + // Library unique name: + // Platform: NATIVE + // Native targets: linux_x64 + // Compiler version: 1.9.22 + // ABI version: 1.8.0 + final class org.example/ShardedClass { // org.example/ShardedClass|null[0] + final val value // org.example/ShardedClass.value|{}value[0] + final fun (): kotlin/Int // org.example/ShardedClass.value.|(){}[0] + constructor (kotlin/Int) // org.example/ShardedClass.|(kotlin.Int){}[0] + final fun add(kotlin/Int): kotlin/Int // org.example/ShardedClass.add|add(kotlin.Int){}[0] + } + final fun org.example/ShardedClass(kotlin/Int, kotlin/Float, kotlin/Long): org.example/ShardedClass // org.example/ShardedClass|ShardedClass(kotlin.Int;kotlin.Float;kotlin.Long){}[0] + """.trimIndent()) + + val linuxArm64Dump = createDumpWithContent(""" + // Rendering settings: + // - Signature version: 2 + // - Show manifest properties: false + // - Show declarations: true + + // Library unique name: + // Platform: NATIVE + // Native targets: linux_arm64 + // Compiler version: 1.9.22 + // ABI version: 1.8.0 + final class org.example/ShardedClass { // org.example/ShardedClass|null[0] + final val value // org.example/ShardedClass.value|{}value[0] + final fun (): kotlin/Int // org.example/ShardedClass.value.|(){}[0] + constructor (kotlin/Int) // org.example/ShardedClass.|(kotlin.Int){}[0] + final fun add(kotlin/Int): kotlin/Int // org.example/ShardedClass.add|add(kotlin.Int){}[0] + } + """.trimIndent()) + + val mergedDump = KlibDump().apply { + merge(linuxX64Dump) + merge(linuxArm64Dump) + } + val mergedDumpContent = buildString { mergedDump.saveTo(this) } + + assertEquals(""" + // KLib ABI Dump + // Targets: [linuxArm64, linuxX64] + // Rendering settings: + // - Signature version: 2 + // - Show manifest properties: false + // - Show declarations: true + + // Library unique name: + final class org.example/ShardedClass { // org.example/ShardedClass|null[0] + constructor (kotlin/Int) // org.example/ShardedClass.|(kotlin.Int){}[0] + final fun add(kotlin/Int): kotlin/Int // org.example/ShardedClass.add|add(kotlin.Int){}[0] + final val value // org.example/ShardedClass.value|{}value[0] + final fun (): kotlin/Int // org.example/ShardedClass.value.|(){}[0] + } + // Targets: [linuxX64] + final fun org.example/ShardedClass(kotlin/Int, kotlin/Float, kotlin/Long): org.example/ShardedClass // org.example/ShardedClass|ShardedClass(kotlin.Int;kotlin.Float;kotlin.Long){}[0] + + """.trimIndent(), mergedDumpContent) + } + + @OptIn(ExperimentalBCVApi::class) + @Test + fun mergeDumpObjects() { + val linuxX64Dump = createDumpWithContent(""" + // Rendering settings: + // - Signature version: 2 + // - Show manifest properties: false + // - Show declarations: true + + // Library unique name: + // Platform: NATIVE + // Native targets: linux_x64 + // Compiler version: 1.9.22 + // ABI version: 1.8.0 + final class org.example/ShardedClass { // org.example/ShardedClass|null[0] + final val value // org.example/ShardedClass.value|{}value[0] + final fun (): kotlin/Int // org.example/ShardedClass.value.|(){}[0] + constructor (kotlin/Int) // org.example/ShardedClass.|(kotlin.Int){}[0] + final fun add(kotlin/Int): kotlin/Int // org.example/ShardedClass.add|add(kotlin.Int){}[0] + } + final fun org.example/ShardedClass(kotlin/Int, kotlin/Float, kotlin/Long): org.example/ShardedClass // org.example/ShardedClass|ShardedClass(kotlin.Int;kotlin.Float;kotlin.Long){}[0] + """.trimIndent()) + + val linuxArm64Dump = createDumpWithContent(""" + // Rendering settings: + // - Signature version: 2 + // - Show manifest properties: false + // - Show declarations: true + + // Library unique name: + // Platform: NATIVE + // Native targets: linux_arm64 + // Compiler version: 1.9.22 + // ABI version: 1.8.0 + final class org.example/ShardedClass { // org.example/ShardedClass|null[0] + final val value // org.example/ShardedClass.value|{}value[0] + final fun (): kotlin/Int // org.example/ShardedClass.value.|(){}[0] + constructor (kotlin/Int) // org.example/ShardedClass.|(kotlin.Int){}[0] + final fun add(kotlin/Int): kotlin/Int // org.example/ShardedClass.add|add(kotlin.Int){}[0] + } + """.trimIndent()) + + val mergedDump = KlibDump() + mergedDump.merge(KlibDump.from(linuxArm64Dump)) + mergedDump.merge(KlibDump.from(linuxX64Dump, configurableTargetName = "linuxX86_64")) + val mergedDumpContent = buildString { mergedDump.saveTo(this) } + + assertEquals(""" + // KLib ABI Dump + // Targets: [linuxArm64, linuxX64.linuxX86_64] + // Rendering settings: + // - Signature version: 2 + // - Show manifest properties: false + // - Show declarations: true + + // Library unique name: + final class org.example/ShardedClass { // org.example/ShardedClass|null[0] + constructor (kotlin/Int) // org.example/ShardedClass.|(kotlin.Int){}[0] + final fun add(kotlin/Int): kotlin/Int // org.example/ShardedClass.add|add(kotlin.Int){}[0] + final val value // org.example/ShardedClass.value|{}value[0] + final fun (): kotlin/Int // org.example/ShardedClass.value.|(){}[0] + } + // Targets: [linuxX64.linuxX86_64] + final fun org.example/ShardedClass(kotlin/Int, kotlin/Float, kotlin/Long): org.example/ShardedClass // org.example/ShardedClass|ShardedClass(kotlin.Int;kotlin.Float;kotlin.Long){}[0] + + """.trimIndent(), mergedDumpContent) + + mergedDump.remove(listOf(KlibTarget.parse("linuxX64.linuxX86_64"))) + val filteredDumpContent = buildString { mergedDump.saveTo(this) } + assertEquals(""" + // KLib ABI Dump + // Targets: [linuxArm64] + // Rendering settings: + // - Signature version: 2 + // - Show manifest properties: false + // - Show declarations: true + + // Library unique name: + final class org.example/ShardedClass { // org.example/ShardedClass|null[0] + constructor (kotlin/Int) // org.example/ShardedClass.|(kotlin.Int){}[0] + final fun add(kotlin/Int): kotlin/Int // org.example/ShardedClass.add|add(kotlin.Int){}[0] + final val value // org.example/ShardedClass.value|{}value[0] + final fun (): kotlin/Int // org.example/ShardedClass.value.|(){}[0] + } + + """.trimIndent(), filteredDumpContent) + } + + @OptIn(ExperimentalBCVApi::class) + @Test + fun extractTargets() { + // Oh no, we're running on Windows and Apple targets are unsupported, let's filter it out! + val mergedDump = createDumpWithContent(""" + // KLib ABI Dump + // Targets: [iosArm64, iosSimulatorArm64, iosX64, linuxArm64, linuxX64] + // Rendering settings: + // - Signature version: 2 + // - Show manifest properties: true + // - Show declarations: true + + // Library unique name: + abstract class examples.classes/Klass // examples.classes/Klass|null[0] + // Targets: [iosArm64, iosX64] + abstract interface examples.classes/Iface // examples.classes/Iface|null[0] + + """.trimIndent()) + + val dump = KlibDump.from(mergedDump) + assertEquals( + listOf("iosArm64", "iosSimulatorArm64", "iosX64", "linuxArm64", "linuxX64").map(KlibTarget::parse).toSet(), + dump.targets + ) + // remove everything except linux* + dump.retain(dump.targets.filter { it.targetName.startsWith("linux") }) + + val filteredDumpContent = buildString { dump.saveTo(this) } + assertEquals(""" + // KLib ABI Dump + // Targets: [linuxArm64, linuxX64] + // Rendering settings: + // - Signature version: 2 + // - Show manifest properties: true + // - Show declarations: true + + // Library unique name: + abstract class examples.classes/Klass // examples.classes/Klass|null[0] + + """.trimIndent(), + filteredDumpContent) + } + + @OptIn(ExperimentalBCVApi::class) + @Test + fun inferDump() { + // We want to get a dump for iosArm64, but our host compiler doesn't support it. + val unsupportedTarget = KlibTarget.parse("iosArm64") + // Thankfully, we have an old merged dump ... + val oldMergedDump = createDumpWithContent(""" + // KLib ABI Dump + // Targets: [iosArm64, linuxArm64] + // Rendering settings: + // - Signature version: 2 + // - Show manifest properties: true + // - Show declarations: true + + // Library unique name: + abstract class examples.classes/Klass // examples.classes/Klass|null[0] + // Targets: [iosArm64] + abstract interface examples.classes/Iface // examples.classes/Iface|null[0] + + """.trimIndent()) + + // ... and a new dump for linuxArm64 + val linuxDump = createDumpWithContent(""" + // KLib ABI Dump + // Targets: [linuxArm64] + // Rendering settings: + // - Signature version: 2 + // - Show manifest properties: true + // - Show declarations: true + + // Library unique name: + abstract class examples.classes/NewKlass // examples.classes/NewKlass|null[0] + + """.trimIndent()) + + // Let's use these dumps to infer a public ABI on iosArm64 + val inferredArm64Dump = inferAbi( + unsupportedTarget = unsupportedTarget, + supportedTargetDumps = listOf(KlibDump.from(linuxDump)), + oldMergedDump = KlibDump.from(oldMergedDump)) + + assertEquals(unsupportedTarget, inferredArm64Dump.targets.single()) + + val inferredDumpContent = buildString { inferredArm64Dump.saveTo(this) } + assertEquals(""" + // KLib ABI Dump + // Targets: [iosArm64] + // Rendering settings: + // - Signature version: 2 + // - Show manifest properties: true + // - Show declarations: true + + // Library unique name: + abstract class examples.classes/NewKlass // examples.classes/NewKlass|null[0] + abstract interface examples.classes/Iface // examples.classes/Iface|null[0] + + """.trimIndent(), + inferredDumpContent) + } +} From 68ce7af490360754a9e171e3ce5981591678ebef Mon Sep 17 00:00:00 2001 From: Filipp Zhinkin Date: Thu, 7 Mar 2024 18:35:46 +0100 Subject: [PATCH 39/53] Update README --- README.md | 53 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/README.md b/README.md index 22c1ff99..4dab4c63 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,7 @@ The tool allows dumping binary API of a JVM part of a Kotlin library that is pub * [Tasks](#tasks) * [Optional parameters](#optional-parameters) * [Workflow](#workflow) + * [Experimental KLib ABI validation support](#experimental-klib-abi-validation-support) * [What constitutes the public API](#what-constitutes-the-public-api) * [Classes](#classes) * [Members](#members) @@ -175,6 +176,58 @@ When starting to validate your library public API, we recommend the following wo the resulting diff in `.api` file should be verified: only signatures you expected to change should be changed. * Commit the resulting `.api` diff along with code changes. +### Experimental KLib ABI validation support + +The KLib validation support is experimental and is a subject to change (applies to both an API and the ABI dump format). + +To validate public ABI of a Kotlin library (KLib) corresponding option should be enabled explicitly: +```kotlin +apiValidation { + @OptIn(kotlinx.validation.ExperimentalBCVApi::class) + klib { + enabled = true + } +} +``` + +When enabled, KLib support adds additional dependencies to existing `apiDump` and `apiCheck` tasks. +Generate KLib ABI dumps are places alongside JVM dumps (in `api` subfolder, by default) +in files named `.klib.api`. +The dump file combines all dumps generated for individual targets with declarations specific to some targets being +annotated with corresponding target names. +During the validation phase, that file is compared to the dump extracted from the latest version of the library, +and any differences between these two files are reported as errors. + +Currently, all options described in [Optional parameters](#optional-parameters) section are supported for klibs too. +The only caveat here is that all class names should be specified in the JVM-format, +like `package.name.ClassName$SubclassName`. + +Please refer to a [design document](docs/design/KLibSupport.md) for details on the format and rationale behind the +current implementation. + +#### KLib ABI dump generation and validation on Linux and Windows hosts + +Currently, compilation to Apple-specific targets (like `iosArm64` or `watchosX86`) supported only on Apple hosts. +To ease the development on Windows and Linux hosts, binary compatibility validator does not validate ABI for targets +not supported on the current host, even if `.klib.api` file contains declarations for these targets. + +This behavior could be altered to force an error when klibs for some targets could not be compiled: +```kotlin +apiValidation { + @OptIn(kotlinx.validation.ExperimentalBCVApi::class) + klib { + enabled = true + // treat a target being unsupported on a host as an error + strictValidation = true + } +} +``` + +When it comes to dump generation (`apiDump` task) on non-Apple hosts, binary compatibility validator attempts +to infer an ABI from dumps generated for supported targets and an old dump from project's `api` folder (if any). +Inferred dump may not match an actual dump, +and it is recommended to update a dump on hosts supporting all required targets, if possible. + # What constitutes the public API ### Classes From 4f4512e3b78e9e9d93cdb6b71747c567c1c698a3 Mon Sep 17 00:00:00 2001 From: Filipp Zhinkin Date: Thu, 7 Mar 2024 20:43:37 +0100 Subject: [PATCH 40/53] Update docs --- README.md | 1 + src/main/kotlin/api/klib/KlibDump.kt | 36 +++++++++++++++++++++++--- src/main/kotlin/api/klib/KlibTarget.kt | 31 +++++++++++++++------- 3 files changed, 56 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 4dab4c63..d1555bb1 100644 --- a/README.md +++ b/README.md @@ -179,6 +179,7 @@ When starting to validate your library public API, we recommend the following wo ### Experimental KLib ABI validation support The KLib validation support is experimental and is a subject to change (applies to both an API and the ABI dump format). +A project has to use Kotlin 1.9.20 or newer to use this feature. To validate public ABI of a Kotlin library (KLib) corresponding option should be enabled explicitly: ```kotlin diff --git a/src/main/kotlin/api/klib/KlibDump.kt b/src/main/kotlin/api/klib/KlibDump.kt index 1c587cc0..7995b427 100644 --- a/src/main/kotlin/api/klib/KlibDump.kt +++ b/src/main/kotlin/api/klib/KlibDump.kt @@ -12,6 +12,36 @@ import java.io.FileNotFoundException /** * Represents KLib ABI dump and allows manipulating it. + * + * Usual [KlibDump] workflows consists of loading, updating and writing a dump back. + * + * **Creating a textual dump from a klib** + * ```kotlin + * val dump = KlibDump.fromKlib(File("/path/to/library.klib")) + * File("/path/to/dump.klib.api").bufferedWriter().use { dump.saveTo(it) }) + * ``` + * + * **Loading a dump** + * ```kotlin + * val dump = KlibDump.from(File("/path/to/dump.klib.api")) + * ``` + * + * **Merging multiple dumps into a new merged dump** + * ```kotlin + * val klibs = listOf(File("/path/to/library-linuxX64.klib"), File("/path/to/library-linuxArm64.klib"), ...) + * val mergedDump = KlibDump() + * klibs.forEach { mergedDump.mergeFromKlib(it) } + * File("/path/to/merged.klib.api").bufferedWriter().use { mergedDump.saveTo(it) } + * ``` + * + * **Updating an existing merged dump** + * ```kotlin + * val mergedDump = KlibDump.from(File("/path/to/merged.klib.api")) + * val newTargetDump = KlibDump.fromKlib(File("/path/to/library-linuxX64.klib")) + * mergedDump.remove(newTargetDump.targets) + * mergedDump.merge(newTargetDump) + * File("/path/to/merged.klib.api").bufferedWrite().use { mergedDump.saveTo(it) } + * ``` */ @ExperimentalBCVApi public class KlibDump { @@ -135,7 +165,7 @@ public class KlibDump { * Dumps a public ABI of a klib represented by [klibFile] using [filters] * and returns a [KlibDump] representing it. * - * To control which declarations are dumped, [filters] could be used. By default, no filter will be applied. + * To control which declarations are dumped, [filters] could be used. By default, no filters will be applied. * * If a klib contains only a single target, it's possible to specify a custom configurable target name. * Please refer to [KlibTarget.configurableName] for more details on the meaning of that name. @@ -204,7 +234,7 @@ public fun inferAbi( /** * Dumps a public ABI of a klib represented by [klibFile] using [filters] and merges it into this dump. * - * To control which declarations are dumped, [filters] could be used. By default, no filter will be applied. + * To control which declarations are dumped, [filters] could be used. By default, no filters will be applied. * * If a klib contains only a single target, it's possible to specify a custom configurable target name. * Please refer to [KlibTarget.configurableName] for more details on the meaning of that name. @@ -221,7 +251,7 @@ public fun inferAbi( * @throws FileNotFoundException if [klibFile] does not exist. */ @ExperimentalBCVApi -public fun KlibDump.mergeKlib( +public fun KlibDump.mergeFromKlib( klibFile: File, configurableTargetName: String? = null, filters: KLibDumpFilters = KLibDumpFilters.DEFAULT ) { diff --git a/src/main/kotlin/api/klib/KlibTarget.kt b/src/main/kotlin/api/klib/KlibTarget.kt index 404b51f6..ad81b09b 100644 --- a/src/main/kotlin/api/klib/KlibTarget.kt +++ b/src/main/kotlin/api/klib/KlibTarget.kt @@ -10,15 +10,22 @@ package kotlinx.validation.api.klib * Target name consisting of two parts: a [configurableName] that could be configured by a user, and an [targetName] * that names a target platform and could not be configured by a user. * - * When serialized, the target represented as a tuple `.`, like `ios.iosArm64`. + * When serialized, the target represented as a tuple `.`, like `ios.iosArm64`. * If both names are the same (they are by default, unless a user decides to use a custom name), the serialized * from is shortened to a single term. For example, `macosArm64.macosArm64` and `macosArm64` are a long and a short * serialized forms of the same target. */ public class KlibTarget internal constructor( + /** + * A name of a target that could be configured by a user in a build script. + * Usually, it's the same name as [targetName]. + */ public val configurableName: String, - public val targetName: String) -{ + /** + * An actual name of a target that remains unaffected by a custom name settings in a build script. + */ + public val targetName: String +) { init { require(!configurableName.contains(".")) { "Configurable name can't contain the '.' character: $configurableName" @@ -28,15 +35,21 @@ public class KlibTarget internal constructor( } } public companion object { - public fun parse(line: String): KlibTarget { - require(line.isNotBlank()) { "Target name could not be blank." } - if (!line.contains('.')) { - return KlibTarget(line) + /** + * Parses a [KlibTarget] from a [value] string in a long (`.`) + * or a short (``) format. + * + * @throws IllegalArgumentException if [value] does not conform the format. + */ + public fun parse(value: String): KlibTarget { + require(value.isNotBlank()) { "Target name could not be blank." } + if (!value.contains('.')) { + return KlibTarget(value) } - val parts = line.split('.') + val parts = value.split('.') if (parts.size != 2 || parts.any { it.isBlank() }) { throw IllegalArgumentException( - "Target has illegal name format: \"$line\", expected: ." + "Target has illegal name format: \"$value\", expected: ." ) } return KlibTarget(parts[1], parts[0]) From ba9fcf16130baf59b1d0ff50197cbc2368bd18cb Mon Sep 17 00:00:00 2001 From: Filipp Zhinkin Date: Thu, 7 Mar 2024 20:53:31 +0100 Subject: [PATCH 41/53] Update API dump and target aliasing rule --- api/binary-compatibility-validator.api | 4 ++-- src/main/kotlin/klib/KlibAbiDumpFileMerger.kt | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/api/binary-compatibility-validator.api b/api/binary-compatibility-validator.api index b773d00e..ef7be90f 100644 --- a/api/binary-compatibility-validator.api +++ b/api/binary-compatibility-validator.api @@ -157,8 +157,8 @@ public final class kotlinx/validation/api/klib/KlibDumpFiltersKt { public final class kotlinx/validation/api/klib/KlibDumpKt { public static final fun inferAbi (Lkotlinx/validation/api/klib/KlibTarget;Ljava/lang/Iterable;Lkotlinx/validation/api/klib/KlibDump;)Lkotlinx/validation/api/klib/KlibDump; public static synthetic fun inferAbi$default (Lkotlinx/validation/api/klib/KlibTarget;Ljava/lang/Iterable;Lkotlinx/validation/api/klib/KlibDump;ILjava/lang/Object;)Lkotlinx/validation/api/klib/KlibDump; - public static final fun mergeKlib (Lkotlinx/validation/api/klib/KlibDump;Ljava/io/File;Ljava/lang/String;Lkotlinx/validation/api/klib/KLibDumpFilters;)V - public static synthetic fun mergeKlib$default (Lkotlinx/validation/api/klib/KlibDump;Ljava/io/File;Ljava/lang/String;Lkotlinx/validation/api/klib/KLibDumpFilters;ILjava/lang/Object;)V + public static final fun mergeFromKlib (Lkotlinx/validation/api/klib/KlibDump;Ljava/io/File;Ljava/lang/String;Lkotlinx/validation/api/klib/KLibDumpFilters;)V + public static synthetic fun mergeFromKlib$default (Lkotlinx/validation/api/klib/KlibDump;Ljava/io/File;Ljava/lang/String;Lkotlinx/validation/api/klib/KLibDumpFilters;ILjava/lang/Object;)V } public final class kotlinx/validation/api/klib/KlibSignatureVersion { diff --git a/src/main/kotlin/klib/KlibAbiDumpFileMerger.kt b/src/main/kotlin/klib/KlibAbiDumpFileMerger.kt index 947f807c..b1b106df 100644 --- a/src/main/kotlin/klib/KlibAbiDumpFileMerger.kt +++ b/src/main/kotlin/klib/KlibAbiDumpFileMerger.kt @@ -704,7 +704,7 @@ internal class KLibsTargetsFormatter(klibDump: KlibAbiDumpMerger) { val targetsMut = targets.toMutableSet() val resultingTargets = mutableListOf() for (alias in aliases) { - if (targetsMut.containsAll(alias.targets)) { + if (targetsMut == alias.targets) { targetsMut.removeAll(alias.targets) resultingTargets.add(alias.name) } From 5e01cf47c5b572f427308b4e513ab154866a0bb2 Mon Sep 17 00:00:00 2001 From: Filipp Zhinkin Date: Mon, 11 Mar 2024 12:20:21 +0100 Subject: [PATCH 42/53] Improved test coverage, clarified some corner cases --- src/main/kotlin/api/klib/KlibDump.kt | 22 +- src/main/kotlin/api/klib/KlibDumpFilters.kt | 9 +- src/main/kotlin/klib/KlibAbiDumpFileMerger.kt | 22 +- src/main/kotlin/klib/TargetHierarchy.kt | 15 - src/test/kotlin/samples/KlibDumpSamples.kt | 6 +- .../kotlin/tests/ClassNameConvertionTest.kt | 2 + src/test/kotlin/tests/KlibAbiMergingTest.kt | 4 + src/test/kotlin/tests/KlibDumpTest.kt | 554 ++++++++++++++++++ .../kotlin/tests/KlibSignatureVersionTest.kt | 39 ++ src/test/kotlin/tests/KlibTargetNameTest.kt | 45 ++ 10 files changed, 692 insertions(+), 26 deletions(-) create mode 100644 src/test/kotlin/tests/KlibDumpTest.kt create mode 100644 src/test/kotlin/tests/KlibSignatureVersionTest.kt diff --git a/src/main/kotlin/api/klib/KlibDump.kt b/src/main/kotlin/api/klib/KlibDump.kt index 7995b427..e6c9b502 100644 --- a/src/main/kotlin/api/klib/KlibDump.kt +++ b/src/main/kotlin/api/klib/KlibDump.kt @@ -71,12 +71,14 @@ public class KlibDump { * @throws IllegalArgumentException if this dump and [dumpFile] shares same targets. * @throws IllegalArgumentException if [dumpFile] contains multiple targets * and [configurableTargetName] is not null. + * @throws IllegalArgumentException if [dumpFile] is not a file. * @throws FileNotFoundException if [dumpFile] does not exist. * * @sample samples.KlibDumpSamples.mergeDumps */ public fun merge(dumpFile: File, configurableTargetName: String? = null) { - if (!dumpFile.exists()) throw FileNotFoundException(dumpFile.absolutePath) + if(!dumpFile.exists()) { throw FileNotFoundException("File does not exist: ${dumpFile.absolutePath}") } + require(dumpFile.isFile) { "Not a file: ${dumpFile.absolutePath}" } merger.merge(dumpFile, configurableTargetName) } @@ -152,12 +154,15 @@ public class KlibDump { * * @throws IllegalArgumentException if [dumpFile] contains multiple targets * and [configurableTargetName] is not null. + * @throws IllegalArgumentException if [dumpFile] is empty. + * @throws IllegalArgumentException if [dumpFile] is not a file. * @throws FileNotFoundException if [dumpFile] does not exist. * * @sample samples.KlibDumpSamples.mergeDumpObjects */ public fun from(dumpFile: File, configurableTargetName: String? = null): KlibDump { - check(dumpFile.exists()) { "File does not exist: ${dumpFile.absolutePath}" } + if(!dumpFile.exists()) { throw FileNotFoundException("File does not exist: ${dumpFile.absolutePath}") } + require(dumpFile.isFile) { "Not a file: ${dumpFile.absolutePath}" } return KlibDump().apply { merge(dumpFile, configurableTargetName) } } @@ -177,6 +182,7 @@ public class KlibDump { * * @throws IllegalArgumentException if [klibFile] contains multiple targets * and [configurableTargetName] is not null. + * @throws IllegalStateException if a klib could not be loaded from [klibFile]. * @throws FileNotFoundException if [klibFile] does not exist. */ public fun fromKlib( @@ -205,6 +211,9 @@ public class KlibDump { * The function aimed to facilitate ABI dumps generation for targets that are not supported by a host compiler. * In practice, it means generating dumps for Apple targets on non-Apple hosts. * + * @throws IllegalArgumentException when one of [supportedTargetDumps] contains [unsupportedTarget] + * @throws IllegalArgumentException when [supportedTargetDumps] are empty and [oldMergedDump] is null + * * @sample samples.KlibDumpSamples.inferDump */ @ExperimentalBCVApi @@ -213,6 +222,14 @@ public fun inferAbi( supportedTargetDumps: Iterable, oldMergedDump: KlibDump? = null ): KlibDump { + require(supportedTargetDumps.iterator().hasNext() || oldMergedDump != null) { + "Can't infer a dump without any dumps provided (supportedTargetDumps is empty, oldMergedDump is null)" + } + supportedTargetDumps.asSequence().flatMap { it.targets }.toSet().also { + require(!it.contains(unsupportedTarget)) { + "Supported target dumps already contains unsupportedTarget=$unsupportedTarget" + } + } val retainedDump = KlibDump().apply { if (oldMergedDump != null) { @@ -248,6 +265,7 @@ public fun inferAbi( * @throws IllegalArgumentException if this dump and [klibFile] shares same targets. * @throws IllegalArgumentException if [klibFile] contains multiple targets * and [configurableTargetName] is not null. + * @throws IllegalStateException if a klib could not be loaded from [klibFile]. * @throws FileNotFoundException if [klibFile] does not exist. */ @ExperimentalBCVApi diff --git a/src/main/kotlin/api/klib/KlibDumpFilters.kt b/src/main/kotlin/api/klib/KlibDumpFilters.kt index d7fd9f72..0a950cda 100644 --- a/src/main/kotlin/api/klib/KlibDumpFilters.kt +++ b/src/main/kotlin/api/klib/KlibDumpFilters.kt @@ -9,6 +9,7 @@ import kotlinx.validation.ExperimentalBCVApi import org.jetbrains.kotlin.gradle.utils.`is` import org.jetbrains.kotlin.library.abi.* import java.io.File +import java.io.FileNotFoundException /** * Filters affecting how the klib ABI will be represented in a dump. @@ -98,7 +99,7 @@ public fun KLibDumpFilters(builderAction: KLibDumpFilters.Builder.() -> Unit): K @ExperimentalBCVApi @OptIn(ExperimentalLibraryAbiReader::class) internal fun dumpTo(to: Appendable, klibFile: File, filters: KLibDumpFilters) { - require(klibFile.exists()) { "File does not exist: ${klibFile.absolutePath}" } + if(!klibFile.exists()) { throw FileNotFoundException("File does not exist: ${klibFile.absolutePath}") } val abiFilters = mutableListOf() filters.ignoredClasses.toKlibNames().also { if (it.isNotEmpty()) { @@ -114,7 +115,11 @@ internal fun dumpTo(to: Appendable, klibFile: File, filters: KLibDumpFilters) { abiFilters.add(AbiReadingFilter.ExcludedPackages(filters.ignoredPackages.map { AbiCompoundName(it) })) } - val library = LibraryAbiReader.readAbiInfo(klibFile, abiFilters) + val library = try { + LibraryAbiReader.readAbiInfo(klibFile, abiFilters) + } catch (t: Throwable) { + throw IllegalStateException("Unable to read klib from ${klibFile.absolutePath}", t) + } val supportedSignatureVersions = library.signatureVersions.asSequence().filter { it.isSupportedByAbiReader } diff --git a/src/main/kotlin/klib/KlibAbiDumpFileMerger.kt b/src/main/kotlin/klib/KlibAbiDumpFileMerger.kt index b1b106df..3bb5b17e 100644 --- a/src/main/kotlin/klib/KlibAbiDumpFileMerger.kt +++ b/src/main/kotlin/klib/KlibAbiDumpFileMerger.kt @@ -103,13 +103,14 @@ internal class KlibAbiDumpMerger { } private fun merge(lines: LinesProvider, configurableTargetName: String?) { + require(lines.peek() != null) { "File is empty" } val isMergedFile = lines.determineFileType() val aliases = mutableMapOf>() val bcvTargets = mutableSetOf() if (isMergedFile) { lines.next() // skip the heading line - bcvTargets.addAll(lines.parseTargets()) + bcvTargets.addAll(lines.parseTargets(configurableTargetName)) check(bcvTargets.size == 1 || configurableTargetName == null) { "Can't use an explicit target name with a multi-target dump. " + "targetName: $configurableTargetName, dump targets: $bcvTargets" @@ -189,7 +190,7 @@ internal class KlibAbiDumpMerger { } } - private fun LinesProvider.parseTargets(): Set { + private fun LinesProvider.parseTargets(configurableTargetName: String?): Set { val line = peek() require(line != null) { "List of targets expected, but there are no more lines left." @@ -198,7 +199,14 @@ internal class KlibAbiDumpMerger { "The line should starts with $TARGETS_LIST_PREFIX, but was: $line" } next() - return parseBcvTargetsLine(line) + val targets = parseBcvTargetsLine(line) + require(configurableTargetName == null || targets.size == 1) { + "Can't use configurableTargetName ($configurableTargetName) for a multi-target dump: $targets" + } + if (configurableTargetName != null) { + return setOf(KlibTarget(configurableTargetName, targets.first().targetName)) + } + return targets } private fun LinesProvider.parseAliases(): Map> { @@ -295,7 +303,7 @@ internal class KlibAbiDumpMerger { val targetsList = targetsString.split(TARGETS_DELIMITER).map { konanTargetNameMapping[it.trim()] ?: throw IllegalStateException("Unknown native target: $it") } - check(targetsList.size == 1 || configurableTargetName == null) { + require(targetsList.size == 1 || configurableTargetName == null) { "Can't use configurableTargetName ($configurableTargetName) for a multi-target dump: $targetsList" } if (targetsList.size == 1 && configurableTargetName != null) { @@ -348,6 +356,12 @@ internal class KlibAbiDumpMerger { } fun dump(appendable: Appendable) { + if (targets.isEmpty()) { + check(topLevelDeclaration.children.isEmpty()) { + "Dump containing some declaration should have at least a single target" + } + return + } val formatter = createFormatter() appendable.append(MERGED_DUMP_FILE_HEADER).append('\n') appendable.append(formatter.formatHeader(targets)).append('\n') diff --git a/src/main/kotlin/klib/TargetHierarchy.kt b/src/main/kotlin/klib/TargetHierarchy.kt index cf76d58a..900ddf28 100644 --- a/src/main/kotlin/klib/TargetHierarchy.kt +++ b/src/main/kotlin/klib/TargetHierarchy.kt @@ -148,18 +148,3 @@ internal val konanTargetNameMapping = mapOf( "wasm32" to "wasm32" ) -internal val konanTargetNameReverseMapping = konanTargetNameMapping.asSequence().map { it.value to it.key }.toMap() - -internal fun platformByCanonicalName(canonicalName: String): String { - return when (canonicalName) { - "js" -> "JS" - "wasmJs" -> "WASM" - "wasmWasi" -> "WASM" - else -> { - require(konanTargetNameReverseMapping.containsKey(canonicalName)) { - "Unsupported target: $canonicalName" - } - return "NATIVE" - } - } -} diff --git a/src/test/kotlin/samples/KlibDumpSamples.kt b/src/test/kotlin/samples/KlibDumpSamples.kt index 74f23782..6c5971bf 100644 --- a/src/test/kotlin/samples/KlibDumpSamples.kt +++ b/src/test/kotlin/samples/KlibDumpSamples.kt @@ -264,14 +264,14 @@ class KlibDumpSamples { """.trimIndent()) // Let's use these dumps to infer a public ABI on iosArm64 - val inferredArm64Dump = inferAbi( + val inferredIosArm64Dump = inferAbi( unsupportedTarget = unsupportedTarget, supportedTargetDumps = listOf(KlibDump.from(linuxDump)), oldMergedDump = KlibDump.from(oldMergedDump)) - assertEquals(unsupportedTarget, inferredArm64Dump.targets.single()) + assertEquals(unsupportedTarget, inferredIosArm64Dump.targets.single()) - val inferredDumpContent = buildString { inferredArm64Dump.saveTo(this) } + val inferredDumpContent = buildString { inferredIosArm64Dump.saveTo(this) } assertEquals(""" // KLib ABI Dump // Targets: [iosArm64] diff --git a/src/test/kotlin/tests/ClassNameConvertionTest.kt b/src/test/kotlin/tests/ClassNameConvertionTest.kt index 3dcd28e1..9777d4fb 100644 --- a/src/test/kotlin/tests/ClassNameConvertionTest.kt +++ b/src/test/kotlin/tests/ClassNameConvertionTest.kt @@ -27,6 +27,8 @@ class ClassNameConvertionTest { checkNames("p.a\$b\$c", AbiQualifiedName("p", "a.b.c")) checkNames("org.example.Outer\$Inner\$\$serializer", AbiQualifiedName("org.example", "Outer.Inner.\$serializer")) + checkNames("org.example.Outer\$Inner\$\$\$serializer", + AbiQualifiedName("org.example", "Outer.Inner.\$\$serializer")) checkNames("a.b.e.s.c.MapStream\$Stream\$", AbiQualifiedName("a.b.e.s.c", "MapStream.Stream\$")) } diff --git a/src/test/kotlin/tests/KlibAbiMergingTest.kt b/src/test/kotlin/tests/KlibAbiMergingTest.kt index 16424b92..65b24c0c 100644 --- a/src/test/kotlin/tests/KlibAbiMergingTest.kt +++ b/src/test/kotlin/tests/KlibAbiMergingTest.kt @@ -331,5 +331,9 @@ class KlibAbiMergingTest { ) val expectedTargets = expectedTargetNames.asSequence().map(KlibTarget::parse).toSet() assertEquals(expectedTargets, lib.targets) + + assertFailsWith { + KlibAbiDumpMerger().merge(file("/merge/stdlib_native_common.abi"), "target") + } } } diff --git a/src/test/kotlin/tests/KlibDumpTest.kt b/src/test/kotlin/tests/KlibDumpTest.kt new file mode 100644 index 00000000..6018569c --- /dev/null +++ b/src/test/kotlin/tests/KlibDumpTest.kt @@ -0,0 +1,554 @@ +/* + * Copyright 2016-2024 JetBrains s.r.o. + * Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file. + */ + +package tests + +import kotlinx.validation.ExperimentalBCVApi +import kotlinx.validation.api.klib.KlibDump +import kotlinx.validation.api.klib.KlibTarget +import kotlinx.validation.api.klib.inferAbi +import kotlinx.validation.api.klib.mergeFromKlib +import org.jetbrains.kotlin.backend.common.phaser.dumpIrElement +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TemporaryFolder +import java.io.File +import java.io.FileNotFoundException +import java.util.* +import kotlin.math.sin +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith +import kotlin.test.assertTrue + +private val rawLinuxDump = """ + // Rendering settings: + // - Signature version: 2 + // - Show manifest properties: false + // - Show declarations: true + + // Library unique name: + // Platform: NATIVE + // Native targets: linux_x64 + // Compiler version: 1.9.22 + // ABI version: 1.8.0 + final class org.example/ShardedClass { // org.example/ShardedClass|null[0] + final val value // org.example/ShardedClass.value|{}value[0] + final fun (): kotlin/Int // org.example/ShardedClass.value.|(){}[0] + constructor (kotlin/Int) // org.example/ShardedClass.|(kotlin.Int){}[0] + final fun add(kotlin/Int): kotlin/Int // org.example/ShardedClass.add|add(kotlin.Int){}[0] + } + final fun org.example/ShardedClass(kotlin/Int, kotlin/Float, kotlin/Long): org.example/ShardedClass // org.example/ShardedClass|ShardedClass(kotlin.Int;kotlin.Float;kotlin.Long){}[0] +""".trimIndent() + +private val mergedLinuxDump = """ + // KLib ABI Dump + // Targets: [linuxX64] + // Rendering settings: + // - Signature version: 2 + // - Show manifest properties: false + // - Show declarations: true + + // Library unique name: + final class org.example/ShardedClass { // org.example/ShardedClass|null[0] + constructor (kotlin/Int) // org.example/ShardedClass.|(kotlin.Int){}[0] + final fun add(kotlin/Int): kotlin/Int // org.example/ShardedClass.add|add(kotlin.Int){}[0] + final val value // org.example/ShardedClass.value|{}value[0] + final fun (): kotlin/Int // org.example/ShardedClass.value.|(){}[0] + } + final fun org.example/ShardedClass(kotlin/Int, kotlin/Float, kotlin/Long): org.example/ShardedClass // org.example/ShardedClass|ShardedClass(kotlin.Int;kotlin.Float;kotlin.Long){}[0] + +""".trimIndent() + + +private val mergedLinuxDumpWithTargetSpecificDeclaration = """ + // KLib ABI Dump + // Targets: [linuxArm64, linuxX64] + // Rendering settings: + // - Signature version: 2 + // - Show manifest properties: false + // - Show declarations: true + + // Library unique name: + final class org.example/ShardedClass { // org.example/ShardedClass|null[0] + constructor (kotlin/Int) // org.example/ShardedClass.|(kotlin.Int){}[0] + final val value // org.example/ShardedClass.value|{}value[0] + final fun (): kotlin/Int // org.example/ShardedClass.value.|(){}[0] + // Targets: [linuxArm64] + final fun add2(kotlin/Int): kotlin/Int // org.example/ShardedClass.add|add(kotlin.Int){}[0] + // Targets: [linuxX64] + final fun add(kotlin/Int): kotlin/Int // org.example/ShardedClass.add|add(kotlin.Int){}[0] + } + // Targets: [linuxArm64] + final fun org.example/ShardedClass2(kotlin/Int, kotlin/Float, kotlin/Long): org.example/ShardedClass // org.example/ShardedClass|ShardedClass(kotlin.Int;kotlin.Float;kotlin.Long){}[0] + // Targets: [linuxX64] + final fun org.example/ShardedClass(kotlin/Int, kotlin/Float, kotlin/Long): org.example/ShardedClass // org.example/ShardedClass|ShardedClass(kotlin.Int;kotlin.Float;kotlin.Long){}[0] + +""".trimIndent() + +private val mergedLinuxArm64Dump = """ + // KLib ABI Dump + // Targets: [linuxArm64] + // Rendering settings: + // - Signature version: 2 + // - Show manifest properties: false + // - Show declarations: true + + // Library unique name: + final class org.example/ShardedClass { // org.example/ShardedClass|null[0] + constructor (kotlin/Int) // org.example/ShardedClass.|(kotlin.Int){}[0] + final val value // org.example/ShardedClass.value|{}value[0] + final fun (): kotlin/Int // org.example/ShardedClass.value.|(){}[0] + final fun add2(kotlin/Int): kotlin/Int // org.example/ShardedClass.add|add(kotlin.Int){}[0] + } + final fun org.example/ShardedClass2(kotlin/Int, kotlin/Float, kotlin/Long): org.example/ShardedClass // org.example/ShardedClass|ShardedClass(kotlin.Int;kotlin.Float;kotlin.Long){}[0] + +""".trimIndent() + +private val mergedLinuxDumpWithCustomName = """ + // KLib ABI Dump + // Targets: [linuxX64.testTarget] + // Rendering settings: + // - Signature version: 2 + // - Show manifest properties: false + // - Show declarations: true + + // Library unique name: + final class org.example/ShardedClass { // org.example/ShardedClass|null[0] + constructor (kotlin/Int) // org.example/ShardedClass.|(kotlin.Int){}[0] + final fun add(kotlin/Int): kotlin/Int // org.example/ShardedClass.add|add(kotlin.Int){}[0] + final val value // org.example/ShardedClass.value|{}value[0] + final fun (): kotlin/Int // org.example/ShardedClass.value.|(){}[0] + } + final fun org.example/ShardedClass(kotlin/Int, kotlin/Float, kotlin/Long): org.example/ShardedClass // org.example/ShardedClass|ShardedClass(kotlin.Int;kotlin.Float;kotlin.Long){}[0] + +""".trimIndent() + +private val rawMultitargetDump = """ + // Rendering settings: + // - Signature version: 2 + // - Show manifest properties: true + // - Show declarations: true + + // Library unique name: + // Platform: NATIVE + // Native targets: android_arm32, android_arm64, android_x64, android_x86, ios_arm64, ios_simulator_arm64, ios_x64, linux_arm32_hfp, linux_arm64, linux_x64, macos_arm64, macos_x64, mingw_x64, tvos_arm64, tvos_simulator_arm64, tvos_x64, watchos_arm32, watchos_arm64, watchos_device_arm64, watchos_simulator_arm64, watchos_x64 + // Compiler version: 2.0.255-SNAPSHOT + // ABI version: 1.8.0 + abstract interface kotlin/Annotation // kotlin/Annotation|null[0] +""".trimIndent() + +private val mergedMultitargetDump = """ + // KLib ABI Dump + // Targets: [androidNativeArm32, androidNativeArm64, androidNativeX64, androidNativeX86, iosArm64, iosSimulatorArm64, iosX64, linuxArm32Hfp, linuxArm64, linuxX64, macosArm64, macosX64, mingwX64, tvosArm64, tvosSimulatorArm64, tvosX64, watchosArm32, watchosArm64, watchosDeviceArm64, watchosSimulatorArm64, watchosX64] + // Rendering settings: + // - Signature version: 2 + // - Show manifest properties: true + // - Show declarations: true + + // Library unique name: + abstract interface kotlin/Annotation // kotlin/Annotation|null[0] + +""".trimIndent() + + +private val mergedMultitargetDumpFiltered = """ + // KLib ABI Dump + // Targets: [androidNativeArm32] + // Rendering settings: + // - Signature version: 2 + // - Show manifest properties: true + // - Show declarations: true + + // Library unique name: + abstract interface kotlin/Annotation // kotlin/Annotation|null[0] + +""".trimIndent() + +// Note that some cases are already covered in KlibDumpSamples.kt and not duplicated here +@OptIn(ExperimentalBCVApi::class) +class KlibDumpTest { + @JvmField + @Rule + var tmpFolder = TemporaryFolder() + + private fun asFile(dump: String): File { + val file = tmpFolder.newFile() + file.bufferedWriter().use { it.write(dump) } + return file + } + + @Test + fun emptyDump() { + val dump = buildString { + KlibDump().saveTo(this) + } + assertEquals("", dump) + + assertFailsWith { + KlibDump.from(tmpFolder.newFile()) + } + } + + @Test + fun loadFromNonExistingFile() { + assertFailsWith { + KlibDump.from(tmpFolder.root.resolve(UUID.randomUUID().toString())) + } + assertFailsWith { + KlibDump().merge(tmpFolder.root.resolve(UUID.randomUUID().toString())) + } + assertFailsWith { + KlibDump.fromKlib(tmpFolder.root.resolve(UUID.randomUUID().toString())) + } + assertFailsWith { + KlibDump().mergeFromKlib(tmpFolder.root.resolve(UUID.randomUUID().toString())) + } + } + + @Test + fun loadKlibFromNonKlib() { + assertFailsWith { KlibDump.fromKlib(tmpFolder.root) } + assertFailsWith { KlibDump.fromKlib(tmpFolder.newFile()) } + + assertFailsWith { KlibDump().also { it.mergeFromKlib(tmpFolder.root) } } + assertFailsWith { KlibDump().also { it.mergeFromKlib(tmpFolder.newFile()) } } + } + + @Test + fun loadFromDirectory() { + assertFailsWith { + KlibDump.from(tmpFolder.root) + } + assertFailsWith { + KlibDump().merge(tmpFolder.root) + } + } + + @Test + fun loadDumpWithSingleTarget() { + val klibDump = KlibDump.from(asFile(rawLinuxDump)) + assertEquals(setOf(KlibTarget.parse("linuxX64")), klibDump.targets) + assertEquals(mergedLinuxDump, buildString { klibDump.saveTo(this) }) + + val mergedKlibDump = KlibDump.from(asFile(mergedLinuxDump)) + assertEquals(setOf(KlibTarget.parse("linuxX64")), mergedKlibDump.targets) + assertEquals(mergedLinuxDump, buildString { mergedKlibDump.saveTo(this) }) + } + + @Test + fun mergeDumpWithSingleTarget() { + val klibDump = KlibDump().also { it.merge(asFile(rawLinuxDump)) } + assertEquals(setOf(KlibTarget.parse("linuxX64")), klibDump.targets) + assertEquals(mergedLinuxDump, buildString { klibDump.saveTo(this) }) + + val mergedKlibDump = KlibDump().also { it.merge(asFile(mergedLinuxDump)) } + assertEquals(setOf(KlibTarget.parse("linuxX64")), mergedKlibDump.targets) + assertEquals(mergedLinuxDump, buildString { mergedKlibDump.saveTo(this) }) + } + + @Test + fun loadDumpWithSingleTargetWithCustomName() { + val klibDump = KlibDump.from(asFile(rawLinuxDump), configurableTargetName = "testTarget") + assertEquals(setOf(KlibTarget.parse("linuxX64.testTarget")), klibDump.targets) + assertEquals(mergedLinuxDumpWithCustomName, buildString { klibDump.saveTo(this) }) + + val mergedKlibDump = KlibDump.from(asFile(mergedLinuxDump), configurableTargetName = "testTarget") + assertEquals(setOf(KlibTarget.parse("linuxX64.testTarget")), mergedKlibDump.targets) + assertEquals(mergedLinuxDumpWithCustomName, buildString { mergedKlibDump.saveTo(this) }) + + val customTargetDump = KlibDump.from(asFile(mergedLinuxDumpWithCustomName)) + assertEquals(setOf(KlibTarget.parse("linuxX64.testTarget")), customTargetDump.targets) + assertEquals(mergedLinuxDumpWithCustomName, buildString { customTargetDump.saveTo(this) }) + } + + @Test + fun mergeDumpWithSingleTargetWithCustomName() { + val klibDump = KlibDump().also { it.merge(asFile(rawLinuxDump), configurableTargetName = "testTarget") } + assertEquals(setOf(KlibTarget.parse("linuxX64.testTarget")), klibDump.targets) + assertEquals(mergedLinuxDumpWithCustomName, buildString { klibDump.saveTo(this) }) + + val mergedKlibDump = + KlibDump().also { it.merge(asFile(mergedLinuxDump), configurableTargetName = "testTarget") } + assertEquals(setOf(KlibTarget.parse("linuxX64.testTarget")), mergedKlibDump.targets) + assertEquals(mergedLinuxDumpWithCustomName, buildString { mergedKlibDump.saveTo(this) }) + + val customTargetDump = KlibDump().also { it.merge(asFile(mergedLinuxDumpWithCustomName)) } + assertEquals(setOf(KlibTarget.parse("linuxX64.testTarget")), customTargetDump.targets) + assertEquals(mergedLinuxDumpWithCustomName, buildString { customTargetDump.saveTo(this) }) + } + + @Test + fun loadMultitargetDump() { + val dump = KlibDump.from(asFile(rawMultitargetDump)) + assertEquals(21, dump.targets.size) + assertEquals(mergedMultitargetDump, buildString { dump.saveTo(this) }) + + val mergedDump = KlibDump.from(asFile(mergedMultitargetDump)) + assertEquals(21, mergedDump.targets.size) + assertEquals(mergedMultitargetDump, buildString { mergedDump.saveTo(this) }) + } + + @Test + fun mergeMultitargetDump() { + val dump = KlibDump().also { it.merge(asFile(rawMultitargetDump)) } + assertEquals(21, dump.targets.size) + assertEquals(mergedMultitargetDump, buildString { dump.saveTo(this) }) + + val mergedDump = KlibDump().also { it.merge(asFile(mergedMultitargetDump)) } + assertEquals(21, mergedDump.targets.size) + assertEquals(mergedMultitargetDump, buildString { mergedDump.saveTo(this) }) + } + + @Test + fun loadMultitargetDumpUsingCustomName() { + assertFailsWith { + KlibDump.from(asFile(rawMultitargetDump), "abc") + } + assertFailsWith { + KlibDump().also { it.merge(asFile(rawMultitargetDump), "abc") } + } + + assertFailsWith { + KlibDump.from(asFile(mergedMultitargetDump), "abc") + } + assertFailsWith { + KlibDump().also { it.merge(asFile(mergedMultitargetDump), "abc") } + } + } + + @Test + fun retainAll() { + val dump = KlibDump.from(asFile(mergedMultitargetDump)) + val oldTargets = setOf(*dump.targets.toTypedArray()) + + dump.retain(oldTargets) + assertEquals(oldTargets, dump.targets) + assertEquals(mergedMultitargetDump, buildString { dump.saveTo(this) }) + } + + @Test + fun retainSingle() { + val dump = KlibDump.from(asFile(mergedMultitargetDump)) + val singleTarget = KlibTarget.parse("androidNativeArm32") + + dump.retain(setOf(singleTarget)) + assertEquals(setOf(singleTarget), dump.targets) + assertEquals(mergedMultitargetDumpFiltered, buildString { dump.saveTo(this) }) + } + + @Test + fun retainNone() { + val dump = KlibDump.from(asFile(mergedMultitargetDump)) + dump.retain(emptySet()) + + assertTrue(dump.targets.isEmpty()) + assertEquals("", buildString { dump.saveTo(this) }) + } + + @Test + fun removeAll() { + val dump = KlibDump.from(asFile(mergedMultitargetDump)) + dump.remove(listOf(*dump.targets.toTypedArray())) + + assertTrue(dump.targets.isEmpty()) + assertEquals("", buildString { dump.saveTo(this) }) + } + + @Test + fun removeNone() { + val dump = KlibDump.from(asFile(mergedMultitargetDump)) + val oldTargets = setOf(*dump.targets.toTypedArray()) + + dump.remove(emptySet()) + assertEquals(oldTargets, dump.targets) + assertEquals(mergedMultitargetDump, buildString { dump.saveTo(this) }) + } + + @Test + fun removeSome() { + val dump = KlibDump.from(asFile(mergedMultitargetDump)) + val singleTarget = KlibTarget.parse("androidNativeArm32") + + dump.remove(dump.targets.subtract(setOf(singleTarget))) + assertEquals(setOf(singleTarget), dump.targets) + assertEquals(mergedMultitargetDumpFiltered, buildString { dump.saveTo(this) }) + } + + @Test + fun removeOrRetainTargetsNotPresentedInDump() { + val dump = KlibDump.from(asFile(mergedMultitargetDump)) + val targets = setOf(*dump.targets.toTypedArray()) + dump.remove(listOf(KlibTarget.parse("linuxX64.blablabla"))) + assertEquals(targets, dump.targets) + + dump.retain(listOf(KlibTarget.parse("macosArm64.macos"))) + assertTrue(dump.targets.isEmpty()) + } + + @Test + fun removeDeclarationsAlongWithTargets() { + val dump = KlibDump.from(asFile(mergedLinuxDumpWithTargetSpecificDeclaration)) + val toRemove = KlibTarget.parse("linuxArm64") + + dump.remove(listOf(toRemove)) + assertEquals(mergedLinuxDump, buildString { dump.saveTo(this) }) + } + + @Test + fun testCopy() { + val dump = KlibDump.from(asFile(mergedLinuxDumpWithTargetSpecificDeclaration)) + val copy = dump.copy() + + dump.remove(listOf(KlibTarget.parse("linuxArm64"))) + assertEquals(mergedLinuxDumpWithTargetSpecificDeclaration, buildString { copy.saveTo(this) }) + } + + @Test + fun testMergeDumps() { + val dump = KlibDump().also { + it.merge(asFile(mergedLinuxDump)) + it.merge(asFile(mergedLinuxArm64Dump)) + } + assertEquals(mergedLinuxDumpWithTargetSpecificDeclaration, buildString { dump.saveTo(this) }) + } + + @Test + fun mergeDumpsWithIntersectingTargets() { + val mergedDump = KlibDump.from(asFile(rawMultitargetDump)) + + assertFailsWith { + mergedDump.merge(asFile(rawMultitargetDump)) + } + + assertFailsWith { + mergedDump.merge(asFile(mergedLinuxDump)) + } + } + + @Test + fun inferWithoutAnOldDump() { + val unsupportedTarget = KlibTarget.parse("iosArm64") + + val linuxDump = KlibDump.from( + asFile( + """ + // KLib ABI Dump + // Targets: [linuxArm64] + // Rendering settings: + // - Signature version: 2 + // - Show manifest properties: true + // - Show declarations: true + + // Library unique name: + abstract class examples.classes/NewKlass // examples.classes/NewKlass|null[0] + """.trimIndent() + ) + ) + + // Let's use these dumps to infer a public ABI on iosArm64 + val inferredIosArm64Dump = inferAbi( + unsupportedTarget = unsupportedTarget, + supportedTargetDumps = listOf(linuxDump), + oldMergedDump = null + ) + + assertEquals(unsupportedTarget, inferredIosArm64Dump.targets.single()) + + val inferredDumpContent = buildString { inferredIosArm64Dump.saveTo(this) } + assertEquals( + """ + // KLib ABI Dump + // Targets: [iosArm64] + // Rendering settings: + // - Signature version: 2 + // - Show manifest properties: true + // - Show declarations: true + + // Library unique name: + abstract class examples.classes/NewKlass // examples.classes/NewKlass|null[0] + + """.trimIndent(), inferredDumpContent + ) + } + + @Test + fun inferFromAnOldDumpOnly() { + val unsupportedTarget = KlibTarget.parse("iosArm64") + + val oldDump = KlibDump.from( + asFile( + """ + // KLib ABI Dump + // Targets: [iosArm64, linuxArm64] + // Rendering settings: + // - Signature version: 2 + // - Show manifest properties: true + // - Show declarations: true + + // Library unique name: + abstract class examples.classes/Klass // examples.classes/Klass|null[0] + // Targets: [iosArm64] + abstract interface examples.classes/Iface // examples.classes/Iface|null[0] + + """.trimIndent() + ) + ) + + // Let's use these dumps to infer a public ABI on iosArm64 + val inferredIosArm64Dump = inferAbi( + unsupportedTarget = unsupportedTarget, + supportedTargetDumps = emptySet(), + oldMergedDump = oldDump + ) + + assertEquals(unsupportedTarget, inferredIosArm64Dump.targets.single()) + + val inferredDumpContent = buildString { inferredIosArm64Dump.saveTo(this) } + assertEquals( + """ + // KLib ABI Dump + // Targets: [iosArm64] + // Rendering settings: + // - Signature version: 2 + // - Show manifest properties: true + // - Show declarations: true + + // Library unique name: + abstract interface examples.classes/Iface // examples.classes/Iface|null[0] + + """.trimIndent(), inferredDumpContent + ) + } + + @Test + fun inferOutOfThinAir() { + val unsupportedTarget = KlibTarget.parse("iosArm64") + + assertFailsWith { + inferAbi(unsupportedTarget, emptySet(), null) + } + } + + @Test + fun inferFromSelf() { + val dump = KlibDump.from(asFile(mergedLinuxDump)) + assertFailsWith { + inferAbi(dump.targets.first(), listOf(dump)) + } + } + + @Test + fun inferFromIntersectingDumps() { + assertFailsWith { + inferAbi( + KlibTarget.parse("iosArm64.unsupported"), + listOf( + KlibDump.from(asFile(mergedLinuxDump)), + KlibDump.from(asFile(mergedMultitargetDump)) + ) + ) + } + } +} diff --git a/src/test/kotlin/tests/KlibSignatureVersionTest.kt b/src/test/kotlin/tests/KlibSignatureVersionTest.kt new file mode 100644 index 00000000..d08abb75 --- /dev/null +++ b/src/test/kotlin/tests/KlibSignatureVersionTest.kt @@ -0,0 +1,39 @@ +/* + * Copyright 2016-2024 JetBrains s.r.o. + * Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file. + */ + +package tests + +import kotlinx.validation.api.klib.KlibSignatureVersion +import org.junit.Test +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith +import kotlin.test.assertNotEquals + +class KlibSignatureVersionTest { + @Test + fun signatureConstruction() { + assertFailsWith { KlibSignatureVersion.of(-1) } + assertFailsWith { KlibSignatureVersion.of(0) } + + val correctVersion = KlibSignatureVersion.of(42) + assertEquals(42, correctVersion.version) + } + + @Test + fun signaturesEqual() { + assertEquals(KlibSignatureVersion.of(1), KlibSignatureVersion.of(1)) + KlibSignatureVersion.of(2).also { + assertEquals(it, it) + } + + assertNotEquals(KlibSignatureVersion.of(2), KlibSignatureVersion.of(3)) + } + + @Test + fun signatureHashCode() { + assertEquals(KlibSignatureVersion.of(1).hashCode(), KlibSignatureVersion.of(1).hashCode()) + assertNotEquals(KlibSignatureVersion.of(1).hashCode(), KlibSignatureVersion.of(2).hashCode()) + } +} diff --git a/src/test/kotlin/tests/KlibTargetNameTest.kt b/src/test/kotlin/tests/KlibTargetNameTest.kt index ced22847..48caf32f 100644 --- a/src/test/kotlin/tests/KlibTargetNameTest.kt +++ b/src/test/kotlin/tests/KlibTargetNameTest.kt @@ -10,6 +10,7 @@ import org.junit.Test import kotlin.test.assertEquals import kotlin.test.assertFails import kotlin.test.assertFailsWith +import kotlin.test.assertNotEquals class KlibTargetNameTest { @Test @@ -49,4 +50,48 @@ class KlibTargetNameTest { KlibTarget("a", "b.c") } } + + @Test + fun targetsEqual() { + assertEquals(KlibTarget.parse("androidNativeArm64"), KlibTarget.parse("androidNativeArm64")) + assertNotEquals(KlibTarget.parse("androidNativeArm64"), KlibTarget.parse("androidNativeArm32")) + + assertEquals( + KlibTarget.parse("androidNativeArm64.android"), KlibTarget.parse("androidNativeArm64.android") + ) + assertNotEquals( + KlibTarget.parse("androidNativeArm64.android"), KlibTarget.parse("androidNativeArm64") + ) + + assertEquals( + KlibTarget.parse("androidNativeArm64.androidNativeArm64"), + KlibTarget.parse("androidNativeArm64") + ) + } + + @Test + fun targetHashCode() { + assertEquals( + KlibTarget.parse("androidNativeArm64").hashCode(), + KlibTarget.parse("androidNativeArm64").hashCode() + ) + assertNotEquals( + KlibTarget.parse("androidNativeArm64").hashCode(), + KlibTarget.parse("androidNativeArm32").hashCode() + ) + + assertEquals( + KlibTarget.parse("androidNativeArm64.android").hashCode(), + KlibTarget.parse("androidNativeArm64.android").hashCode() + ) + assertNotEquals( + KlibTarget.parse("androidNativeArm64.android").hashCode(), + KlibTarget.parse("androidNativeArm64").hashCode() + ) + + assertEquals( + KlibTarget.parse("androidNativeArm64.androidNativeArm64").hashCode(), + KlibTarget.parse("androidNativeArm64").hashCode() + ) + } } From ba29177dc2a9b908abd16a52f4361d5f4117678a Mon Sep 17 00:00:00 2001 From: Filipp Zhinkin Date: Mon, 11 Mar 2024 13:21:51 +0100 Subject: [PATCH 43/53] Fixed typo in a filename --- src/test/kotlin/tests/KlibAbiMergingTest.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/kotlin/tests/KlibAbiMergingTest.kt b/src/test/kotlin/tests/KlibAbiMergingTest.kt index 65b24c0c..674594aa 100644 --- a/src/test/kotlin/tests/KlibAbiMergingTest.kt +++ b/src/test/kotlin/tests/KlibAbiMergingTest.kt @@ -73,7 +73,7 @@ class KlibAbiMergingTest { @Test fun divergingDumpFiles() { - val targets = mutableListOf("androidNativeArm64", "linuxArm64", "linuxX64", "tvOsX64") + val targets = mutableListOf("androidNativeArm64", "linuxArm64", "linuxX64", "tvosX64") val random = Random(42) for (i in 0 until 10) { val klib = KlibAbiDumpMerger() From dd776f292b069d7f4d2242e070febf9db60774a1 Mon Sep 17 00:00:00 2001 From: Filipp Zhinkin Date: Mon, 11 Mar 2024 13:48:58 +0100 Subject: [PATCH 44/53] Update the design doc --- docs/design/KLibSupport.md | 47 +++++++++++++++++++++++++++++++++++--- 1 file changed, 44 insertions(+), 3 deletions(-) diff --git a/docs/design/KLibSupport.md b/docs/design/KLibSupport.md index a196e304..0ce4bc59 100644 --- a/docs/design/KLibSupport.md +++ b/docs/design/KLibSupport.md @@ -86,7 +86,7 @@ is presented only on `linux`-targets (`// Targets: [linux]`). The next line declares a target alias (`// Alias: => []`). There are only one alias (`linux`). Aliases are generated for target groups corresponding to groups from a default hierarchy, that could appear in a file and consist of more than a single target. -For instance, there's no `andoidNative` group alias as there are no declarations having only +For instance, there's no `androidNative` group alias as there are no declarations having only corresponding android-native targets and there is no group for `mingwX64` as there would no other targets in such a group. After that, a regular KLib ABI dump header continues, with an exception to some declarations being @@ -205,8 +205,49 @@ target name, not on the visible one. ### Programmatic API for KLib ABI validation -To support scenarios when Gradle plugin could not be used (like in the case of Kotlin stdlib), +To support scenarios, when Gradle plugin could not be used (like in the case of Kotlin stdlib), or when the way users build klibs is different from "compile all the klibs here and now", the programmatic API should be provided. The API should simply provide an entry point to all the functionality used by the plugin. -The API is not yet designed and is the subject of further development. +Initially, it does make sense to provide an API allowing to implement the same functionality as Gradle Plugin does. +It also seems reasonable to provide an abstraction layer over the Kotlin compiler's API that dumps a klib so that +when needed, we could alter the dumping procedure without waiting for a Kotlin release. + +There are a few entities that should be exposed for now, namely: +- a config affecting how a klib dump will look like (`KLibDumpFilters`); +- a class representing a dump (`KlibDump`) and allowing to perform some actions on it, +namely, load, save, merge and, also infer; +- a few supplementary classes, like `KlibTarget` and `KlibSignatureVersion` to give a better and more meaningful +representation for entities that otherwise would be strings or numbers. + +There are not some many options that affect a resulting dump, so for the beginning `KLibDumpFilters` may include only +`nonPublicMarkers`, `ignoredPackages` and `ignoredClasses` to reflect what could be configured through +`kotlinx.validation.ApiValidationExtension`, and, also a `signatureVersion` (represented by a dedicated class). +The latter is only required to handle potential klib signature versions update in the future, so by default simply +the latest version should be used. + +As a side note, in the Gradle plugin, `nonPublicMarkers`, `ignoredPackages` and `ignoredClasses` should treat values as +regular Java class (or package) names, so that users who already use the BCV could enable KLib validation and everything +continues works correctly, without any config updates. So for simplicity, API should threat these values the same way. + +The main scenarios for KLib dumps we have in the plugin right now are: +- merging multiple dumps together; +- extracting declarations for a subset of targets stored in the merged dump; +- updating the merged dump with an updated dump for one or several targets; +- inferring a dump for an unsupported target. + +To cover these scenarios, the following operations are proposed for the `KlibDump`: +- `merge`, that combines several dumps together; +- `remove` and `retain` operations that either removes all the specified targets (along with declarations) from a dump, +or, contrary, retain only specified targets; +- `save`, that converts a dump back into textual form; +- `infer`, that infers the dump for unsupported targets. + +Loading a dump extracted from a klib using compiler API into `KlibDump` and then converting it back to a textual dump +will produce a file that won't be bitwise identical. To hide this inconsistency, it's proposed to always convert a klib +dump into `KlibDump` when creating a new dump (i.e. there will be no intermediate step that will extract from a klib +into a textual form using the compiler API, that then should be loaded into `KlibDump`). So yet another operation that +should `KlibDump` should have is `mergeFromKlib`, that will create a dump and merge it directly to `KlibDump` given +a klib file and an optional `KLibDumpFilters`. + +All the API will be explicitly marked as experimental, so we could freely change it in the future. From 489eefaaa7c6ebc0092796b0a015d3dbea505a25 Mon Sep 17 00:00:00 2001 From: Filipp Zhinkin Date: Tue, 12 Mar 2024 11:28:59 +0100 Subject: [PATCH 45/53] Addressed review comments and suggestions - fixed naming - simplified some code constructions - swapped the order of KlibTarget fields - updated merged dump's header - removed unsupported targets from target hierarchy --- docs/design/KLibSupport.md | 2 +- .../AnotherBuildConfig.klib.clash.dump | 2 +- .../AnotherBuildConfig.klib.custom.dump | 2 +- .../classes/AnotherBuildConfig.klib.dump | 2 +- ...AnotherBuildConfig.klib.renamedTarget.dump | 2 +- .../classes/AnotherBuildConfig.klib.web.dump | 2 +- ...AnotherBuildConfigLinux.klib.grouping.dump | 2 +- ...notherBuildConfigLinuxArm64Extra.klib.dump | 2 +- .../AnotherBuildConfigModified.klib.dump | 2 +- .../classes/ClassWithPublicMarkers.klib.dump | 2 +- .../examples/classes/Empty.klib.dump | 2 +- .../classes/HiddenDeclarations.klib.dump | 2 +- .../examples/classes/Properties.klib.dump | 2 +- .../examples/classes/Subclasses.klib.dump | 2 +- .../TopLevelDeclarations.klib.all.dump | 2 +- .../classes/TopLevelDeclarations.klib.dump | 2 +- .../TopLevelDeclarations.klib.unsup.dump | 2 +- .../classes/TopLevelDeclarations.klib.v1.dump | 2 +- ...lDeclarations.klib.with.guessed.linux.dump | 2 +- .../TopLevelDeclarations.klib.with.linux.dump | 2 +- .../BinaryCompatibilityValidatorPlugin.kt | 2 +- ...linKlibInferAbiForUnsupportedTargetTask.kt | 2 +- .../kotlin/api/klib/KlibSignatureVersion.kt | 4 +- src/main/kotlin/api/klib/KlibTarget.kt | 16 ++- src/main/kotlin/klib/KlibAbiDumpFileMerger.kt | 103 +++++++++--------- src/main/kotlin/klib/TargetHierarchy.kt | 45 +++----- src/test/kotlin/samples/KlibDumpSamples.kt | 16 +-- src/test/kotlin/tests/KlibAbiMergingTest.kt | 1 + src/test/kotlin/tests/KlibDumpTest.kt | 20 ++-- .../kotlin/tests/KlibTargetHierarchyTest.kt | 4 +- src/test/kotlin/tests/KlibTargetNameTest.kt | 2 +- .../merge/diverging/linuxArm64.extracted.api | 2 +- src/test/resources/merge/diverging/merged.abi | 2 +- .../merge/diverging/merged_with_aliases.abi | 2 +- .../merged_with_aliases_and_custom_names.abi | 2 +- src/test/resources/merge/guess/common.api | 2 +- src/test/resources/merge/guess/guessed.api | 2 +- .../merge/guess/linuxArm64Specific.api | 2 +- src/test/resources/merge/guess/merged.api | 2 +- .../merge/idempotent/bcv-klib-test.abi | 2 +- src/test/resources/merge/identical/merged.abi | 2 +- .../merge/parseNarrowChildrenDecls/merged.abi | 2 +- .../withoutLinuxAll.abi | 2 +- .../withoutLinuxArm64.abi | 2 +- .../resources/merge/webTargets/js.ext.abi | 2 +- .../resources/merge/webTargets/merged.abi | 2 +- .../resources/merge/webTargets/wasmJs.ext.abi | 2 +- .../merge/webTargets/wasmWasi.ext.abi | 2 +- 48 files changed, 139 insertions(+), 150 deletions(-) diff --git a/docs/design/KLibSupport.md b/docs/design/KLibSupport.md index 0ce4bc59..1eac44ff 100644 --- a/docs/design/KLibSupport.md +++ b/docs/design/KLibSupport.md @@ -53,7 +53,7 @@ in a file's header. Here's a brief example of such a merged dump file: ``` -// KLib ABI Dump +// Klib ABI Dump // Targets: [androidNativeArm32, androidNativeArm64, androidNativeX64, androidNativeX86, linuxArm64, linuxX64, mingwX64] // Alias: linux => [linuxArm64, linuxX64] // Rendering settings: diff --git a/src/functionalTest/resources/examples/classes/AnotherBuildConfig.klib.clash.dump b/src/functionalTest/resources/examples/classes/AnotherBuildConfig.klib.clash.dump index 0aba4248..78f09cea 100644 --- a/src/functionalTest/resources/examples/classes/AnotherBuildConfig.klib.clash.dump +++ b/src/functionalTest/resources/examples/classes/AnotherBuildConfig.klib.clash.dump @@ -1,4 +1,4 @@ -// KLib ABI Dump +// Klib ABI Dump // Targets: [androidNativeArm32, androidNativeArm64, androidNativeX64, androidNativeX86, linuxArm64, linuxX64.linux, mingwX64] // Alias: linux => [linuxArm64, linuxX64.linux] // Rendering settings: diff --git a/src/functionalTest/resources/examples/classes/AnotherBuildConfig.klib.custom.dump b/src/functionalTest/resources/examples/classes/AnotherBuildConfig.klib.custom.dump index 8633ae30..1adbdac8 100644 --- a/src/functionalTest/resources/examples/classes/AnotherBuildConfig.klib.custom.dump +++ b/src/functionalTest/resources/examples/classes/AnotherBuildConfig.klib.custom.dump @@ -1,4 +1,4 @@ -// KLib ABI Dump +// Klib ABI Dump // Targets: [linuxX64.linuxA, linuxX64.linuxB] // Rendering settings: // - Signature version: 2 diff --git a/src/functionalTest/resources/examples/classes/AnotherBuildConfig.klib.dump b/src/functionalTest/resources/examples/classes/AnotherBuildConfig.klib.dump index c1884a35..9511bef9 100644 --- a/src/functionalTest/resources/examples/classes/AnotherBuildConfig.klib.dump +++ b/src/functionalTest/resources/examples/classes/AnotherBuildConfig.klib.dump @@ -1,4 +1,4 @@ -// KLib ABI Dump +// Klib ABI Dump // Targets: [androidNativeArm32, androidNativeArm64, androidNativeX64, androidNativeX86, linuxArm64, linuxX64, mingwX64] // Rendering settings: // - Signature version: 2 diff --git a/src/functionalTest/resources/examples/classes/AnotherBuildConfig.klib.renamedTarget.dump b/src/functionalTest/resources/examples/classes/AnotherBuildConfig.klib.renamedTarget.dump index 7a63e873..8b84f00e 100644 --- a/src/functionalTest/resources/examples/classes/AnotherBuildConfig.klib.renamedTarget.dump +++ b/src/functionalTest/resources/examples/classes/AnotherBuildConfig.klib.renamedTarget.dump @@ -1,4 +1,4 @@ -// KLib ABI Dump +// Klib ABI Dump // Targets: [androidNativeArm32, androidNativeArm64, androidNativeX64, androidNativeX86, linuxArm64.linux, linuxX64, mingwX64] // Rendering settings: // - Signature version: 2 diff --git a/src/functionalTest/resources/examples/classes/AnotherBuildConfig.klib.web.dump b/src/functionalTest/resources/examples/classes/AnotherBuildConfig.klib.web.dump index cb7fa733..bebc349d 100644 --- a/src/functionalTest/resources/examples/classes/AnotherBuildConfig.klib.web.dump +++ b/src/functionalTest/resources/examples/classes/AnotherBuildConfig.klib.web.dump @@ -1,4 +1,4 @@ -// KLib ABI Dump +// Klib ABI Dump // Targets: [androidNativeArm32, androidNativeArm64, androidNativeX64, androidNativeX86, js, linuxArm64, linuxX64, mingwX64, wasmJs, wasmWasi] // Rendering settings: // - Signature version: 2 diff --git a/src/functionalTest/resources/examples/classes/AnotherBuildConfigLinux.klib.grouping.dump b/src/functionalTest/resources/examples/classes/AnotherBuildConfigLinux.klib.grouping.dump index 41092e34..f67ac443 100644 --- a/src/functionalTest/resources/examples/classes/AnotherBuildConfigLinux.klib.grouping.dump +++ b/src/functionalTest/resources/examples/classes/AnotherBuildConfigLinux.klib.grouping.dump @@ -1,4 +1,4 @@ -// KLib ABI Dump +// Klib ABI Dump // Targets: [androidNativeArm32, androidNativeArm64, androidNativeX64, androidNativeX86, linuxArm64, linuxX64, mingwX64] // Alias: linux => [linuxArm64, linuxX64] // Rendering settings: diff --git a/src/functionalTest/resources/examples/classes/AnotherBuildConfigLinuxArm64Extra.klib.dump b/src/functionalTest/resources/examples/classes/AnotherBuildConfigLinuxArm64Extra.klib.dump index c4f9abfc..20292d7e 100644 --- a/src/functionalTest/resources/examples/classes/AnotherBuildConfigLinuxArm64Extra.klib.dump +++ b/src/functionalTest/resources/examples/classes/AnotherBuildConfigLinuxArm64Extra.klib.dump @@ -1,4 +1,4 @@ -// KLib ABI Dump +// Klib ABI Dump // Targets: [androidNativeArm32, androidNativeArm64, androidNativeX64, androidNativeX86, linuxArm64, linuxX64, mingwX64] // Rendering settings: // - Signature version: 2 diff --git a/src/functionalTest/resources/examples/classes/AnotherBuildConfigModified.klib.dump b/src/functionalTest/resources/examples/classes/AnotherBuildConfigModified.klib.dump index d24e55c2..75eb66b2 100644 --- a/src/functionalTest/resources/examples/classes/AnotherBuildConfigModified.klib.dump +++ b/src/functionalTest/resources/examples/classes/AnotherBuildConfigModified.klib.dump @@ -1,4 +1,4 @@ -// KLib ABI Dump +// Klib ABI Dump // Targets: [androidNativeArm32, androidNativeArm64, androidNativeX64, androidNativeX86, linuxArm64, linuxX64, mingwX64] // Rendering settings: // - Signature version: 2 diff --git a/src/functionalTest/resources/examples/classes/ClassWithPublicMarkers.klib.dump b/src/functionalTest/resources/examples/classes/ClassWithPublicMarkers.klib.dump index 7e91492e..b1e8f295 100644 --- a/src/functionalTest/resources/examples/classes/ClassWithPublicMarkers.klib.dump +++ b/src/functionalTest/resources/examples/classes/ClassWithPublicMarkers.klib.dump @@ -1,4 +1,4 @@ -// KLib ABI Dump +// Klib ABI Dump // Targets: [androidNativeArm32, androidNativeArm64, androidNativeX64, androidNativeX86, linuxArm64, linuxX64, mingwX64] // Alias: androidNative => [androidNativeArm32, androidNativeArm64, androidNativeX64, androidNativeX86] // Alias: linux => [linuxArm64, linuxX64] diff --git a/src/functionalTest/resources/examples/classes/Empty.klib.dump b/src/functionalTest/resources/examples/classes/Empty.klib.dump index 03380850..40583d92 100644 --- a/src/functionalTest/resources/examples/classes/Empty.klib.dump +++ b/src/functionalTest/resources/examples/classes/Empty.klib.dump @@ -1,4 +1,4 @@ -// KLib ABI Dump +// Klib ABI Dump // Targets: [mingwX64] // Rendering settings: // - Signature version: 2 diff --git a/src/functionalTest/resources/examples/classes/HiddenDeclarations.klib.dump b/src/functionalTest/resources/examples/classes/HiddenDeclarations.klib.dump index 337ef7a3..bed1c06f 100644 --- a/src/functionalTest/resources/examples/classes/HiddenDeclarations.klib.dump +++ b/src/functionalTest/resources/examples/classes/HiddenDeclarations.klib.dump @@ -1,4 +1,4 @@ -// KLib ABI Dump +// Klib ABI Dump // Targets: [androidNativeArm32, androidNativeArm64, androidNativeX64, androidNativeX86, linuxArm64, linuxX64, mingwX64] // Rendering settings: // - Signature version: 2 diff --git a/src/functionalTest/resources/examples/classes/Properties.klib.dump b/src/functionalTest/resources/examples/classes/Properties.klib.dump index d4ae3a68..6359372b 100644 --- a/src/functionalTest/resources/examples/classes/Properties.klib.dump +++ b/src/functionalTest/resources/examples/classes/Properties.klib.dump @@ -1,4 +1,4 @@ -// KLib ABI Dump +// Klib ABI Dump // Targets: [androidNativeArm32, androidNativeArm64, androidNativeX64, androidNativeX86, linuxArm64, linuxX64, mingwX64] // Rendering settings: // - Signature version: 2 diff --git a/src/functionalTest/resources/examples/classes/Subclasses.klib.dump b/src/functionalTest/resources/examples/classes/Subclasses.klib.dump index e06df81c..e13fa3f6 100644 --- a/src/functionalTest/resources/examples/classes/Subclasses.klib.dump +++ b/src/functionalTest/resources/examples/classes/Subclasses.klib.dump @@ -1,4 +1,4 @@ -// KLib ABI Dump +// Klib ABI Dump // Targets: [androidNativeArm32, androidNativeArm64, androidNativeX64, androidNativeX86, linuxArm64, linuxX64, mingwX64] // Rendering settings: // - Signature version: 2 diff --git a/src/functionalTest/resources/examples/classes/TopLevelDeclarations.klib.all.dump b/src/functionalTest/resources/examples/classes/TopLevelDeclarations.klib.all.dump index 5505d583..aa08591a 100644 --- a/src/functionalTest/resources/examples/classes/TopLevelDeclarations.klib.all.dump +++ b/src/functionalTest/resources/examples/classes/TopLevelDeclarations.klib.all.dump @@ -1,4 +1,4 @@ -// KLib ABI Dump +// Klib ABI Dump // Targets: [androidNativeArm32, androidNativeArm64, androidNativeX64, androidNativeX86, iosArm64, iosSimulatorArm64, iosX64, linuxArm64, linuxX64, macosArm64, macosX64, mingwX64, tvosArm64, tvosSimulatorArm64, tvosX64, watchosArm32, watchosArm64, watchosDeviceArm64, watchosSimulatorArm64, watchosX64] // Rendering settings: // - Signature version: 2 diff --git a/src/functionalTest/resources/examples/classes/TopLevelDeclarations.klib.dump b/src/functionalTest/resources/examples/classes/TopLevelDeclarations.klib.dump index 35f9ccd5..c7bb38f6 100644 --- a/src/functionalTest/resources/examples/classes/TopLevelDeclarations.klib.dump +++ b/src/functionalTest/resources/examples/classes/TopLevelDeclarations.klib.dump @@ -1,4 +1,4 @@ -// KLib ABI Dump +// Klib ABI Dump // Targets: [androidNativeArm32, androidNativeArm64, androidNativeX64, androidNativeX86, linuxArm64, linuxX64, mingwX64] // Rendering settings: // - Signature version: 2 diff --git a/src/functionalTest/resources/examples/classes/TopLevelDeclarations.klib.unsup.dump b/src/functionalTest/resources/examples/classes/TopLevelDeclarations.klib.unsup.dump index 01682364..9764aec3 100644 --- a/src/functionalTest/resources/examples/classes/TopLevelDeclarations.klib.unsup.dump +++ b/src/functionalTest/resources/examples/classes/TopLevelDeclarations.klib.unsup.dump @@ -1,4 +1,4 @@ -// KLib ABI Dump +// Klib ABI Dump // Targets: [androidNativeArm32, androidNativeArm64, androidNativeX64, androidNativeX86, linuxX64, mingwX64] // Rendering settings: // - Signature version: 2 diff --git a/src/functionalTest/resources/examples/classes/TopLevelDeclarations.klib.v1.dump b/src/functionalTest/resources/examples/classes/TopLevelDeclarations.klib.v1.dump index 23824600..9442fd64 100644 --- a/src/functionalTest/resources/examples/classes/TopLevelDeclarations.klib.v1.dump +++ b/src/functionalTest/resources/examples/classes/TopLevelDeclarations.klib.v1.dump @@ -1,4 +1,4 @@ -// KLib ABI Dump +// Klib ABI Dump // Targets: [androidNativeArm32, androidNativeArm64, androidNativeX64, androidNativeX86, linuxArm64, linuxX64, mingwX64] // Rendering settings: // - Signature version: 1 diff --git a/src/functionalTest/resources/examples/classes/TopLevelDeclarations.klib.with.guessed.linux.dump b/src/functionalTest/resources/examples/classes/TopLevelDeclarations.klib.with.guessed.linux.dump index 5da647fd..5b397f22 100644 --- a/src/functionalTest/resources/examples/classes/TopLevelDeclarations.klib.with.guessed.linux.dump +++ b/src/functionalTest/resources/examples/classes/TopLevelDeclarations.klib.with.guessed.linux.dump @@ -1,4 +1,4 @@ -// KLib ABI Dump +// Klib ABI Dump // Targets: [androidNativeArm32, androidNativeArm64, androidNativeX64, androidNativeX86, linuxArm64, linuxX64.linux, mingwX64] // Rendering settings: // - Signature version: 2 diff --git a/src/functionalTest/resources/examples/classes/TopLevelDeclarations.klib.with.linux.dump b/src/functionalTest/resources/examples/classes/TopLevelDeclarations.klib.with.linux.dump index 35f9ccd5..c7bb38f6 100644 --- a/src/functionalTest/resources/examples/classes/TopLevelDeclarations.klib.with.linux.dump +++ b/src/functionalTest/resources/examples/classes/TopLevelDeclarations.klib.with.linux.dump @@ -1,4 +1,4 @@ -// KLib ABI Dump +// Klib ABI Dump // Targets: [androidNativeArm32, androidNativeArm64, androidNativeX64, androidNativeX86, linuxArm64, linuxX64, mingwX64] // Rendering settings: // - Signature version: 2 diff --git a/src/main/kotlin/BinaryCompatibilityValidatorPlugin.kt b/src/main/kotlin/BinaryCompatibilityValidatorPlugin.kt index d189aca4..78f552d8 100644 --- a/src/main/kotlin/BinaryCompatibilityValidatorPlugin.kt +++ b/src/main/kotlin/BinaryCompatibilityValidatorPlugin.kt @@ -574,7 +574,7 @@ private class KlibValidationPipelineBuilder( true } } - .map { KlibTarget(it.targetName, extractUnderlyingTarget(it)).toString() } + .map { KlibTarget(extractUnderlyingTarget(it), it.targetName).toString() } .toSet() } } diff --git a/src/main/kotlin/KotlinKlibInferAbiForUnsupportedTargetTask.kt b/src/main/kotlin/KotlinKlibInferAbiForUnsupportedTargetTask.kt index fee4aafa..916004f7 100644 --- a/src/main/kotlin/KotlinKlibInferAbiForUnsupportedTargetTask.kt +++ b/src/main/kotlin/KotlinKlibInferAbiForUnsupportedTargetTask.kt @@ -76,7 +76,7 @@ internal abstract class KotlinKlibInferAbiForUnsupportedTargetTask : DefaultTask @OptIn(ExperimentalBCVApi::class) @TaskAction internal fun generate() { - val unsupportedTarget = KlibTarget(unsupportedTargetName, unsupportedTargetCanonicalName) + val unsupportedTarget = KlibTarget(unsupportedTargetCanonicalName, unsupportedTargetName) val supportedTargetNames = supportedTargets.get().map { KlibTarget.parse(it) }.toSet() // Find a set of supported targets that are closer to unsupported target in the hierarchy. // Note that dumps are stored using configurable name, but grouped by the canonical target name. diff --git a/src/main/kotlin/api/klib/KlibSignatureVersion.kt b/src/main/kotlin/api/klib/KlibSignatureVersion.kt index d7b63ad3..eee9ba20 100644 --- a/src/main/kotlin/api/klib/KlibSignatureVersion.kt +++ b/src/main/kotlin/api/klib/KlibSignatureVersion.kt @@ -20,9 +20,7 @@ public class KlibSignatureVersion internal constructor(internal val version: Int override fun equals(other: Any?): Boolean { if (this === other) return true - if (javaClass != other?.javaClass) return false - - other as KlibSignatureVersion + if (other !is KlibSignatureVersion) return false return version == other.version } diff --git a/src/main/kotlin/api/klib/KlibTarget.kt b/src/main/kotlin/api/klib/KlibTarget.kt index ad81b09b..bfcc78da 100644 --- a/src/main/kotlin/api/klib/KlibTarget.kt +++ b/src/main/kotlin/api/klib/KlibTarget.kt @@ -17,14 +17,14 @@ package kotlinx.validation.api.klib */ public class KlibTarget internal constructor( /** - * A name of a target that could be configured by a user in a build script. - * Usually, it's the same name as [targetName]. + * An actual name of a target that remains unaffected by a custom name settings in a build script. */ - public val configurableName: String, + public val targetName: String, /** - * An actual name of a target that remains unaffected by a custom name settings in a build script. + * A name of a target that could be configured by a user in a build script. + * Usually, it's the same name as [targetName]. */ - public val targetName: String + public val configurableName: String ) { init { require(!configurableName.contains(".")) { @@ -52,7 +52,7 @@ public class KlibTarget internal constructor( "Target has illegal name format: \"$value\", expected: ." ) } - return KlibTarget(parts[1], parts[0]) + return KlibTarget(parts[0], parts[1]) } } @@ -62,9 +62,7 @@ public class KlibTarget internal constructor( override fun equals(other: Any?): Boolean { if (this === other) return true - if (javaClass != other?.javaClass) return false - - other as KlibTarget + if (other !is KlibTarget) return false if (configurableName != other.configurableName) return false if (targetName != other.targetName) return false diff --git a/src/main/kotlin/klib/KlibAbiDumpFileMerger.kt b/src/main/kotlin/klib/KlibAbiDumpFileMerger.kt index 3bb5b17e..b4e504d4 100644 --- a/src/main/kotlin/klib/KlibAbiDumpFileMerger.kt +++ b/src/main/kotlin/klib/KlibAbiDumpFileMerger.kt @@ -11,7 +11,7 @@ import java.nio.file.Files import java.util.* import kotlin.Comparator -private class LinesProvider(private val lines: Iterator) : Iterator { +private class PeekingLineIterator(private val lines: Iterator) : Iterator { private var nextLine: String? = null public fun peek(): String? { @@ -39,7 +39,7 @@ private class LinesProvider(private val lines: Iterator) : Iterator = mutableSetOf() + private val _targets: MutableSet = mutableSetOf() private val headerContent: MutableList = mutableListOf() private val topLevelDeclaration: DeclarationContainer = DeclarationContainer("") /** * All targets for which this dump contains declarations. */ - internal val targets: Set = targetsMut + internal val targets: Set = _targets internal fun merge(file: File, configurableTargetName: String? = null) { require(file.exists()) { "File does not exist: $file" } + // TODO: replace with file.toPath().useLines once language version get upgraded Files.lines(file.toPath()).use { merge(it.iterator(), configurableTargetName) } } internal fun merge(lines: Iterator, configurableTargetName: String? = null) { - merge(LinesProvider(lines), configurableTargetName) + merge(PeekingLineIterator(lines), configurableTargetName) } - private fun merge(lines: LinesProvider, configurableTargetName: String?) { + private fun merge(lines: PeekingLineIterator, configurableTargetName: String?) { require(lines.peek() != null) { "File is empty" } val isMergedFile = lines.determineFileType() @@ -123,7 +124,7 @@ internal class KlibAbiDumpMerger { check(it.isEmpty()) { "This dump and a file to merge share some targets: $it" } } - if (this.targetsMut.isEmpty()) { + if (this._targets.isEmpty()) { headerContent.addAll(header.content) } else if (headerContent != header.content) { throw IllegalStateException( @@ -131,7 +132,7 @@ internal class KlibAbiDumpMerger { + headerContent.toString() + "\n\n\n" + header.content.toString() ) } - this.targetsMut.addAll(bcvTargets) + this._targets.addAll(bcvTargets) topLevelDeclaration.targets.addAll(bcvTargets) // All declarations belonging to the same scope have equal indentation. @@ -141,7 +142,7 @@ internal class KlibAbiDumpMerger { // and we must pop one or several declarations out of the parsing stack. var currentContainer = topLevelDeclaration var depth = -1 - val targetsStack = Stack>().apply { push(bcvTargets) } + val targetsStack = mutableListOf>().apply { add(bcvTargets) } while (lines.hasNext()) { val line = lines.peek()!! @@ -153,7 +154,7 @@ internal class KlibAbiDumpMerger { depth == lineDepth -> { // pop it off to swap previous value from the same depth, // parseDeclaration will update it - targetsStack.pop() + targetsStack.removeLast() currentContainer = lines.parseDeclaration(lineDepth, currentContainer.parent!!, targetsStack, aliases) } @@ -174,7 +175,7 @@ internal class KlibAbiDumpMerger { else -> { while (currentContainer.text.depth() > lineDepth) { currentContainer = currentContainer.parent!! - targetsStack.pop() + targetsStack.removeLast() } // If the line is '}' - add it as a terminator to the corresponding declaration, it'll simplify // dumping the merged file back to text format. @@ -190,7 +191,7 @@ internal class KlibAbiDumpMerger { } } - private fun LinesProvider.parseTargets(configurableTargetName: String?): Set { + private fun PeekingLineIterator.parseTargets(configurableTargetName: String?): Set { val line = peek() require(line != null) { "List of targets expected, but there are no more lines left." @@ -204,12 +205,12 @@ internal class KlibAbiDumpMerger { "Can't use configurableTargetName ($configurableTargetName) for a multi-target dump: $targets" } if (configurableTargetName != null) { - return setOf(KlibTarget(configurableTargetName, targets.first().targetName)) + return setOf(KlibTarget(targets.first().targetName, configurableTargetName)) } return targets } - private fun LinesProvider.parseAliases(): Map> { + private fun PeekingLineIterator.parseAliases(): Map> { val aliases = mutableMapOf>() while (peek()?.startsWith(ALIAS_PREFIX) == true) { val line = next() @@ -231,7 +232,7 @@ internal class KlibAbiDumpMerger { return aliases } - private fun LinesProvider.parseFileHeader( + private fun PeekingLineIterator.parseFileHeader( isMergedFile: Boolean, configurableTargetName: String? ): KlibAbiDumpHeader { @@ -291,10 +292,11 @@ internal class KlibAbiDumpMerger { return setOf(KlibTarget(configurableTargetName)) } if (platformString != "NATIVE") { + val platformStringLc = platformString.toLowerCase(Locale.getDefault()) return if (configurableTargetName == null) { - setOf(KlibTarget(platformString.toLowerCase())) + setOf(KlibTarget(platformStringLc)) } else { - setOf(KlibTarget(configurableTargetName, platformString.toLowerCase())) + setOf(KlibTarget(platformStringLc, configurableTargetName)) } } @@ -307,12 +309,12 @@ internal class KlibAbiDumpMerger { "Can't use configurableTargetName ($configurableTargetName) for a multi-target dump: $targetsList" } if (targetsList.size == 1 && configurableTargetName != null) { - return setOf(KlibTarget(configurableTargetName, targetsList.first())) + return setOf(KlibTarget(targetsList.first(), configurableTargetName)) } return targetsList.asSequence().map { KlibTarget(it) }.toSet() } - private fun LinesProvider.determineFileType(): Boolean { + private fun PeekingLineIterator.determineFileType(): Boolean { val headerLine = peek() ?: throw IllegalStateException("File is empty") if (headerLine.trimEnd() == MERGED_DUMP_FILE_HEADER) { return true @@ -331,10 +333,10 @@ internal class KlibAbiDumpMerger { ) } - private fun LinesProvider.parseDeclaration( + private fun PeekingLineIterator.parseDeclaration( depth: Int, parent: DeclarationContainer, - parentTargetsStack: Stack>, + parentTargetsStack: MutableList>, aliases: Map> ): DeclarationContainer { val line = peek()!! @@ -346,12 +348,12 @@ internal class KlibAbiDumpMerger { val expandedTargets = targets.flatMap { aliases[it.configurableName] ?: listOf(it) }.toSet() - parentTargetsStack.push(expandedTargets) + parentTargetsStack.add(expandedTargets) parent.createOrUpdateChildren(next(), expandedTargets) } else { // Inherit all targets from a parent - parentTargetsStack.push(parentTargetsStack.peek()) - parent.createOrUpdateChildren(next(), parentTargetsStack.peek()) + parentTargetsStack.add(parentTargetsStack.last()) + parent.createOrUpdateChildren(next(), parentTargetsStack.last()) } } @@ -369,7 +371,7 @@ internal class KlibAbiDumpMerger { appendable.append(it).append('\n') } topLevelDeclaration.children.values.sortedWith(DeclarationsComparator).forEach { - it.dump(appendable, targetsMut, formatter) + it.dump(appendable, _targets, formatter) } } @@ -391,11 +393,11 @@ internal class KlibAbiDumpMerger { * If some declaration was declared only for [target], it will be removed from the dump. */ fun remove(target: KlibTarget) { - if (!targetsMut.contains(target)) { + if (!_targets.contains(target)) { return } - targetsMut.remove(target) + _targets.remove(target) topLevelDeclaration.remove(target) } @@ -406,24 +408,24 @@ internal class KlibAbiDumpMerger { * 2) it defined for all [targets], but contains target-specific child declaration. */ fun retainTargetSpecificAbi(target: KlibTarget) { - if (!targetsMut.contains(target)) { - targetsMut.clear() + if (!_targets.contains(target)) { + _targets.clear() topLevelDeclaration.children.clear() topLevelDeclaration.targets.clear() return } - topLevelDeclaration.retainSpecific(target, targetsMut) - targetsMut.retainAll(setOf(target)) + topLevelDeclaration.retainSpecific(target, _targets) + _targets.retainAll(setOf(target)) } /** * Remove all declarations that are not defined for all [KlibAbiDumpMerger.targets]. */ fun retainCommonAbi() { - topLevelDeclaration.retainCommon(targetsMut) + topLevelDeclaration.retainCommon(_targets) if (topLevelDeclaration.children.isEmpty()) { - targetsMut.clear() + _targets.clear() } } @@ -432,15 +434,15 @@ internal class KlibAbiDumpMerger { * The dump [other] should contain exactly one target and this dump should not contain that target. */ fun mergeTargetSpecific(other: KlibAbiDumpMerger) { - require(other.targetsMut.size == 1) { + require(other._targets.size == 1) { "The dump to merge in should have a single target, but its targets are: ${other.targets}" } - require(other.targetsMut.first() !in targetsMut) { + require(other._targets.first() !in _targets) { "Targets of this dump and the dump to merge into it should not intersect. " + "Common target: ${other.targets.first()}}" } - targetsMut.addAll(other.targetsMut) + _targets.addAll(other._targets) topLevelDeclaration.mergeTargetSpecific(other.topLevelDeclaration) } @@ -464,7 +466,7 @@ internal class KlibAbiDumpMerger { } } - targetsMut.addAll(other.targetsMut) + _targets.addAll(other._targets) topLevelDeclaration.merge(other.topLevelDeclaration) } @@ -472,16 +474,14 @@ internal class KlibAbiDumpMerger { * For each declaration change targets to a specified [targets] set. */ fun overrideTargets(targets: Set) { - targetsMut.clear() - targetsMut.addAll(targets) + _targets.clear() + _targets.addAll(targets) topLevelDeclaration.overrideTargets(targets) } internal fun visit(action: (DeclarationContainer) -> Unit) { - topLevelDeclaration.children.forEach { - action(it.value) - } + topLevelDeclaration.children.values.forEach(action) } } @@ -626,13 +626,17 @@ internal class DeclarationContainer(val text: String, val parent: DeclarationCon // TODO: optimize private object DeclarationsComparator : Comparator { + private fun Set.serializeAndSort(): MutableList { + return this.mapTo(mutableListOf()) { it.toString() }.apply { sort() } + } + override fun compare(c0: DeclarationContainer, c1: DeclarationContainer): Int { return if (c0.targets == c1.targets) { c0.text.compareTo(c1.text) } else { if (c0.targets.size == c1.targets.size) { - val c0targets = c0.targets.asSequence().map { it.toString() }.sorted().iterator() - val c1targets = c1.targets.asSequence().map { it.toString() }.sorted().iterator() + val c0targets = c0.targets.serializeAndSort().iterator() + val c1targets = c1.targets.serializeAndSort().iterator() var result = 0 while (c1targets.hasNext() && c0targets.hasNext() && result == 0) { result = c0targets.next().compareTo(c1targets.next()) @@ -654,7 +658,7 @@ internal class KLibsTargetsFormatter(klibDump: KlibAbiDumpMerger) { init { val allTargets = klibDump.targets val aliasesBuilder = mutableListOf() - TargetHierarchy.hierarchyIndex.asSequence() + TargetHierarchy.hierarchyIndex.entries // place smaller groups (more specific groups) closer to the beginning of the list .sortedWith(compareBy({ it.value.allLeafs.size }, { it.key })) .forEach { @@ -715,15 +719,16 @@ internal class KLibsTargetsFormatter(klibDump: KlibAbiDumpMerger) { } fun formatDeclarationTargets(targets: Set): String { - val targetsMut = targets.toMutableSet() + val mutableTargets = targets.toMutableSet() val resultingTargets = mutableListOf() for (alias in aliases) { - if (targetsMut == alias.targets) { - targetsMut.removeAll(alias.targets) + if (mutableTargets == alias.targets) { + mutableTargets.clear() resultingTargets.add(alias.name) + break } } - resultingTargets.addAll(targetsMut.map { it.toString() }) + resultingTargets.addAll(mutableTargets.map { it.toString() }) return resultingTargets.sorted().joinToString( prefix = TARGETS_LIST_PREFIX, postfix = TARGETS_LIST_SUFFIX, diff --git a/src/main/kotlin/klib/TargetHierarchy.kt b/src/main/kotlin/klib/TargetHierarchy.kt index 900ddf28..b6bd6e1f 100644 --- a/src/main/kotlin/klib/TargetHierarchy.kt +++ b/src/main/kotlin/klib/TargetHierarchy.kt @@ -18,10 +18,6 @@ internal object TargetHierarchy { it.parent = this } } - - fun visit(visitor: (Node) -> T): T { - return visitor(this) - } } data class NodeClosure(val node: Node, val allLeafs: Set) @@ -33,7 +29,6 @@ internal object TargetHierarchy { Node("js"), Node("wasmJs"), Node("wasmWasi"), - Node("wasm32"), Node( "native", Node( @@ -46,8 +41,6 @@ internal object TargetHierarchy { Node("linuxArm64"), Node("linuxArm32Hfp"), Node("linuxX64"), - Node("linuxMips32"), - Node("linuxMipsel32") ), Node( "androidNative", @@ -89,24 +82,22 @@ internal object TargetHierarchy { ) ) - init { - val closure = mutableMapOf() - - fun collectLeafs(node: Node): Set { - if (node.children.isEmpty()) { - closure[node.name] = NodeClosure(node, setOf(node.name)) - return setOf(node.name) - } - val leafs = mutableSetOf() - node.children.forEach { - leafs.addAll(it.visit(::collectLeafs)) - } - closure[node.name] = NodeClosure(node, leafs) - return leafs + private fun Node.collectLeafs(to: MutableMap): Set { + val leafs = mutableSetOf() + if (children.isEmpty()) { + leafs.add(name) + } else { + children.forEach { leafs.addAll(it.collectLeafs(to)) } } - val leafs = hierarchy.visit(::collectLeafs) - closure[hierarchy.name] = NodeClosure(hierarchy, leafs) - hierarchyIndex = closure + to[name] = NodeClosure(this, leafs) + return leafs + } + + init { + val index = mutableMapOf() + val leafs = hierarchy.collectLeafs(index) + index[hierarchy.name] = NodeClosure(hierarchy, leafs) + hierarchyIndex = index } fun parent(targetOrGroup: String): String? { @@ -142,9 +133,5 @@ internal val konanTargetNameMapping = mapOf( "ios_arm32" to "iosArm32", "watchos_x86" to "watchosX86", "linux_arm32_hfp" to "linuxArm32Hfp", - "mingw_x86" to "mingwX86", - "linux_mips32" to "linuxMips32", - "linux_mipsel32" to "linuxMipsel32", - "wasm32" to "wasm32" + "mingw_x86" to "mingwX86" ) - diff --git a/src/test/kotlin/samples/KlibDumpSamples.kt b/src/test/kotlin/samples/KlibDumpSamples.kt index 6c5971bf..f3cf5e01 100644 --- a/src/test/kotlin/samples/KlibDumpSamples.kt +++ b/src/test/kotlin/samples/KlibDumpSamples.kt @@ -77,7 +77,7 @@ class KlibDumpSamples { val mergedDumpContent = buildString { mergedDump.saveTo(this) } assertEquals(""" - // KLib ABI Dump + // Klib ABI Dump // Targets: [linuxArm64, linuxX64] // Rendering settings: // - Signature version: 2 @@ -145,7 +145,7 @@ class KlibDumpSamples { val mergedDumpContent = buildString { mergedDump.saveTo(this) } assertEquals(""" - // KLib ABI Dump + // Klib ABI Dump // Targets: [linuxArm64, linuxX64.linuxX86_64] // Rendering settings: // - Signature version: 2 @@ -167,7 +167,7 @@ class KlibDumpSamples { mergedDump.remove(listOf(KlibTarget.parse("linuxX64.linuxX86_64"))) val filteredDumpContent = buildString { mergedDump.saveTo(this) } assertEquals(""" - // KLib ABI Dump + // Klib ABI Dump // Targets: [linuxArm64] // Rendering settings: // - Signature version: 2 @@ -190,7 +190,7 @@ class KlibDumpSamples { fun extractTargets() { // Oh no, we're running on Windows and Apple targets are unsupported, let's filter it out! val mergedDump = createDumpWithContent(""" - // KLib ABI Dump + // Klib ABI Dump // Targets: [iosArm64, iosSimulatorArm64, iosX64, linuxArm64, linuxX64] // Rendering settings: // - Signature version: 2 @@ -214,7 +214,7 @@ class KlibDumpSamples { val filteredDumpContent = buildString { dump.saveTo(this) } assertEquals(""" - // KLib ABI Dump + // Klib ABI Dump // Targets: [linuxArm64, linuxX64] // Rendering settings: // - Signature version: 2 @@ -235,7 +235,7 @@ class KlibDumpSamples { val unsupportedTarget = KlibTarget.parse("iosArm64") // Thankfully, we have an old merged dump ... val oldMergedDump = createDumpWithContent(""" - // KLib ABI Dump + // Klib ABI Dump // Targets: [iosArm64, linuxArm64] // Rendering settings: // - Signature version: 2 @@ -251,7 +251,7 @@ class KlibDumpSamples { // ... and a new dump for linuxArm64 val linuxDump = createDumpWithContent(""" - // KLib ABI Dump + // Klib ABI Dump // Targets: [linuxArm64] // Rendering settings: // - Signature version: 2 @@ -273,7 +273,7 @@ class KlibDumpSamples { val inferredDumpContent = buildString { inferredIosArm64Dump.saveTo(this) } assertEquals(""" - // KLib ABI Dump + // Klib ABI Dump // Targets: [iosArm64] // Rendering settings: // - Signature version: 2 diff --git a/src/test/kotlin/tests/KlibAbiMergingTest.kt b/src/test/kotlin/tests/KlibAbiMergingTest.kt index 674594aa..5b4e0116 100644 --- a/src/test/kotlin/tests/KlibAbiMergingTest.kt +++ b/src/test/kotlin/tests/KlibAbiMergingTest.kt @@ -138,6 +138,7 @@ class KlibAbiMergingTest { } @Test + @Ignore fun read() { val klib = KlibAbiDumpMerger() klib.merge(file("/merge/idempotent/bcv-klib-test.abi")) diff --git a/src/test/kotlin/tests/KlibDumpTest.kt b/src/test/kotlin/tests/KlibDumpTest.kt index 6018569c..8f1fc7af 100644 --- a/src/test/kotlin/tests/KlibDumpTest.kt +++ b/src/test/kotlin/tests/KlibDumpTest.kt @@ -43,7 +43,7 @@ private val rawLinuxDump = """ """.trimIndent() private val mergedLinuxDump = """ - // KLib ABI Dump + // Klib ABI Dump // Targets: [linuxX64] // Rendering settings: // - Signature version: 2 @@ -63,7 +63,7 @@ private val mergedLinuxDump = """ private val mergedLinuxDumpWithTargetSpecificDeclaration = """ - // KLib ABI Dump + // Klib ABI Dump // Targets: [linuxArm64, linuxX64] // Rendering settings: // - Signature version: 2 @@ -88,7 +88,7 @@ private val mergedLinuxDumpWithTargetSpecificDeclaration = """ """.trimIndent() private val mergedLinuxArm64Dump = """ - // KLib ABI Dump + // Klib ABI Dump // Targets: [linuxArm64] // Rendering settings: // - Signature version: 2 @@ -107,7 +107,7 @@ private val mergedLinuxArm64Dump = """ """.trimIndent() private val mergedLinuxDumpWithCustomName = """ - // KLib ABI Dump + // Klib ABI Dump // Targets: [linuxX64.testTarget] // Rendering settings: // - Signature version: 2 @@ -140,7 +140,7 @@ private val rawMultitargetDump = """ """.trimIndent() private val mergedMultitargetDump = """ - // KLib ABI Dump + // Klib ABI Dump // Targets: [androidNativeArm32, androidNativeArm64, androidNativeX64, androidNativeX86, iosArm64, iosSimulatorArm64, iosX64, linuxArm32Hfp, linuxArm64, linuxX64, macosArm64, macosX64, mingwX64, tvosArm64, tvosSimulatorArm64, tvosX64, watchosArm32, watchosArm64, watchosDeviceArm64, watchosSimulatorArm64, watchosX64] // Rendering settings: // - Signature version: 2 @@ -154,7 +154,7 @@ private val mergedMultitargetDump = """ private val mergedMultitargetDumpFiltered = """ - // KLib ABI Dump + // Klib ABI Dump // Targets: [androidNativeArm32] // Rendering settings: // - Signature version: 2 @@ -434,7 +434,7 @@ class KlibDumpTest { val linuxDump = KlibDump.from( asFile( """ - // KLib ABI Dump + // Klib ABI Dump // Targets: [linuxArm64] // Rendering settings: // - Signature version: 2 @@ -459,7 +459,7 @@ class KlibDumpTest { val inferredDumpContent = buildString { inferredIosArm64Dump.saveTo(this) } assertEquals( """ - // KLib ABI Dump + // Klib ABI Dump // Targets: [iosArm64] // Rendering settings: // - Signature version: 2 @@ -480,7 +480,7 @@ class KlibDumpTest { val oldDump = KlibDump.from( asFile( """ - // KLib ABI Dump + // Klib ABI Dump // Targets: [iosArm64, linuxArm64] // Rendering settings: // - Signature version: 2 @@ -508,7 +508,7 @@ class KlibDumpTest { val inferredDumpContent = buildString { inferredIosArm64Dump.saveTo(this) } assertEquals( """ - // KLib ABI Dump + // Klib ABI Dump // Targets: [iosArm64] // Rendering settings: // - Signature version: 2 diff --git a/src/test/kotlin/tests/KlibTargetHierarchyTest.kt b/src/test/kotlin/tests/KlibTargetHierarchyTest.kt index f6447c87..b8a18a57 100644 --- a/src/test/kotlin/tests/KlibTargetHierarchyTest.kt +++ b/src/test/kotlin/tests/KlibTargetHierarchyTest.kt @@ -48,10 +48,10 @@ class KlibTargetHierarchyTest { @Test fun testAllTargetsAreMapped() { val notMappedTargets = KonanTarget.predefinedTargets.keys.subtract(konanTargetNameMapping.keys) - assertTrue(notMappedTargets.isEmpty(), "Following targets are not mapped: $notMappedTargets") + assertEquals(setOf("wasm32", "linux_mips32", "linux_mipsel32"), notMappedTargets, + "Following targets are not mapped: $notMappedTargets") } - @OptIn(ExperimentalStdlibApi::class) private fun hierarchyFrom(groupOrTarget: String): List { return buildList { var i = 0 diff --git a/src/test/kotlin/tests/KlibTargetNameTest.kt b/src/test/kotlin/tests/KlibTargetNameTest.kt index 48caf32f..997cd220 100644 --- a/src/test/kotlin/tests/KlibTargetNameTest.kt +++ b/src/test/kotlin/tests/KlibTargetNameTest.kt @@ -15,7 +15,7 @@ import kotlin.test.assertNotEquals class KlibTargetNameTest { @Test fun parse() { - assertEquals("a.b", KlibTarget("b", "a").toString()) + assertEquals("a.b", KlibTarget("a", "b").toString()) assertEquals("a", KlibTarget("a").toString()) assertEquals("a", KlibTarget("a", "a").toString()) diff --git a/src/test/resources/merge/diverging/linuxArm64.extracted.api b/src/test/resources/merge/diverging/linuxArm64.extracted.api index 855bafb3..f0b0753b 100644 --- a/src/test/resources/merge/diverging/linuxArm64.extracted.api +++ b/src/test/resources/merge/diverging/linuxArm64.extracted.api @@ -1,4 +1,4 @@ -// KLib ABI Dump +// Klib ABI Dump // Targets: [linuxArm64.linux] // Rendering settings: // - Signature version: 2 diff --git a/src/test/resources/merge/diverging/merged.abi b/src/test/resources/merge/diverging/merged.abi index 26761ebc..de31ec4a 100644 --- a/src/test/resources/merge/diverging/merged.abi +++ b/src/test/resources/merge/diverging/merged.abi @@ -1,4 +1,4 @@ -// KLib ABI Dump +// Klib ABI Dump // Targets: [androidNativeArm64, linuxArm64, linuxX64, tvosX64] // Alias: linux => [linuxArm64, linuxX64] // Rendering settings: diff --git a/src/test/resources/merge/diverging/merged_with_aliases.abi b/src/test/resources/merge/diverging/merged_with_aliases.abi index 26761ebc..de31ec4a 100644 --- a/src/test/resources/merge/diverging/merged_with_aliases.abi +++ b/src/test/resources/merge/diverging/merged_with_aliases.abi @@ -1,4 +1,4 @@ -// KLib ABI Dump +// Klib ABI Dump // Targets: [androidNativeArm64, linuxArm64, linuxX64, tvosX64] // Alias: linux => [linuxArm64, linuxX64] // Rendering settings: diff --git a/src/test/resources/merge/diverging/merged_with_aliases_and_custom_names.abi b/src/test/resources/merge/diverging/merged_with_aliases_and_custom_names.abi index 32e8bdbf..1013b501 100644 --- a/src/test/resources/merge/diverging/merged_with_aliases_and_custom_names.abi +++ b/src/test/resources/merge/diverging/merged_with_aliases_and_custom_names.abi @@ -1,4 +1,4 @@ -// KLib ABI Dump +// Klib ABI Dump // Targets: [androidNativeArm64.android, linuxArm64.linux, linuxX64, tvosX64] // Alias: linux => [linuxArm64.linux, linuxX64] // Rendering settings: diff --git a/src/test/resources/merge/guess/common.api b/src/test/resources/merge/guess/common.api index d90c8cf5..b2823f75 100644 --- a/src/test/resources/merge/guess/common.api +++ b/src/test/resources/merge/guess/common.api @@ -1,4 +1,4 @@ -// KLib ABI Dump +// Klib ABI Dump // Targets: [androidNativeArm64, linuxX64, tvOsX64] // Rendering settings: // - Signature version: 2 diff --git a/src/test/resources/merge/guess/guessed.api b/src/test/resources/merge/guess/guessed.api index 6dbdad42..ebed8a76 100644 --- a/src/test/resources/merge/guess/guessed.api +++ b/src/test/resources/merge/guess/guessed.api @@ -1,4 +1,4 @@ -// KLib ABI Dump +// Klib ABI Dump // Targets: [linuxArm64] // Rendering settings: // - Signature version: 2 diff --git a/src/test/resources/merge/guess/linuxArm64Specific.api b/src/test/resources/merge/guess/linuxArm64Specific.api index 2ece7ab5..f52246ee 100644 --- a/src/test/resources/merge/guess/linuxArm64Specific.api +++ b/src/test/resources/merge/guess/linuxArm64Specific.api @@ -1,4 +1,4 @@ -// KLib ABI Dump +// Klib ABI Dump // Targets: [linuxArm64] // Rendering settings: // - Signature version: 2 diff --git a/src/test/resources/merge/guess/merged.api b/src/test/resources/merge/guess/merged.api index 12322ba5..adf561ec 100644 --- a/src/test/resources/merge/guess/merged.api +++ b/src/test/resources/merge/guess/merged.api @@ -1,4 +1,4 @@ -// KLib ABI Dump +// Klib ABI Dump // Targets: [androidNativeArm64, linuxArm64, linuxX64, tvOsX64] // Rendering settings: // - Signature version: 2 diff --git a/src/test/resources/merge/idempotent/bcv-klib-test.abi b/src/test/resources/merge/idempotent/bcv-klib-test.abi index 12c1d64b..44c339d4 100644 --- a/src/test/resources/merge/idempotent/bcv-klib-test.abi +++ b/src/test/resources/merge/idempotent/bcv-klib-test.abi @@ -1,4 +1,4 @@ -// KLib ABI Dump +// Klib ABI Dump // Targets: [androidNativeArm32, androidNativeArm64, iosSimulatorArm64, linuxArm64, linuxX64, macosArm64, tvosArm64, tvosSimulatorArm64, tvosX64] // Alias: apple => [iosSimulatorArm64, macosArm64, tvosArm64, tvosSimulatorArm64, tvosX64] // Alias: linux => [linuxArm64, linuxX64] diff --git a/src/test/resources/merge/identical/merged.abi b/src/test/resources/merge/identical/merged.abi index 087ab730..27a1c6d6 100644 --- a/src/test/resources/merge/identical/merged.abi +++ b/src/test/resources/merge/identical/merged.abi @@ -1,4 +1,4 @@ -// KLib ABI Dump +// Klib ABI Dump // Targets: [linuxX64, macosArm64] // Rendering settings: // - Signature version: 2 diff --git a/src/test/resources/merge/parseNarrowChildrenDecls/merged.abi b/src/test/resources/merge/parseNarrowChildrenDecls/merged.abi index 5d24a140..e9a1408a 100644 --- a/src/test/resources/merge/parseNarrowChildrenDecls/merged.abi +++ b/src/test/resources/merge/parseNarrowChildrenDecls/merged.abi @@ -1,4 +1,4 @@ -// KLib ABI Dump +// Klib ABI Dump // Targets: [androidNativeArm64, linuxArm64, linuxX64, tvOsX64] // Rendering settings: // - Signature version: 2 diff --git a/src/test/resources/merge/parseNarrowChildrenDecls/withoutLinuxAll.abi b/src/test/resources/merge/parseNarrowChildrenDecls/withoutLinuxAll.abi index 31b540d9..07552376 100644 --- a/src/test/resources/merge/parseNarrowChildrenDecls/withoutLinuxAll.abi +++ b/src/test/resources/merge/parseNarrowChildrenDecls/withoutLinuxAll.abi @@ -1,4 +1,4 @@ -// KLib ABI Dump +// Klib ABI Dump // Targets: [androidNativeArm64, tvOsX64] // Rendering settings: // - Signature version: 2 diff --git a/src/test/resources/merge/parseNarrowChildrenDecls/withoutLinuxArm64.abi b/src/test/resources/merge/parseNarrowChildrenDecls/withoutLinuxArm64.abi index d0da23f4..f8ae14ba 100644 --- a/src/test/resources/merge/parseNarrowChildrenDecls/withoutLinuxArm64.abi +++ b/src/test/resources/merge/parseNarrowChildrenDecls/withoutLinuxArm64.abi @@ -1,4 +1,4 @@ -// KLib ABI Dump +// Klib ABI Dump // Targets: [androidNativeArm64, linuxX64, tvOsX64] // Rendering settings: // - Signature version: 2 diff --git a/src/test/resources/merge/webTargets/js.ext.abi b/src/test/resources/merge/webTargets/js.ext.abi index 61cad6c7..fdb21462 100644 --- a/src/test/resources/merge/webTargets/js.ext.abi +++ b/src/test/resources/merge/webTargets/js.ext.abi @@ -1,4 +1,4 @@ -// KLib ABI Dump +// Klib ABI Dump // Targets: [js] // Rendering settings: // - Signature version: 2 diff --git a/src/test/resources/merge/webTargets/merged.abi b/src/test/resources/merge/webTargets/merged.abi index 8bb24824..29f24c8f 100644 --- a/src/test/resources/merge/webTargets/merged.abi +++ b/src/test/resources/merge/webTargets/merged.abi @@ -1,4 +1,4 @@ -// KLib ABI Dump +// Klib ABI Dump // Targets: [js, wasmJs, wasmWasi] // Rendering settings: // - Signature version: 2 diff --git a/src/test/resources/merge/webTargets/wasmJs.ext.abi b/src/test/resources/merge/webTargets/wasmJs.ext.abi index af4f7f4b..44cb7a7f 100644 --- a/src/test/resources/merge/webTargets/wasmJs.ext.abi +++ b/src/test/resources/merge/webTargets/wasmJs.ext.abi @@ -1,4 +1,4 @@ -// KLib ABI Dump +// Klib ABI Dump // Targets: [wasmJs] // Rendering settings: // - Signature version: 2 diff --git a/src/test/resources/merge/webTargets/wasmWasi.ext.abi b/src/test/resources/merge/webTargets/wasmWasi.ext.abi index 1451d373..6e7083ed 100644 --- a/src/test/resources/merge/webTargets/wasmWasi.ext.abi +++ b/src/test/resources/merge/webTargets/wasmWasi.ext.abi @@ -1,4 +1,4 @@ -// KLib ABI Dump +// Klib ABI Dump // Targets: [wasmWasi] // Rendering settings: // - Signature version: 2 From a68f661e454c207c76e3f6bb3d232cff64082c26 Mon Sep 17 00:00:00 2001 From: Filipp Zhinkin Date: Tue, 12 Mar 2024 11:43:45 +0100 Subject: [PATCH 46/53] Use a length of path from a target hierarchy root to a node as a measure of a node being more or less common --- src/main/kotlin/klib/KlibAbiDumpFileMerger.kt | 7 +++++-- src/main/kotlin/klib/TargetHierarchy.kt | 13 +++++++------ src/test/kotlin/tests/KlibAbiMergingTest.kt | 1 - .../resources/merge/idempotent/bcv-klib-test.abi | 2 +- 4 files changed, 13 insertions(+), 10 deletions(-) diff --git a/src/main/kotlin/klib/KlibAbiDumpFileMerger.kt b/src/main/kotlin/klib/KlibAbiDumpFileMerger.kt index b4e504d4..d1d29cbe 100644 --- a/src/main/kotlin/klib/KlibAbiDumpFileMerger.kt +++ b/src/main/kotlin/klib/KlibAbiDumpFileMerger.kt @@ -656,11 +656,14 @@ internal class KLibsTargetsFormatter(klibDump: KlibAbiDumpMerger) { private val aliases: List init { + // place more specific groups (with a higher depth value) closer to the beginning of the list + val nodesDescendingComparator = + compareByDescending> { it.value.depth } + .thenByDescending { it.key } val allTargets = klibDump.targets val aliasesBuilder = mutableListOf() TargetHierarchy.hierarchyIndex.entries - // place smaller groups (more specific groups) closer to the beginning of the list - .sortedWith(compareBy({ it.value.allLeafs.size }, { it.key })) + .sortedWith(nodesDescendingComparator) .forEach { // intersect with all targets to use only enabled targets in aliases // intersection is based on underlying target name as a set of such names is fixed diff --git a/src/main/kotlin/klib/TargetHierarchy.kt b/src/main/kotlin/klib/TargetHierarchy.kt index b6bd6e1f..00b2e9f8 100644 --- a/src/main/kotlin/klib/TargetHierarchy.kt +++ b/src/main/kotlin/klib/TargetHierarchy.kt @@ -20,7 +20,7 @@ internal object TargetHierarchy { } } - data class NodeClosure(val node: Node, val allLeafs: Set) + data class NodeClosure(val node: Node, val depth: Int, val allLeafs: Set) internal val hierarchyIndex: Map @@ -82,21 +82,22 @@ internal object TargetHierarchy { ) ) - private fun Node.collectLeafs(to: MutableMap): Set { + private fun Node.collectLeafs(to: MutableMap, depth: Int): Set { val leafs = mutableSetOf() if (children.isEmpty()) { leafs.add(name) } else { - children.forEach { leafs.addAll(it.collectLeafs(to)) } + children.forEach { leafs.addAll(it.collectLeafs(to, depth + 1)) } } - to[name] = NodeClosure(this, leafs) + to[name] = NodeClosure(this, depth, leafs) return leafs } init { val index = mutableMapOf() - val leafs = hierarchy.collectLeafs(index) - index[hierarchy.name] = NodeClosure(hierarchy, leafs) + val rootDepth = 0 + val leafs = hierarchy.collectLeafs(index, rootDepth + 1) + index[hierarchy.name] = NodeClosure(hierarchy, rootDepth, leafs) hierarchyIndex = index } diff --git a/src/test/kotlin/tests/KlibAbiMergingTest.kt b/src/test/kotlin/tests/KlibAbiMergingTest.kt index 5b4e0116..674594aa 100644 --- a/src/test/kotlin/tests/KlibAbiMergingTest.kt +++ b/src/test/kotlin/tests/KlibAbiMergingTest.kt @@ -138,7 +138,6 @@ class KlibAbiMergingTest { } @Test - @Ignore fun read() { val klib = KlibAbiDumpMerger() klib.merge(file("/merge/idempotent/bcv-klib-test.abi")) diff --git a/src/test/resources/merge/idempotent/bcv-klib-test.abi b/src/test/resources/merge/idempotent/bcv-klib-test.abi index 44c339d4..7a81715c 100644 --- a/src/test/resources/merge/idempotent/bcv-klib-test.abi +++ b/src/test/resources/merge/idempotent/bcv-klib-test.abi @@ -1,8 +1,8 @@ // Klib ABI Dump // Targets: [androidNativeArm32, androidNativeArm64, iosSimulatorArm64, linuxArm64, linuxX64, macosArm64, tvosArm64, tvosSimulatorArm64, tvosX64] +// Alias: androidNative => [androidNativeArm32, androidNativeArm64] // Alias: apple => [iosSimulatorArm64, macosArm64, tvosArm64, tvosSimulatorArm64, tvosX64] // Alias: linux => [linuxArm64, linuxX64] -// Alias: androidNative => [androidNativeArm32, androidNativeArm64] // Rendering settings: // - Signature version: 2 // - Show manifest properties: false From 20b7fd90023cb8846f5e76211786935e3cf5ed4a Mon Sep 17 00:00:00 2001 From: Filipp Zhinkin Date: Tue, 12 Mar 2024 11:55:45 +0100 Subject: [PATCH 47/53] Consolidated all klib dump related classes in a single package --- src/main/kotlin/BinaryCompatibilityValidatorPlugin.kt | 2 +- src/main/kotlin/KotlinKlibExtractSupportedTargetsAbiTask.kt | 2 +- src/main/kotlin/KotlinKlibInferAbiForUnsupportedTargetTask.kt | 4 +--- src/main/kotlin/{ => api}/klib/KlibAbiDumpFileMerger.kt | 2 +- src/main/kotlin/api/klib/KlibDump.kt | 1 - src/main/kotlin/{ => api}/klib/TargetHierarchy.kt | 2 +- src/test/kotlin/tests/KlibAbiMergingTest.kt | 2 +- src/test/kotlin/tests/KlibTargetHierarchyTest.kt | 2 +- 8 files changed, 7 insertions(+), 10 deletions(-) rename src/main/kotlin/{ => api}/klib/KlibAbiDumpFileMerger.kt (99%) rename src/main/kotlin/{ => api}/klib/TargetHierarchy.kt (99%) diff --git a/src/main/kotlin/BinaryCompatibilityValidatorPlugin.kt b/src/main/kotlin/BinaryCompatibilityValidatorPlugin.kt index 78f552d8..1548c4db 100644 --- a/src/main/kotlin/BinaryCompatibilityValidatorPlugin.kt +++ b/src/main/kotlin/BinaryCompatibilityValidatorPlugin.kt @@ -6,7 +6,7 @@ package kotlinx.validation import kotlinx.validation.api.klib.KlibTarget -import kotlinx.validation.klib.konanTargetNameMapping +import kotlinx.validation.api.klib.konanTargetNameMapping import org.gradle.api.* import org.gradle.api.plugins.* import org.gradle.api.provider.* diff --git a/src/main/kotlin/KotlinKlibExtractSupportedTargetsAbiTask.kt b/src/main/kotlin/KotlinKlibExtractSupportedTargetsAbiTask.kt index d719d318..bd5f3ee2 100644 --- a/src/main/kotlin/KotlinKlibExtractSupportedTargetsAbiTask.kt +++ b/src/main/kotlin/KotlinKlibExtractSupportedTargetsAbiTask.kt @@ -6,7 +6,7 @@ package kotlinx.validation import kotlinx.validation.api.klib.KlibDump -import kotlinx.validation.klib.KlibAbiDumpMerger +import kotlinx.validation.api.klib.KlibAbiDumpMerger import kotlinx.validation.api.klib.KlibTarget import org.gradle.api.DefaultTask import org.gradle.api.provider.Provider diff --git a/src/main/kotlin/KotlinKlibInferAbiForUnsupportedTargetTask.kt b/src/main/kotlin/KotlinKlibInferAbiForUnsupportedTargetTask.kt index 916004f7..15cf2382 100644 --- a/src/main/kotlin/KotlinKlibInferAbiForUnsupportedTargetTask.kt +++ b/src/main/kotlin/KotlinKlibInferAbiForUnsupportedTargetTask.kt @@ -6,14 +6,12 @@ package kotlinx.validation import kotlinx.validation.api.klib.KlibDump -import kotlinx.validation.klib.KlibAbiDumpMerger import kotlinx.validation.api.klib.KlibTarget import kotlinx.validation.api.klib.inferAbi -import kotlinx.validation.klib.TargetHierarchy +import kotlinx.validation.api.klib.TargetHierarchy import org.gradle.api.DefaultTask import org.gradle.api.provider.Provider import org.gradle.api.tasks.* -import org.jetbrains.kotlin.utils.keysToMap import java.io.File /** diff --git a/src/main/kotlin/klib/KlibAbiDumpFileMerger.kt b/src/main/kotlin/api/klib/KlibAbiDumpFileMerger.kt similarity index 99% rename from src/main/kotlin/klib/KlibAbiDumpFileMerger.kt rename to src/main/kotlin/api/klib/KlibAbiDumpFileMerger.kt index d1d29cbe..b2b6b02a 100644 --- a/src/main/kotlin/klib/KlibAbiDumpFileMerger.kt +++ b/src/main/kotlin/api/klib/KlibAbiDumpFileMerger.kt @@ -3,7 +3,7 @@ * Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file. */ -package kotlinx.validation.klib +package kotlinx.validation.api.klib import kotlinx.validation.api.klib.KlibTarget import java.io.File diff --git a/src/main/kotlin/api/klib/KlibDump.kt b/src/main/kotlin/api/klib/KlibDump.kt index e6c9b502..f5c041ff 100644 --- a/src/main/kotlin/api/klib/KlibDump.kt +++ b/src/main/kotlin/api/klib/KlibDump.kt @@ -6,7 +6,6 @@ package kotlinx.validation.api.klib import kotlinx.validation.ExperimentalBCVApi -import kotlinx.validation.klib.KlibAbiDumpMerger import java.io.File import java.io.FileNotFoundException diff --git a/src/main/kotlin/klib/TargetHierarchy.kt b/src/main/kotlin/api/klib/TargetHierarchy.kt similarity index 99% rename from src/main/kotlin/klib/TargetHierarchy.kt rename to src/main/kotlin/api/klib/TargetHierarchy.kt index 00b2e9f8..fe5aac63 100644 --- a/src/main/kotlin/klib/TargetHierarchy.kt +++ b/src/main/kotlin/api/klib/TargetHierarchy.kt @@ -3,7 +3,7 @@ * Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file. */ -package kotlinx.validation.klib +package kotlinx.validation.api.klib /** * A hierarchy of KMP targets that should resemble the default hierarchy template. diff --git a/src/test/kotlin/tests/KlibAbiMergingTest.kt b/src/test/kotlin/tests/KlibAbiMergingTest.kt index 674594aa..b4fdf42c 100644 --- a/src/test/kotlin/tests/KlibAbiMergingTest.kt +++ b/src/test/kotlin/tests/KlibAbiMergingTest.kt @@ -5,7 +5,7 @@ package tests -import kotlinx.validation.klib.KlibAbiDumpMerger +import kotlinx.validation.api.klib.KlibAbiDumpMerger import kotlinx.validation.api.klib.KlibTarget import org.junit.Rule import org.junit.rules.TemporaryFolder diff --git a/src/test/kotlin/tests/KlibTargetHierarchyTest.kt b/src/test/kotlin/tests/KlibTargetHierarchyTest.kt index b8a18a57..52a298be 100644 --- a/src/test/kotlin/tests/KlibTargetHierarchyTest.kt +++ b/src/test/kotlin/tests/KlibTargetHierarchyTest.kt @@ -3,7 +3,7 @@ * Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file. */ -package kotlinx.validation.klib +package kotlinx.validation.api.klib import org.jetbrains.kotlin.konan.target.KonanTarget import org.junit.Test From 4b19820d3ddf23f243b2217f3de5447001c88ae8 Mon Sep 17 00:00:00 2001 From: Filipp Zhinkin Date: Tue, 12 Mar 2024 14:50:48 +0100 Subject: [PATCH 48/53] Use root locale instead of a default one --- src/main/kotlin/api/klib/KlibAbiDumpFileMerger.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/kotlin/api/klib/KlibAbiDumpFileMerger.kt b/src/main/kotlin/api/klib/KlibAbiDumpFileMerger.kt index b2b6b02a..3e6e308c 100644 --- a/src/main/kotlin/api/klib/KlibAbiDumpFileMerger.kt +++ b/src/main/kotlin/api/klib/KlibAbiDumpFileMerger.kt @@ -292,7 +292,7 @@ internal class KlibAbiDumpMerger { return setOf(KlibTarget(configurableTargetName)) } if (platformString != "NATIVE") { - val platformStringLc = platformString.toLowerCase(Locale.getDefault()) + val platformStringLc = platformString.toLowerCase(Locale.ROOT) return if (configurableTargetName == null) { setOf(KlibTarget(platformStringLc)) } else { From 077c7ec635409270c11fe1db27537a36496afa13 Mon Sep 17 00:00:00 2001 From: Filipp Zhinkin Date: Tue, 12 Mar 2024 17:50:35 +0100 Subject: [PATCH 49/53] Rephrased kdoc Co-authored-by: ilya-g --- src/main/kotlin/api/klib/KlibDump.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/kotlin/api/klib/KlibDump.kt b/src/main/kotlin/api/klib/KlibDump.kt index f5c041ff..853aa045 100644 --- a/src/main/kotlin/api/klib/KlibDump.kt +++ b/src/main/kotlin/api/klib/KlibDump.kt @@ -101,9 +101,9 @@ public class KlibDump { } /** - * Remove all declarations that do not belong to specified targets and remove these targets from the dump. + * Removes all declarations that do not belong to specified targets and removes these targets from the dump. * - * All targets not stored within a dump will be ignored. + * All targets in the [targets] collection not contained within this dump will be ignored. * * @sample samples.KlibDumpSamples.extractTargets */ From c1c99215268b1b31e8d47982964b74e900367679 Mon Sep 17 00:00:00 2001 From: Filipp Zhinkin Date: Tue, 12 Mar 2024 17:38:15 +0100 Subject: [PATCH 50/53] Reworked target group aliasing --- .../kotlin/api/klib/KlibAbiDumpFileMerger.kt | 73 ++++++++++----- src/test/kotlin/tests/KlibDumpTest.kt | 92 ++++++++++++++++++- 2 files changed, 141 insertions(+), 24 deletions(-) diff --git a/src/main/kotlin/api/klib/KlibAbiDumpFileMerger.kt b/src/main/kotlin/api/klib/KlibAbiDumpFileMerger.kt index 3e6e308c..dccfb423 100644 --- a/src/main/kotlin/api/klib/KlibAbiDumpFileMerger.kt +++ b/src/main/kotlin/api/klib/KlibAbiDumpFileMerger.kt @@ -5,7 +5,6 @@ package kotlinx.validation.api.klib -import kotlinx.validation.api.klib.KlibTarget import java.io.File import java.nio.file.Files import java.util.* @@ -677,31 +676,62 @@ internal class KLibsTargetsFormatter(klibDump: KlibAbiDumpMerger) { // filter out all groups consisting of less than one member aliasesBuilder.removeIf { it.targets.size < 2 } aliasesBuilder.removeIf { it.targets == allTargets } - // collect all actually used target groups and remove all unused aliases - val usedAliases = mutableSetOf>() + filterOutDumplicateGroups(aliasesBuilder) + filterOutUnusedGroups(klibDump, aliasesBuilder) + + // reverse the order to place a common group first + aliases = aliasesBuilder.reversed() + } + + private fun filterOutDumplicateGroups(allGroups: MutableList) { + // Remove all duplicating groups. At this point, aliases are sorted so + // that more specific groups are before more general groups, so we'll remove + // more common groups here. + for (idx in allGroups.size - 1 downTo 1) { + if (allGroups[idx].targets == allGroups[idx - 1].targets) { + // TODO: can we avoid shifting the whole trailing part of the list? + allGroups.removeAt(idx) + } + } + } + + // TODO: optimize the algorithm + private fun filterOutUnusedGroups(klibDump: KlibAbiDumpMerger, allGroups: MutableList) { + // Collect all target sets that are actually in use + val targetSetsInUse = mutableSetOf>() + val allTargets = klibDump.targets fun visitor(decl: DeclarationContainer) { - usedAliases.add(decl.targets) + if (decl.targets != allTargets) { + targetSetsInUse.add(decl.targets) + } decl.visit(::visitor) } klibDump.visit(::visitor) - aliasesBuilder.removeIf { !usedAliases.contains(it.targets) } - // Remove all duplicating groups. At this point, aliases are sorted so - // that more specific groups are before more common groups, so we'll remove - // more common groups here. - val toRemove = mutableListOf() - for (i in aliasesBuilder.indices) { - for (j in i + 1 until aliasesBuilder.size) { - if (aliasesBuilder[j].targets == aliasesBuilder[i].targets) { - toRemove.add(j) + // Scan groups from general to specific and check it there are some + // declarations having a target set such that it includes a group. + // If there are no such declarations - a group has to be removed. + for (idx in allGroups.size - 1 downTo 0) { + val alias = allGroups[idx] + val updatedTargetSets = mutableSetOf>() + // scan actually used target sets + val targetSetIterator = targetSetsInUse.iterator() + while (targetSetIterator.hasNext()) { + val s = targetSetIterator.next() + // If a target set includes this group, take that set, remove all targets + // corresponding to the current group from it and then, later, add the set back. + if (s.containsAll(alias.targets)) { + targetSetIterator.remove() + updatedTargetSets.add(s.subtract(alias.targets)) } } + // If updatedTargetSets is empty, there are no target sets including the current group, + // so we have to remove the group. + if (updatedTargetSets.isEmpty()) { + allGroups.removeAt(idx) + } else { + targetSetsInUse.addAll(updatedTargetSets) + } } - toRemove.sortDescending() - toRemove.forEach { - aliasesBuilder.removeAt(it) - } - // reverse the order to place a common group first - aliases = aliasesBuilder.reversed() } fun formatHeader(targets: Set): String { @@ -725,10 +755,9 @@ internal class KLibsTargetsFormatter(klibDump: KlibAbiDumpMerger) { val mutableTargets = targets.toMutableSet() val resultingTargets = mutableListOf() for (alias in aliases) { - if (mutableTargets == alias.targets) { - mutableTargets.clear() + if (mutableTargets.containsAll(alias.targets)) { + mutableTargets.removeAll(alias.targets) resultingTargets.add(alias.name) - break } } resultingTargets.addAll(mutableTargets.map { it.toString() }) diff --git a/src/test/kotlin/tests/KlibDumpTest.kt b/src/test/kotlin/tests/KlibDumpTest.kt index 8f1fc7af..369fe7d1 100644 --- a/src/test/kotlin/tests/KlibDumpTest.kt +++ b/src/test/kotlin/tests/KlibDumpTest.kt @@ -10,14 +10,12 @@ import kotlinx.validation.api.klib.KlibDump import kotlinx.validation.api.klib.KlibTarget import kotlinx.validation.api.klib.inferAbi import kotlinx.validation.api.klib.mergeFromKlib -import org.jetbrains.kotlin.backend.common.phaser.dumpIrElement import org.junit.Rule import org.junit.Test import org.junit.rules.TemporaryFolder import java.io.File import java.io.FileNotFoundException import java.util.* -import kotlin.math.sin import kotlin.test.assertEquals import kotlin.test.assertFailsWith import kotlin.test.assertTrue @@ -551,4 +549,94 @@ class KlibDumpTest { ) } } + + @Test + fun iterativeGrouping() { + val dump = KlibDump.from(asFile(""" + // Klib ABI Dump + // Targets: [androidNativeArm32, androidNativeArm64, androidNativeX64, androidNativeX86, linuxArm64, linuxX64, mingwX64] + // Rendering settings: + // - Signature version: 2 + // - Show manifest properties: true + // - Show declarations: true + + // Library unique name: + final class org.different.pack/BuildConfig { // org.different.pack/BuildConfig|null[0] + constructor () // org.different.pack/BuildConfig.|(){}[0] + final fun f1(): kotlin/Int // org.different.pack/BuildConfig.f1|f1(){}[0] + final val p1 // org.different.pack/BuildConfig.p1|{}p1[0] + final fun (): kotlin/Int // org.different.pack/BuildConfig.p1.|(){}[0] + } + // Targets: [androidNativeArm64] + final fun (org.different.pack/BuildConfig).org.different.pack/linuxArm64Specific(): kotlin/Int // org.different.pack/linuxArm64Specific|linuxArm64Specific@org.different.pack.BuildConfig(){}[0] + // Targets: [linuxArm64, linuxX64] + final fun (org.different.pack/BuildConfig).org.different.pack/linuxArm64Specific2(): kotlin/Int // org.different.pack/linuxArm64Specific2|linuxArm64Specific@org.different.pack.BuildConfig(){}[0] + // Targets: [androidNativeArm32, androidNativeArm64, androidNativeX64, androidNativeX86, linuxArm64, linuxX64] + final fun (org.different.pack/BuildConfig).org.different.pack/linuxArm64Specific3(): kotlin/Int // org.different.pack/linuxArm64Specific3|linuxArm64Specific@org.different.pack.BuildConfig(){}[0] + + """.trimIndent())) + + val expectedDump = """ + // Klib ABI Dump + // Targets: [androidNativeArm32, androidNativeArm64, androidNativeX64, androidNativeX86, linuxArm64, linuxX64, mingwX64] + // Alias: androidNative => [androidNativeArm32, androidNativeArm64, androidNativeX64, androidNativeX86] + // Alias: linux => [linuxArm64, linuxX64] + // Rendering settings: + // - Signature version: 2 + // - Show manifest properties: true + // - Show declarations: true + + // Library unique name: + final class org.different.pack/BuildConfig { // org.different.pack/BuildConfig|null[0] + constructor () // org.different.pack/BuildConfig.|(){}[0] + final fun f1(): kotlin/Int // org.different.pack/BuildConfig.f1|f1(){}[0] + final val p1 // org.different.pack/BuildConfig.p1|{}p1[0] + final fun (): kotlin/Int // org.different.pack/BuildConfig.p1.|(){}[0] + } + // Targets: [androidNative, linux] + final fun (org.different.pack/BuildConfig).org.different.pack/linuxArm64Specific3(): kotlin/Int // org.different.pack/linuxArm64Specific3|linuxArm64Specific@org.different.pack.BuildConfig(){}[0] + // Targets: [linux] + final fun (org.different.pack/BuildConfig).org.different.pack/linuxArm64Specific2(): kotlin/Int // org.different.pack/linuxArm64Specific2|linuxArm64Specific@org.different.pack.BuildConfig(){}[0] + // Targets: [androidNativeArm64] + final fun (org.different.pack/BuildConfig).org.different.pack/linuxArm64Specific(): kotlin/Int // org.different.pack/linuxArm64Specific|linuxArm64Specific@org.different.pack.BuildConfig(){}[0] + + """.trimIndent() + assertEquals(expectedDump, buildString { dump.saveTo(this) }) + } + + @Test + fun similarGroupRemoval() { + // native function should use a group alias "ios", not "apple", or "native" + val dump = KlibDump.from(asFile(""" + // Klib ABI Dump + // Targets: [iosArm64, iosX64, js] + // Rendering settings: + // - Signature version: 2 + // - Show manifest properties: true + // - Show declarations: true + + // Library unique name: + final fun org.example/common(): kotlin/Int // com.example/common|common(){}[0] + // Targets: [iosArm64, iosX64] + final fun org.example/native(): kotlin/Int // com.example/native|native(){}[0] + + """.trimIndent())) + + val expectedDump = """ + // Klib ABI Dump + // Targets: [iosArm64, iosX64, js] + // Alias: ios => [iosArm64, iosX64] + // Rendering settings: + // - Signature version: 2 + // - Show manifest properties: true + // - Show declarations: true + + // Library unique name: + final fun org.example/common(): kotlin/Int // com.example/common|common(){}[0] + // Targets: [ios] + final fun org.example/native(): kotlin/Int // com.example/native|native(){}[0] + + """.trimIndent() + assertEquals(expectedDump, buildString { dump.saveTo(this) }) + } } From 081fdb0f94b6bda8c28ccdb0faf4a33788d2d4c4 Mon Sep 17 00:00:00 2001 From: Filipp Zhinkin Date: Tue, 12 Mar 2024 17:39:29 +0100 Subject: [PATCH 51/53] Decapitalized L in KLib --- src/main/kotlin/KotlinKlibAbiBuildTask.kt | 5 ++- ...otlinKlibExtractSupportedTargetsAbiTask.kt | 3 +- ...linKlibInferAbiForUnsupportedTargetTask.kt | 13 ++++---- src/main/kotlin/KotlinKlibMergeAbiTask.kt | 6 ++-- .../kotlin/api/klib/KlibAbiDumpFileMerger.kt | 8 ++--- src/main/kotlin/api/klib/KlibDump.kt | 17 +++++++--- src/main/kotlin/api/klib/KlibDumpFilters.kt | 21 ++++++------ src/test/kotlin/tests/KlibDumpTest.kt | 33 ++++++++++++++----- 8 files changed, 64 insertions(+), 42 deletions(-) diff --git a/src/main/kotlin/KotlinKlibAbiBuildTask.kt b/src/main/kotlin/KotlinKlibAbiBuildTask.kt index 2b0fc09f..c591c632 100644 --- a/src/main/kotlin/KotlinKlibAbiBuildTask.kt +++ b/src/main/kotlin/KotlinKlibAbiBuildTask.kt @@ -8,6 +8,7 @@ package kotlinx.validation import kotlinx.validation.api.klib.KLibDumpFilters import kotlinx.validation.api.klib.KlibDump import kotlinx.validation.api.klib.KlibSignatureVersion +import kotlinx.validation.api.klib.saveTo import org.gradle.api.file.FileCollection import org.gradle.api.tasks.Input import org.gradle.api.tasks.InputFiles @@ -64,8 +65,6 @@ internal abstract class KotlinKlibAbiBuildTask : BuildTaskBase() { signatureVersion = this@KotlinKlibAbiBuildTask.signatureVersion.toKlibSignatureVersion() }) - outputApiFile.bufferedWriter().use { - dump.saveTo(it) - } + dump.saveTo(outputApiFile) } } diff --git a/src/main/kotlin/KotlinKlibExtractSupportedTargetsAbiTask.kt b/src/main/kotlin/KotlinKlibExtractSupportedTargetsAbiTask.kt index bd5f3ee2..58298a18 100644 --- a/src/main/kotlin/KotlinKlibExtractSupportedTargetsAbiTask.kt +++ b/src/main/kotlin/KotlinKlibExtractSupportedTargetsAbiTask.kt @@ -8,6 +8,7 @@ package kotlinx.validation import kotlinx.validation.api.klib.KlibDump import kotlinx.validation.api.klib.KlibAbiDumpMerger import kotlinx.validation.api.klib.KlibTarget +import kotlinx.validation.api.klib.saveTo import org.gradle.api.DefaultTask import org.gradle.api.provider.Provider import org.gradle.api.tasks.* @@ -62,6 +63,6 @@ internal abstract class KotlinKlibExtractSupportedTargetsAbiTask : DefaultTask() ) } dump.remove(targetsToRemove) - outputAbiFile.bufferedWriter().use { dump.saveTo(it) } + dump.saveTo(outputAbiFile) } } diff --git a/src/main/kotlin/KotlinKlibInferAbiForUnsupportedTargetTask.kt b/src/main/kotlin/KotlinKlibInferAbiForUnsupportedTargetTask.kt index 15cf2382..af0228f0 100644 --- a/src/main/kotlin/KotlinKlibInferAbiForUnsupportedTargetTask.kt +++ b/src/main/kotlin/KotlinKlibInferAbiForUnsupportedTargetTask.kt @@ -5,9 +5,7 @@ package kotlinx.validation -import kotlinx.validation.api.klib.KlibDump -import kotlinx.validation.api.klib.KlibTarget -import kotlinx.validation.api.klib.inferAbi +import kotlinx.validation.api.klib.* import kotlinx.validation.api.klib.TargetHierarchy import org.gradle.api.DefaultTask import org.gradle.api.provider.Provider @@ -101,9 +99,7 @@ internal abstract class KotlinKlibInferAbiForUnsupportedTargetTask : DefaultTask } } - outputFile.bufferedWriter().use { - inferAbi(unsupportedTarget, supportedTargetDumps, image).saveTo(it) - } + inferAbi(unsupportedTarget, supportedTargetDumps, image).saveTo(outputFile) logger.warn( "An ABI dump for target $unsupportedTarget was inferred from the ABI generated for the following targets " + @@ -114,7 +110,10 @@ internal abstract class KotlinKlibInferAbiForUnsupportedTargetTask : DefaultTask ) } - private fun findMatchingTargets(supportedTargets: Set, unsupportedTarget: KlibTarget): Collection { + private fun findMatchingTargets( + supportedTargets: Set, + unsupportedTarget: KlibTarget + ): Collection { var currentGroup: String? = unsupportedTarget.targetName while (currentGroup != null) { // If a current group has some supported targets, use them. diff --git a/src/main/kotlin/KotlinKlibMergeAbiTask.kt b/src/main/kotlin/KotlinKlibMergeAbiTask.kt index 2840c206..ebbf04e2 100644 --- a/src/main/kotlin/KotlinKlibMergeAbiTask.kt +++ b/src/main/kotlin/KotlinKlibMergeAbiTask.kt @@ -6,6 +6,7 @@ package kotlinx.validation import kotlinx.validation.api.klib.KlibDump +import kotlinx.validation.api.klib.saveTo import org.gradle.api.DefaultTask import org.gradle.api.tasks.* import java.io.File @@ -51,11 +52,10 @@ internal abstract class KotlinKlibMergeAbiTask : DefaultTask() { @OptIn(ExperimentalBCVApi::class) @TaskAction internal fun merge() { - val mergedDump = KlibDump().apply { + KlibDump().apply { targetToFile.forEach { (targetName, dumpDir) -> merge(dumpDir.resolve(dumpFileName), targetName) } - } - mergedFile.bufferedWriter().use { mergedDump.saveTo(it) } + }.saveTo(mergedFile) } } diff --git a/src/main/kotlin/api/klib/KlibAbiDumpFileMerger.kt b/src/main/kotlin/api/klib/KlibAbiDumpFileMerger.kt index dccfb423..d748f6e9 100644 --- a/src/main/kotlin/api/klib/KlibAbiDumpFileMerger.kt +++ b/src/main/kotlin/api/klib/KlibAbiDumpFileMerger.kt @@ -374,7 +374,7 @@ internal class KlibAbiDumpMerger { } } - private fun createFormatter(): KLibsTargetsFormatter { + private fun createFormatter(): KlibsTargetsFormatter { for (target in targets) { val node = TargetHierarchy.hierarchyIndex[target.targetName] if (node != null && node.allLeafs.size == 1 && node.allLeafs.first() != node.node.name) { @@ -384,7 +384,7 @@ internal class KlibAbiDumpMerger { ) } } - return KLibsTargetsFormatter(this) + return KlibsTargetsFormatter(this) } /** @@ -502,7 +502,7 @@ internal class DeclarationContainer(val text: String, val parent: DeclarationCon return child } - fun dump(appendable: Appendable, allTargets: Set, formatter: KLibsTargetsFormatter) { + fun dump(appendable: Appendable, allTargets: Set, formatter: KlibsTargetsFormatter) { if (targets != allTargets/* && !dumpFormat.singleTargetDump*/) { // Use the same indentation for target list as for the declaration itself appendable.append(" ".repeat(text.depth() * INDENT_WIDTH)) @@ -649,7 +649,7 @@ private object DeclarationsComparator : Comparator { } } -internal class KLibsTargetsFormatter(klibDump: KlibAbiDumpMerger) { +internal class KlibsTargetsFormatter(klibDump: KlibAbiDumpMerger) { private data class Alias(val name: String, val targets: Set) private val aliases: List diff --git a/src/main/kotlin/api/klib/KlibDump.kt b/src/main/kotlin/api/klib/KlibDump.kt index 853aa045..a64dbab4 100644 --- a/src/main/kotlin/api/klib/KlibDump.kt +++ b/src/main/kotlin/api/klib/KlibDump.kt @@ -6,6 +6,7 @@ package kotlinx.validation.api.klib import kotlinx.validation.ExperimentalBCVApi +import org.jetbrains.kotlin.ir.backend.js.MainModule import java.io.File import java.io.FileNotFoundException @@ -17,7 +18,7 @@ import java.io.FileNotFoundException * **Creating a textual dump from a klib** * ```kotlin * val dump = KlibDump.fromKlib(File("/path/to/library.klib")) - * File("/path/to/dump.klib.api").bufferedWriter().use { dump.saveTo(it) }) + * dump.saveTo(File("/path/to/dump.klib.api")) * ``` * * **Loading a dump** @@ -30,7 +31,7 @@ import java.io.FileNotFoundException * val klibs = listOf(File("/path/to/library-linuxX64.klib"), File("/path/to/library-linuxArm64.klib"), ...) * val mergedDump = KlibDump() * klibs.forEach { mergedDump.mergeFromKlib(it) } - * File("/path/to/merged.klib.api").bufferedWriter().use { mergedDump.saveTo(it) } + * mergedDump.saveTo(File("/path/to/merged.klib.api")) * ``` * * **Updating an existing merged dump** @@ -39,7 +40,7 @@ import java.io.FileNotFoundException * val newTargetDump = KlibDump.fromKlib(File("/path/to/library-linuxX64.klib")) * mergedDump.remove(newTargetDump.targets) * mergedDump.merge(newTargetDump) - * File("/path/to/merged.klib.api").bufferedWrite().use { mergedDump.saveTo(it) } + * mergedDump.saveTo(File("/path/to/merged.klib.api")) * ``` */ @ExperimentalBCVApi @@ -187,7 +188,7 @@ public class KlibDump { public fun fromKlib( klibFile: File, configurableTargetName: String? = null, - filters: KLibDumpFilters = KLibDumpFilters.DEFAULT + filters: KlibDumpFilters = KlibDumpFilters.DEFAULT ): KlibDump { val dump = buildString { dumpTo(this, klibFile, filters) @@ -270,7 +271,13 @@ public fun inferAbi( @ExperimentalBCVApi public fun KlibDump.mergeFromKlib( klibFile: File, configurableTargetName: String? = null, - filters: KLibDumpFilters = KLibDumpFilters.DEFAULT + filters: KlibDumpFilters = KlibDumpFilters.DEFAULT ) { this.merge(KlibDump.fromKlib(klibFile, configurableTargetName, filters)) } + +/** + * Serializes the dump and writes it to [file]. + */ +@ExperimentalBCVApi +public fun KlibDump.saveTo(file: File): Unit = file.bufferedWriter().use { saveTo(it) } diff --git a/src/main/kotlin/api/klib/KlibDumpFilters.kt b/src/main/kotlin/api/klib/KlibDumpFilters.kt index 0a950cda..ce90824b 100644 --- a/src/main/kotlin/api/klib/KlibDumpFilters.kt +++ b/src/main/kotlin/api/klib/KlibDumpFilters.kt @@ -6,7 +6,6 @@ package kotlinx.validation.api.klib import kotlinx.validation.ExperimentalBCVApi -import org.jetbrains.kotlin.gradle.utils.`is` import org.jetbrains.kotlin.library.abi.* import java.io.File import java.io.FileNotFoundException @@ -15,7 +14,7 @@ import java.io.FileNotFoundException * Filters affecting how the klib ABI will be represented in a dump. */ @ExperimentalBCVApi -public class KLibDumpFilters internal constructor( +public class KlibDumpFilters internal constructor( /** * Names of packages that should be excluded from a dump. * If a package is listed here, none of its declarations will be included in a dump. @@ -69,8 +68,8 @@ public class KLibDumpFilters internal constructor( public var signatureVersion: KlibSignatureVersion = KlibSignatureVersion.LATEST @PublishedApi - internal fun build(): KLibDumpFilters { - return KLibDumpFilters(ignoredPackages, ignoredClasses, nonPublicMarkers, signatureVersion) + internal fun build(): KlibDumpFilters { + return KlibDumpFilters(ignoredPackages, ignoredClasses, nonPublicMarkers, signatureVersion) } } @@ -79,26 +78,26 @@ public class KLibDumpFilters internal constructor( * Default KLib ABI dump filters which declares no filters * and uses the latest KLib ABI signature version available. */ - public val DEFAULT: KLibDumpFilters = KLibDumpFilters {} + public val DEFAULT: KlibDumpFilters = KLibDumpFilters {} } } /** - * Builds a new [KLibDumpFilters] instance by invoking a [builderAction] on a temporary - * [KLibDumpFilters.Builder] instance and then converting it into filters. + * Builds a new [KlibDumpFilters] instance by invoking a [builderAction] on a temporary + * [KlibDumpFilters.Builder] instance and then converting it into filters. * - * Supplied [KLibDumpFilters.Builder] is valid only during the scope of [builderAction] execution. + * Supplied [KlibDumpFilters.Builder] is valid only during the scope of [builderAction] execution. */ @ExperimentalBCVApi -public fun KLibDumpFilters(builderAction: KLibDumpFilters.Builder.() -> Unit): KLibDumpFilters { - val builder = KLibDumpFilters.Builder() +public fun KLibDumpFilters(builderAction: KlibDumpFilters.Builder.() -> Unit): KlibDumpFilters { + val builder = KlibDumpFilters.Builder() builderAction(builder) return builder.build() } @ExperimentalBCVApi @OptIn(ExperimentalLibraryAbiReader::class) -internal fun dumpTo(to: Appendable, klibFile: File, filters: KLibDumpFilters) { +internal fun dumpTo(to: Appendable, klibFile: File, filters: KlibDumpFilters) { if(!klibFile.exists()) { throw FileNotFoundException("File does not exist: ${klibFile.absolutePath}") } val abiFilters = mutableListOf() filters.ignoredClasses.toKlibNames().also { diff --git a/src/test/kotlin/tests/KlibDumpTest.kt b/src/test/kotlin/tests/KlibDumpTest.kt index 369fe7d1..7915e4df 100644 --- a/src/test/kotlin/tests/KlibDumpTest.kt +++ b/src/test/kotlin/tests/KlibDumpTest.kt @@ -6,10 +6,7 @@ package tests import kotlinx.validation.ExperimentalBCVApi -import kotlinx.validation.api.klib.KlibDump -import kotlinx.validation.api.klib.KlibTarget -import kotlinx.validation.api.klib.inferAbi -import kotlinx.validation.api.klib.mergeFromKlib +import kotlinx.validation.api.klib.* import org.junit.Rule import org.junit.Test import org.junit.rules.TemporaryFolder @@ -552,7 +549,9 @@ class KlibDumpTest { @Test fun iterativeGrouping() { - val dump = KlibDump.from(asFile(""" + val dump = KlibDump.from( + asFile( + """ // Klib ABI Dump // Targets: [androidNativeArm32, androidNativeArm64, androidNativeX64, androidNativeX86, linuxArm64, linuxX64, mingwX64] // Rendering settings: @@ -574,7 +573,9 @@ class KlibDumpTest { // Targets: [androidNativeArm32, androidNativeArm64, androidNativeX64, androidNativeX86, linuxArm64, linuxX64] final fun (org.different.pack/BuildConfig).org.different.pack/linuxArm64Specific3(): kotlin/Int // org.different.pack/linuxArm64Specific3|linuxArm64Specific@org.different.pack.BuildConfig(){}[0] - """.trimIndent())) + """.trimIndent() + ) + ) val expectedDump = """ // Klib ABI Dump @@ -607,7 +608,9 @@ class KlibDumpTest { @Test fun similarGroupRemoval() { // native function should use a group alias "ios", not "apple", or "native" - val dump = KlibDump.from(asFile(""" + val dump = KlibDump.from( + asFile( + """ // Klib ABI Dump // Targets: [iosArm64, iosX64, js] // Rendering settings: @@ -620,7 +623,9 @@ class KlibDumpTest { // Targets: [iosArm64, iosX64] final fun org.example/native(): kotlin/Int // com.example/native|native(){}[0] - """.trimIndent())) + """.trimIndent() + ) + ) val expectedDump = """ // Klib ABI Dump @@ -639,4 +644,16 @@ class KlibDumpTest { """.trimIndent() assertEquals(expectedDump, buildString { dump.saveTo(this) }) } + + @Test + fun saveToFile() { + val dump = KlibDump.from(asFile(mergedMultitargetDump)) + val tempFile = tmpFolder.newFile() + dump.saveTo(tempFile) + + assertEquals( + buildString { dump.saveTo(this) }, + tempFile.readText(Charsets.US_ASCII) + ) + } } From 4f63bae6f03f9f4225823b24a6340b2758e4ffa0 Mon Sep 17 00:00:00 2001 From: Filipp Zhinkin Date: Tue, 12 Mar 2024 17:47:55 +0100 Subject: [PATCH 52/53] Update api dump --- api/binary-compatibility-validator.api | 55 +++++++++++++------------- 1 file changed, 28 insertions(+), 27 deletions(-) diff --git a/api/binary-compatibility-validator.api b/api/binary-compatibility-validator.api index ef7be90f..07d3bc3d 100644 --- a/api/binary-compatibility-validator.api +++ b/api/binary-compatibility-validator.api @@ -108,28 +108,6 @@ public final class kotlinx/validation/api/KotlinSignaturesLoadingKt { public static synthetic fun retainExplicitlyIncludedIfDeclared$default (Ljava/util/List;Ljava/util/Collection;Ljava/util/Collection;Ljava/util/Collection;ILjava/lang/Object;)Ljava/util/List; } -public final class kotlinx/validation/api/klib/KLibDumpFilters { - public static final field Companion Lkotlinx/validation/api/klib/KLibDumpFilters$Companion; - public final fun getIgnoredClasses ()Ljava/util/Set; - public final fun getIgnoredPackages ()Ljava/util/Set; - public final fun getNonPublicMarkers ()Ljava/util/Set; - public final fun getSignatureVersion ()Lkotlinx/validation/api/klib/KlibSignatureVersion; -} - -public final class kotlinx/validation/api/klib/KLibDumpFilters$Builder { - public fun ()V - public final fun build ()Lkotlinx/validation/api/klib/KLibDumpFilters; - public final fun getIgnoredClasses ()Ljava/util/Set; - public final fun getIgnoredPackages ()Ljava/util/Set; - public final fun getNonPublicMarkers ()Ljava/util/Set; - public final fun getSignatureVersion ()Lkotlinx/validation/api/klib/KlibSignatureVersion; - public final fun setSignatureVersion (Lkotlinx/validation/api/klib/KlibSignatureVersion;)V -} - -public final class kotlinx/validation/api/klib/KLibDumpFilters$Companion { - public final fun getDEFAULT ()Lkotlinx/validation/api/klib/KLibDumpFilters; -} - public final class kotlinx/validation/api/klib/KlibDump { public static final field Companion Lkotlinx/validation/api/klib/KlibDump$Companion; public fun ()V @@ -146,19 +124,42 @@ public final class kotlinx/validation/api/klib/KlibDump { public final class kotlinx/validation/api/klib/KlibDump$Companion { public final fun from (Ljava/io/File;Ljava/lang/String;)Lkotlinx/validation/api/klib/KlibDump; public static synthetic fun from$default (Lkotlinx/validation/api/klib/KlibDump$Companion;Ljava/io/File;Ljava/lang/String;ILjava/lang/Object;)Lkotlinx/validation/api/klib/KlibDump; - public final fun fromKlib (Ljava/io/File;Ljava/lang/String;Lkotlinx/validation/api/klib/KLibDumpFilters;)Lkotlinx/validation/api/klib/KlibDump; - public static synthetic fun fromKlib$default (Lkotlinx/validation/api/klib/KlibDump$Companion;Ljava/io/File;Ljava/lang/String;Lkotlinx/validation/api/klib/KLibDumpFilters;ILjava/lang/Object;)Lkotlinx/validation/api/klib/KlibDump; + public final fun fromKlib (Ljava/io/File;Ljava/lang/String;Lkotlinx/validation/api/klib/KlibDumpFilters;)Lkotlinx/validation/api/klib/KlibDump; + public static synthetic fun fromKlib$default (Lkotlinx/validation/api/klib/KlibDump$Companion;Ljava/io/File;Ljava/lang/String;Lkotlinx/validation/api/klib/KlibDumpFilters;ILjava/lang/Object;)Lkotlinx/validation/api/klib/KlibDump; +} + +public final class kotlinx/validation/api/klib/KlibDumpFilters { + public static final field Companion Lkotlinx/validation/api/klib/KlibDumpFilters$Companion; + public final fun getIgnoredClasses ()Ljava/util/Set; + public final fun getIgnoredPackages ()Ljava/util/Set; + public final fun getNonPublicMarkers ()Ljava/util/Set; + public final fun getSignatureVersion ()Lkotlinx/validation/api/klib/KlibSignatureVersion; +} + +public final class kotlinx/validation/api/klib/KlibDumpFilters$Builder { + public fun ()V + public final fun build ()Lkotlinx/validation/api/klib/KlibDumpFilters; + public final fun getIgnoredClasses ()Ljava/util/Set; + public final fun getIgnoredPackages ()Ljava/util/Set; + public final fun getNonPublicMarkers ()Ljava/util/Set; + public final fun getSignatureVersion ()Lkotlinx/validation/api/klib/KlibSignatureVersion; + public final fun setSignatureVersion (Lkotlinx/validation/api/klib/KlibSignatureVersion;)V +} + +public final class kotlinx/validation/api/klib/KlibDumpFilters$Companion { + public final fun getDEFAULT ()Lkotlinx/validation/api/klib/KlibDumpFilters; } public final class kotlinx/validation/api/klib/KlibDumpFiltersKt { - public static final fun KLibDumpFilters (Lkotlin/jvm/functions/Function1;)Lkotlinx/validation/api/klib/KLibDumpFilters; + public static final fun KLibDumpFilters (Lkotlin/jvm/functions/Function1;)Lkotlinx/validation/api/klib/KlibDumpFilters; } public final class kotlinx/validation/api/klib/KlibDumpKt { public static final fun inferAbi (Lkotlinx/validation/api/klib/KlibTarget;Ljava/lang/Iterable;Lkotlinx/validation/api/klib/KlibDump;)Lkotlinx/validation/api/klib/KlibDump; public static synthetic fun inferAbi$default (Lkotlinx/validation/api/klib/KlibTarget;Ljava/lang/Iterable;Lkotlinx/validation/api/klib/KlibDump;ILjava/lang/Object;)Lkotlinx/validation/api/klib/KlibDump; - public static final fun mergeFromKlib (Lkotlinx/validation/api/klib/KlibDump;Ljava/io/File;Ljava/lang/String;Lkotlinx/validation/api/klib/KLibDumpFilters;)V - public static synthetic fun mergeFromKlib$default (Lkotlinx/validation/api/klib/KlibDump;Ljava/io/File;Ljava/lang/String;Lkotlinx/validation/api/klib/KLibDumpFilters;ILjava/lang/Object;)V + public static final fun mergeFromKlib (Lkotlinx/validation/api/klib/KlibDump;Ljava/io/File;Ljava/lang/String;Lkotlinx/validation/api/klib/KlibDumpFilters;)V + public static synthetic fun mergeFromKlib$default (Lkotlinx/validation/api/klib/KlibDump;Ljava/io/File;Ljava/lang/String;Lkotlinx/validation/api/klib/KlibDumpFilters;ILjava/lang/Object;)V + public static final fun saveTo (Lkotlinx/validation/api/klib/KlibDump;Ljava/io/File;)V } public final class kotlinx/validation/api/klib/KlibSignatureVersion { From 259ad7c13ead5f39bf1a79e5735a572c7e0ee76e Mon Sep 17 00:00:00 2001 From: Filipp Zhinkin Date: Mon, 18 Mar 2024 09:58:37 +0100 Subject: [PATCH 53/53] Improved kdoc, cleaned up the code --- src/main/kotlin/api/klib/KlibAbiDumpFileMerger.kt | 4 +--- src/main/kotlin/api/klib/KlibDump.kt | 4 ++-- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/main/kotlin/api/klib/KlibAbiDumpFileMerger.kt b/src/main/kotlin/api/klib/KlibAbiDumpFileMerger.kt index d748f6e9..a5eddc7e 100644 --- a/src/main/kotlin/api/klib/KlibAbiDumpFileMerger.kt +++ b/src/main/kotlin/api/klib/KlibAbiDumpFileMerger.kt @@ -392,11 +392,9 @@ internal class KlibAbiDumpMerger { * If some declaration was declared only for [target], it will be removed from the dump. */ fun remove(target: KlibTarget) { - if (!_targets.contains(target)) { + if (!_targets.remove(target)) { return } - - _targets.remove(target) topLevelDeclaration.remove(target) } diff --git a/src/main/kotlin/api/klib/KlibDump.kt b/src/main/kotlin/api/klib/KlibDump.kt index a64dbab4..2b8f35c4 100644 --- a/src/main/kotlin/api/klib/KlibDump.kt +++ b/src/main/kotlin/api/klib/KlibDump.kt @@ -45,7 +45,7 @@ import java.io.FileNotFoundException */ @ExperimentalBCVApi public class KlibDump { - internal var merger: KlibAbiDumpMerger = KlibAbiDumpMerger() + internal val merger: KlibAbiDumpMerger = KlibAbiDumpMerger() /** * Set of all targets for which this dump contains declarations. @@ -116,7 +116,7 @@ public class KlibDump { /** * Remove all declarations that do belong to specified targets and remove these targets from the dump. * - * All targets not stored within a dump will be ignored. + * All targets in the [targets] collection not contained within this dump will be ignored. * * @sample samples.KlibDumpSamples.mergeDumpObjects */