From c6956776bd74d772f8dacb3c47260d70136585e8 Mon Sep 17 00:00:00 2001 From: Filipp Zhinkin Date: Tue, 26 Mar 2024 13:27:37 +0100 Subject: [PATCH 1/5] Correctly handle dumps starting with Targets-list (#202) Fixes #201 --- .../kotlin/api/klib/KlibAbiDumpFileMerger.kt | 2 +- src/test/kotlin/tests/KlibAbiMergingTest.kt | 24 +++++++++++++++++++ .../non-overlapping/linux-arm64.klib.abi | 9 +++++++ .../merge/non-overlapping/linux-x64.klib.abi | 9 +++++++ .../merge/non-overlapping/merged.klib.abi | 12 ++++++++++ 5 files changed, 55 insertions(+), 1 deletion(-) create mode 100644 src/test/resources/merge/non-overlapping/linux-arm64.klib.abi create mode 100644 src/test/resources/merge/non-overlapping/linux-x64.klib.abi create mode 100644 src/test/resources/merge/non-overlapping/merged.klib.abi diff --git a/src/main/kotlin/api/klib/KlibAbiDumpFileMerger.kt b/src/main/kotlin/api/klib/KlibAbiDumpFileMerger.kt index a5eddc7..529c670 100644 --- a/src/main/kotlin/api/klib/KlibAbiDumpFileMerger.kt +++ b/src/main/kotlin/api/klib/KlibAbiDumpFileMerger.kt @@ -254,7 +254,7 @@ internal class KlibAbiDumpMerger { // then try to parse a manifest while (hasNext()) { val next = peek()!! - if (!next.startsWith(COMMENT_PREFIX)) break + if (!next.startsWith(COMMENT_PREFIX) || next.startsWith(TARGETS_LIST_PREFIX)) break next() // There's no manifest in merged files check(!isMergedFile) { "Unexpected header line: $next" } diff --git a/src/test/kotlin/tests/KlibAbiMergingTest.kt b/src/test/kotlin/tests/KlibAbiMergingTest.kt index b4fdf42..0c55753 100644 --- a/src/test/kotlin/tests/KlibAbiMergingTest.kt +++ b/src/test/kotlin/tests/KlibAbiMergingTest.kt @@ -336,4 +336,28 @@ class KlibAbiMergingTest { KlibAbiDumpMerger().merge(file("/merge/stdlib_native_common.abi"), "target") } } + + @Test + fun mergeDumpsWithNonOverlappingDeclarations() { + val dump = dumpToFile(KlibAbiDumpMerger().apply { + merge(file("/merge/non-overlapping/linux-arm64.klib.abi")) + merge(file("/merge/non-overlapping/linux-x64.klib.abi")) + }) + + assertContentEquals( + lines("/merge/non-overlapping/merged.klib.abi"), + Files.readAllLines(dump.toPath()).asSequence() + ) + } + + @Test + fun loadMergedDumpWithNonOverlappingDeclarations() { + val dump = dumpToFile(KlibAbiDumpMerger().apply { + merge(file("/merge/non-overlapping/merged.klib.abi")) + }) + assertContentEquals( + lines("/merge/non-overlapping/merged.klib.abi"), + Files.readAllLines(dump.toPath()).asSequence() + ) + } } diff --git a/src/test/resources/merge/non-overlapping/linux-arm64.klib.abi b/src/test/resources/merge/non-overlapping/linux-arm64.klib.abi new file mode 100644 index 0000000..f4dfa09 --- /dev/null +++ b/src/test/resources/merge/non-overlapping/linux-arm64.klib.abi @@ -0,0 +1,9 @@ +// Klib ABI Dump +// Targets: [linuxArm64] +// Rendering settings: +// - Signature version: 2 +// - Show manifest properties: false +// - Show declarations: true + +// Library unique name: +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/non-overlapping/linux-x64.klib.abi b/src/test/resources/merge/non-overlapping/linux-x64.klib.abi new file mode 100644 index 0000000..bb7ad17 --- /dev/null +++ b/src/test/resources/merge/non-overlapping/linux-x64.klib.abi @@ -0,0 +1,9 @@ +// Klib ABI Dump +// Targets: [linuxX64] +// Rendering settings: +// - Signature version: 2 +// - Show manifest properties: false +// - Show declarations: true + +// Library unique name: +final fun org.example/sub(kotlin/Long, kotlin/Long): kotlin/Long // org.example/sub|sub(kotlin.Long;kotlin.Long){}[0] diff --git a/src/test/resources/merge/non-overlapping/merged.klib.abi b/src/test/resources/merge/non-overlapping/merged.klib.abi new file mode 100644 index 0000000..a1f33e2 --- /dev/null +++ b/src/test/resources/merge/non-overlapping/merged.klib.abi @@ -0,0 +1,12 @@ +// Klib ABI Dump +// Targets: [linuxArm64, linuxX64] +// Rendering settings: +// - Signature version: 2 +// - Show manifest properties: false +// - Show declarations: true + +// Library unique name: +// Targets: [linuxArm64] +final fun org.example/add(kotlin/Long, kotlin/Long): kotlin/Long // org.example/add|add(kotlin.Long;kotlin.Long){}[0] +// Targets: [linuxX64] +final fun org.example/sub(kotlin/Long, kotlin/Long): kotlin/Long // org.example/sub|sub(kotlin.Long;kotlin.Long){}[0] From e478dd62c519328210cbf516edaf8436021d4492 Mon Sep 17 00:00:00 2001 From: Filipp Zhinkin Date: Wed, 27 Mar 2024 15:36:40 +0100 Subject: [PATCH 2/5] Correctly handle compilations without sources (#200) * Correctly handle targets with a main compilation resulting in no compiled artifacts * Postpone source directories check by wrapping it into a provider Fixes #199 --- .../kotlin/kotlinx/validation/api/Assert.kt | 7 ++ .../validation/test/KlibVerificationTests.kt | 36 ++++++++ .../AnotherBuildConfig.klib.linuxX64Only.dump | 14 +++ .../BinaryCompatibilityValidatorPlugin.kt | 88 ++++++++++++------- src/main/kotlin/KotlinKlibMergeAbiTask.kt | 5 +- 5 files changed, 116 insertions(+), 34 deletions(-) create mode 100644 src/functionalTest/resources/examples/classes/AnotherBuildConfig.klib.linuxX64Only.dump diff --git a/src/functionalTest/kotlin/kotlinx/validation/api/Assert.kt b/src/functionalTest/kotlin/kotlinx/validation/api/Assert.kt index 6482667..8a3950b 100644 --- a/src/functionalTest/kotlin/kotlinx/validation/api/Assert.kt +++ b/src/functionalTest/kotlin/kotlinx/validation/api/Assert.kt @@ -24,6 +24,13 @@ internal fun BuildResult.assertTaskFailure(task: String) { assertTaskOutcome(TaskOutcome.FAILED, task) } +/** + * Helper `fun` for asserting a [TaskOutcome] to be equal to [TaskOutcome.SKIPPED] + */ +internal fun BuildResult.assertTaskSkipped(task: String) { + assertTaskOutcome(TaskOutcome.SKIPPED, task) +} + private fun BuildResult.assertTaskOutcome(taskOutcome: TaskOutcome, taskName: String) { assertEquals(taskOutcome, task(taskName)?.outcome) } diff --git a/src/functionalTest/kotlin/kotlinx/validation/test/KlibVerificationTests.kt b/src/functionalTest/kotlin/kotlinx/validation/test/KlibVerificationTests.kt index 63198ed..5307e86 100644 --- a/src/functionalTest/kotlin/kotlinx/validation/test/KlibVerificationTests.kt +++ b/src/functionalTest/kotlin/kotlinx/validation/test/KlibVerificationTests.kt @@ -18,6 +18,7 @@ import org.junit.Test import java.io.File import java.nio.file.Files import java.nio.file.Paths +import kotlin.test.assertFalse import kotlin.test.assertTrue internal const val BANNED_TARGETS_PROPERTY_NAME = "binary.compatibility.validator.klib.targets.disabled.for.testing" @@ -633,4 +634,39 @@ internal class KlibVerificationTests : BaseKotlinGradleTest() { ) } } + + @Test + fun `apiDump should not fail for empty project`() { + val runner = test { + baseProjectSetting() + addToSrcSet("/examples/classes/AnotherBuildConfig.kt", sourceSet = "commonTest") + runApiDump() + } + + runner.build().apply { + assertTaskSkipped(":klibApiDump") + } + assertFalse(runner.projectDir.resolve("api").exists()) + } + + @Test + fun `apiDump should not fail if there is only one target`() { + val runner = test { + baseProjectSetting() + addToSrcSet("/examples/classes/AnotherBuildConfig.kt", sourceSet = "commonTest") + addToSrcSet("/examples/classes/AnotherBuildConfig.kt", sourceSet = "linuxX64Main") + runApiDump() + } + checkKlibDump(runner.build(), "/examples/classes/AnotherBuildConfig.klib.linuxX64Only.dump") + } + + @Test + fun `apiCheck should not fail for empty project`() { + val runner = test { + baseProjectSetting() + addToSrcSet("/examples/classes/AnotherBuildConfig.kt", sourceSet = "commonTest") + runApiCheck() + } + runner.build() + } } diff --git a/src/functionalTest/resources/examples/classes/AnotherBuildConfig.klib.linuxX64Only.dump b/src/functionalTest/resources/examples/classes/AnotherBuildConfig.klib.linuxX64Only.dump new file mode 100644 index 0000000..7c6117d --- /dev/null +++ b/src/functionalTest/resources/examples/classes/AnotherBuildConfig.klib.linuxX64Only.dump @@ -0,0 +1,14 @@ +// Klib ABI Dump +// Targets: [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] +} diff --git a/src/main/kotlin/BinaryCompatibilityValidatorPlugin.kt b/src/main/kotlin/BinaryCompatibilityValidatorPlugin.kt index 1548c4d..8fda84a 100644 --- a/src/main/kotlin/BinaryCompatibilityValidatorPlugin.kt +++ b/src/main/kotlin/BinaryCompatibilityValidatorPlugin.kt @@ -105,7 +105,7 @@ public class BinaryCompatibilityValidatorPlugin : Plugin { kotlin.targets.matching { it.jvmBased }.all { target -> val targetConfig = TargetConfig(project, extension, target.name, jvmDirConfig) if (target.platformType == KotlinPlatformType.jvm) { - target.mainCompilations.all { + target.mainCompilationOrNull?.also { project.configureKotlinCompilation(it, extension, targetConfig, commonApiDump, commonApiCheck) } } else if (target.platformType == KotlinPlatformType.androidJvm) { @@ -219,11 +219,9 @@ private fun Project.configureKotlinCompilation( 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() } } + isEnabled = apiCheckEnabled(projectName, extension) + val hasSourcesPredicate = compilation.hasAnySourcesPredicate() + onlyIf { hasSourcesPredicate.get() } // '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" @@ -419,6 +417,8 @@ private class KlibValidationPipelineBuilder( project.name projectApiFile = klibApiDir.get().resolve(klibDumpFileName) generatedApiFile = klibMergeDir.resolve(klibDumpFileName) + val hasCompilableTargets = project.hasCompilableTargetsPredicate() + onlyIf("There are no klibs compiled for the project") { hasCompilableTargets.get() } } private fun Project.dumpKlibsTask( @@ -431,6 +431,8 @@ private class KlibValidationPipelineBuilder( group = "other" from = klibMergeDir.resolve(klibDumpFileName) to = klibApiDir.get().resolve(klibDumpFileName) + val hasCompilableTargets = project.hasCompilableTargetsPredicate() + onlyIf("There are no klibs compiled for the project") { hasCompilableTargets.get() } } private fun Project.extractAbi( @@ -449,6 +451,8 @@ private class KlibValidationPipelineBuilder( supportedTargets = supportedTargets() inputAbiFile = klibApiDir.get().resolve(klibDumpFileName) outputAbiFile = klibOutputDir.resolve(klibDumpFileName) + val hasCompilableTargets = project.hasCompilableTargetsPredicate() + onlyIf("There are no klibs compiled for the project") { hasCompilableTargets.get() } } private fun Project.mergeInferredKlibsUmbrellaTask( @@ -464,6 +468,8 @@ private class KlibValidationPipelineBuilder( "into a single merged KLib ABI dump" dumpFileName = klibDumpFileName mergedFile = klibMergeDir.resolve(klibDumpFileName) + val hasCompilableTargets = project.hasCompilableTargetsPredicate() + onlyIf("There are no dumps to merge") { hasCompilableTargets.get() } } private fun Project.mergeKlibsUmbrellaTask( @@ -475,6 +481,8 @@ private class KlibValidationPipelineBuilder( "different targets into a single merged KLib ABI dump" dumpFileName = klibDumpFileName mergedFile = klibMergeDir.resolve(klibDumpFileName) + val hasCompilableTargets = project.hasCompilableTargetsPredicate() + onlyIf("There are no dumps to merge") { hasCompilableTargets.get() } } fun Project.bannedTargets(): Set { @@ -499,10 +507,7 @@ private class KlibValidationPipelineBuilder( val supportedTargetsProvider = supportedTargets() kotlin.targets.matching { it.emitsKlib }.configureEach { currentTarget -> - val mainCompilations = currentTarget.mainCompilations - if (mainCompilations.none()) { - return@configureEach - } + val mainCompilation = currentTarget.mainCompilationOrNull ?: return@configureEach val targetName = currentTarget.targetName val targetConfig = TargetConfig(project, extension, targetName, intermediateFilesConfig) @@ -510,19 +515,14 @@ private class KlibValidationPipelineBuilder( 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 { - val buildTargetAbi = configureKlibCompilation( - it, extension, targetConfig, - apiBuildDir - ) - mergeTask.configure { - it.addInput(targetName, apiBuildDir) - it.dependsOn(buildTargetAbi) - } - mergeInferredTask.configure { - it.addInput(targetName, apiBuildDir) - it.dependsOn(buildTargetAbi) - } + val buildTargetAbi = configureKlibCompilation(mainCompilation, extension, targetConfig, apiBuildDir) + mergeTask.configure { + it.addInput(targetName, apiBuildDir) + it.dependsOn(buildTargetAbi) + } + mergeInferredTask.configure { + it.addInput(targetName, apiBuildDir) + it.dependsOn(buildTargetAbi) } return@configureEach } @@ -534,9 +534,12 @@ 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, + val proxy = unsupportedTargetDumpProxy( + mainCompilation, + klibApiDir, targetConfig, extractUnderlyingTarget(currentTarget), - apiBuildDir, supportedTargetsProvider) + apiBuildDir, supportedTargetsProvider + ) mergeInferredTask.configure { it.addInput(targetName, apiBuildDir) it.dependsOn(proxy) @@ -555,18 +558,20 @@ private class KlibValidationPipelineBuilder( private fun Project.targetIsSupported(target: KotlinTarget): Boolean { if (bannedTargets().contains(target.targetName)) return false - return when(target) { + return when (target) { is KotlinNativeTarget -> HostManager().isEnabled(target.konanTarget) else -> true } } + // Compilable targets supported by the host compiler private fun Project.supportedTargets(): Provider> { val banned = bannedTargets() // for testing only return project.provider { val hm = HostManager() project.kotlinMultiplatform.targets.matching { it.emitsKlib } .asSequence() + .filter { it.mainCompilationOrNull?.hasAnySources() == true } .filter { if (it is KotlinNativeTarget) { hm.isEnabled(it.konanTarget) && it.targetName !in banned @@ -579,6 +584,14 @@ private class KlibValidationPipelineBuilder( } } + // Returns a predicate that checks if there are any compilable targets + private fun Project.hasCompilableTargetsPredicate(): Provider { + return project.provider { + project.kotlinMultiplatform.targets.matching { it.emitsKlib } + .asSequence() + .any { it.mainCompilationOrNull?.hasAnySources() == true } + } + } private fun Project.configureKlibCompilation( compilation: KotlinCompilation, @@ -590,11 +603,9 @@ private class KlibValidationPipelineBuilder( 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() } } + isEnabled = klibAbiCheckEnabled(projectName, extension) + val hasSourcesPredicate = compilation.hasAnySourcesPredicate() + onlyIf { hasSourcesPredicate.get() } // 'group' is not specified deliberately, so it will be hidden from ./gradlew tasks description = "Builds Kotlin KLib ABI dump for 'main' compilations of $projectName. " + "Complementary task and shouldn't be called manually" @@ -620,6 +631,7 @@ private class KlibValidationPipelineBuilder( } private fun Project.unsupportedTargetDumpProxy( + compilation: KotlinCompilation, klibApiDir: Provider, targetConfig: TargetConfig, underlyingTarget: String, @@ -629,6 +641,8 @@ private class KlibValidationPipelineBuilder( val targetName = targetConfig.targetName!! return project.task(targetConfig.apiTaskName("Infer")) { isEnabled = klibAbiCheckEnabled(project.name, extension) + val hasSourcesPredicate = compilation.hasAnySourcesPredicate() + onlyIf { hasSourcesPredicate.get() } description = "Try to infer the dump for unsupported target $targetName using dumps " + "generated for supported targets." group = "other" @@ -676,10 +690,18 @@ private fun extractUnderlyingTarget(target: KotlinTarget): String { private val Project.kotlinMultiplatform get() = extensions.getByName("kotlin") as KotlinMultiplatformExtension -private val KotlinTarget.mainCompilations - get() = compilations.matching { it.name == "main" } +private val KotlinTarget.mainCompilationOrNull: KotlinCompilation? + get() = compilations.firstOrNull { it.name == KotlinCompilation.MAIN_COMPILATION_NAME } private val Project.jvmDumpFileName: String get() = "$name.api" private val Project.klibDumpFileName: String get() = "$name.klib.api" + +private fun KotlinCompilation.hasAnySources(): Boolean = allKotlinSourceSets.any { + it.kotlin.srcDirs.any(File::exists) +} + +private fun KotlinCompilation.hasAnySourcesPredicate(): Provider = project.provider { + this.hasAnySources() +} diff --git a/src/main/kotlin/KotlinKlibMergeAbiTask.kt b/src/main/kotlin/KotlinKlibMergeAbiTask.kt index ebbf04e..27ce0f2 100644 --- a/src/main/kotlin/KotlinKlibMergeAbiTask.kt +++ b/src/main/kotlin/KotlinKlibMergeAbiTask.kt @@ -54,7 +54,10 @@ internal abstract class KotlinKlibMergeAbiTask : DefaultTask() { internal fun merge() { KlibDump().apply { targetToFile.forEach { (targetName, dumpDir) -> - merge(dumpDir.resolve(dumpFileName), targetName) + val dumpFile = dumpDir.resolve(dumpFileName) + if (dumpFile.exists()) { + merge(dumpFile, targetName) + } } }.saveTo(mergedFile) } From f3b53bbe4eb7d3819eb3f14365d28f718a66d0dc Mon Sep 17 00:00:00 2001 From: Filipp Zhinkin Date: Wed, 3 Apr 2024 20:02:20 +0200 Subject: [PATCH 3/5] Check source sets existence for Klib dump tasks only after compilation (#210) * Added tests * Change dependencies between tasks so that tasks using information about source sets depended on klib compilation --- .../kotlin/kotlinx/validation/api/TestDsl.kt | 8 ++--- .../validation/test/KlibVerificationTests.kt | 29 +++++++++++++++++++ .../classes/GeneratedSources.klib.dump | 12 ++++++++ .../generatedSources.gradle.kts | 22 ++++++++++++++ .../BinaryCompatibilityValidatorPlugin.kt | 9 +++++- 5 files changed, 75 insertions(+), 5 deletions(-) create mode 100644 src/functionalTest/resources/examples/classes/GeneratedSources.klib.dump create mode 100644 src/functionalTest/resources/examples/gradle/configuration/generatedSources/generatedSources.gradle.kts diff --git a/src/functionalTest/kotlin/kotlinx/validation/api/TestDsl.kt b/src/functionalTest/kotlin/kotlinx/validation/api/TestDsl.kt index 42f3c91..43d9677 100644 --- a/src/functionalTest/kotlin/kotlinx/validation/api/TestDsl.kt +++ b/src/functionalTest/kotlin/kotlinx/validation/api/TestDsl.kt @@ -148,8 +148,8 @@ internal fun FileContainer.emptyApiFile(projectName: String) { apiFile(projectName) {} } -internal fun BaseKotlinScope.runner(fn: Runner.() -> Unit) { - val runner = Runner() +internal fun BaseKotlinScope.runner(withConfigurationCache: Boolean = true, fn: Runner.() -> Unit) { + val runner = Runner(withConfigurationCache) fn(runner) this.runner = runner @@ -188,9 +188,9 @@ internal class AppendableScope(val filePath: String) { val files: MutableList = mutableListOf() } -internal class Runner { +internal class Runner(withConfigurationCache: Boolean = true) { val arguments: MutableList = mutableListOf().apply { - if (!koverEnabled) { + if (!koverEnabled && withConfigurationCache) { // Configuration cache is incompatible with javaagents being enabled for Gradle // See https://github.com/gradle/gradle/issues/25979 add("--configuration-cache") diff --git a/src/functionalTest/kotlin/kotlinx/validation/test/KlibVerificationTests.kt b/src/functionalTest/kotlin/kotlinx/validation/test/KlibVerificationTests.kt index 5307e86..a544f03 100644 --- a/src/functionalTest/kotlin/kotlinx/validation/test/KlibVerificationTests.kt +++ b/src/functionalTest/kotlin/kotlinx/validation/test/KlibVerificationTests.kt @@ -669,4 +669,33 @@ internal class KlibVerificationTests : BaseKotlinGradleTest() { } runner.build() } + + @Test + fun `apiDump for a project with generated sources only`() { + val runner = test { + baseProjectSetting() + additionalBuildConfig("/examples/gradle/configuration/generatedSources/generatedSources.gradle.kts") + // TODO: enable configuration cache back when we start skipping tasks correctly + runner(withConfigurationCache = false) { + arguments.add(":apiDump") + } + } + checkKlibDump(runner.build(), "/examples/classes/GeneratedSources.klib.dump") + } + + @Test + fun `apiCheck for a project with generated sources only`() { + val runner = test { + baseProjectSetting() + additionalBuildConfig("/examples/gradle/configuration/generatedSources/generatedSources.gradle.kts") + abiFile(projectName = "testproject") { + resolve("/examples/classes/GeneratedSources.klib.dump") + } + // TODO: enable configuration cache back when we start skipping tasks correctly + runner(withConfigurationCache = false) { + arguments.add(":apiCheck") + } + } + assertApiCheckPassed(runner.build()) + } } diff --git a/src/functionalTest/resources/examples/classes/GeneratedSources.klib.dump b/src/functionalTest/resources/examples/classes/GeneratedSources.klib.dump new file mode 100644 index 0000000..97ba68f --- /dev/null +++ b/src/functionalTest/resources/examples/classes/GeneratedSources.klib.dump @@ -0,0 +1,12 @@ +// 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 /Generated { // /Generated|null[0] + constructor () // /Generated.|(){}[0] + final fun helloCreator(): kotlin/Int // /Generated.helloCreator|helloCreator(){}[0] +} diff --git a/src/functionalTest/resources/examples/gradle/configuration/generatedSources/generatedSources.gradle.kts b/src/functionalTest/resources/examples/gradle/configuration/generatedSources/generatedSources.gradle.kts new file mode 100644 index 0000000..ca514f0 --- /dev/null +++ b/src/functionalTest/resources/examples/gradle/configuration/generatedSources/generatedSources.gradle.kts @@ -0,0 +1,22 @@ +abstract class GenerateSourcesTask : org.gradle.api.DefaultTask() { + @get:org.gradle.api.tasks.OutputDirectory + abstract val outputDirectory: org.gradle.api.file.DirectoryProperty + + @org.gradle.api.tasks.TaskAction + fun generate() { + outputDirectory.asFile.get().mkdirs() + outputDirectory.file("Generated.kt").get().asFile.writeText(""" + public class Generated { public fun helloCreator(): Int = 42 } + """.trimIndent()) + } +} + +val srcgen = project.tasks.register("generateSources", GenerateSourcesTask::class.java) +srcgen.configure { + outputDirectory.set(project.layout.buildDirectory.get().dir("generated").dir("kotlin")) +} + +val kotlin = project.extensions.getByType(org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension::class.java) +kotlin.sourceSets.getByName("commonMain") { + kotlin.srcDir(srcgen) +} diff --git a/src/main/kotlin/BinaryCompatibilityValidatorPlugin.kt b/src/main/kotlin/BinaryCompatibilityValidatorPlugin.kt index 8fda84a..38efe30 100644 --- a/src/main/kotlin/BinaryCompatibilityValidatorPlugin.kt +++ b/src/main/kotlin/BinaryCompatibilityValidatorPlugin.kt @@ -398,9 +398,16 @@ private class KlibValidationPipelineBuilder( commonApiCheck.configure { it.dependsOn(klibCheck) } klibDump.configure { it.dependsOn(klibMergeInferred) } + // Extraction task depends on supportedTargets() provider which returns a set of targets supported + // by the host compiler and having some sources. A set of sources for a target may change until the actual + // klib compilation will take place, so we may observe incorrect value if check source sets earlier. + // Merge task already depends on compilations, so instead of adding each compilation task to the extraction's + // dependency set, we can depend on the merge task itself. + klibExtractAbiForSupportedTargets.configure { + it.dependsOn(klibMerge) + } klibCheck.configure { it.dependsOn(klibExtractAbiForSupportedTargets) - it.dependsOn(klibMerge) } project.configureTargets(klibApiDir, klibMerge, klibMergeInferred) From 4de530aba808fa0e335ceade760c05e57a9ab4d9 Mon Sep 17 00:00:00 2001 From: Filipp Zhinkin Date: Wed, 3 Apr 2024 20:02:50 +0200 Subject: [PATCH 4/5] Revert JVM tasks configuration changes introduced in #200 (#212) * Rollback task skipping logic based on srcset emptyness check for JVM tasks --- .../kotlin/kotlinx/validation/api/Assert.kt | 7 +++ .../validation/test/JvmProjectTests.kt | 54 +++++++++++++++++++ ...kt => MultiPlatformSingleJvmTargetTest.kt} | 44 ++++++++++++++- .../examples/classes/GeneratedSources.dump | 5 ++ .../generatedJvmSources.gradle.kts | 21 ++++++++ .../BinaryCompatibilityValidatorPlugin.kt | 4 +- 6 files changed, 130 insertions(+), 5 deletions(-) create mode 100644 src/functionalTest/kotlin/kotlinx/validation/test/JvmProjectTests.kt rename src/functionalTest/kotlin/kotlinx/validation/test/{MultiPlatformSingleJvmKlibTargetTest.kt => MultiPlatformSingleJvmTargetTest.kt} (77%) create mode 100644 src/functionalTest/resources/examples/classes/GeneratedSources.dump create mode 100644 src/functionalTest/resources/examples/gradle/configuration/generatedSources/generatedJvmSources.gradle.kts diff --git a/src/functionalTest/kotlin/kotlinx/validation/api/Assert.kt b/src/functionalTest/kotlin/kotlinx/validation/api/Assert.kt index 8a3950b..64dd2e4 100644 --- a/src/functionalTest/kotlin/kotlinx/validation/api/Assert.kt +++ b/src/functionalTest/kotlin/kotlinx/validation/api/Assert.kt @@ -31,6 +31,13 @@ internal fun BuildResult.assertTaskSkipped(task: String) { assertTaskOutcome(TaskOutcome.SKIPPED, task) } +/** + * Helper `fun` for asserting a [TaskOutcome] to be equal to [TaskOutcome.UP_TO_DATE] + */ +internal fun BuildResult.assertTaskUpToDate(task: String) { + assertTaskOutcome(TaskOutcome.UP_TO_DATE, task) +} + private fun BuildResult.assertTaskOutcome(taskOutcome: TaskOutcome, taskName: String) { assertEquals(taskOutcome, task(taskName)?.outcome) } diff --git a/src/functionalTest/kotlin/kotlinx/validation/test/JvmProjectTests.kt b/src/functionalTest/kotlin/kotlinx/validation/test/JvmProjectTests.kt new file mode 100644 index 0000000..026f3e5 --- /dev/null +++ b/src/functionalTest/kotlin/kotlinx/validation/test/JvmProjectTests.kt @@ -0,0 +1,54 @@ +/* + * 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.test + +import kotlinx.validation.api.* +import kotlinx.validation.api.resolve +import kotlinx.validation.api.test +import org.assertj.core.api.Assertions +import org.junit.Test + +class JvmProjectTests : BaseKotlinGradleTest() { + @Test + fun `apiDump for a project with generated sources only`() { + val runner = test { + buildGradleKts { + resolve("/examples/gradle/base/withPlugin.gradle.kts") + resolve("/examples/gradle/configuration/generatedSources/generatedJvmSources.gradle.kts") + } + // TODO: enable configuration cache back when we start skipping tasks correctly + runner(withConfigurationCache = false) { + arguments.add(":apiDump") + } + } + runner.build().apply { + assertTaskSuccess(":apiDump") + + val expectedApi = readFileList("/examples/classes/GeneratedSources.dump") + Assertions.assertThat(rootProjectApiDump.readText()).isEqualToIgnoringNewLines(expectedApi) + } + } + + @Test + fun `apiCheck for a project with generated sources only`() { + val runner = test { + buildGradleKts { + resolve("/examples/gradle/base/withPlugin.gradle.kts") + resolve("/examples/gradle/configuration/generatedSources/generatedJvmSources.gradle.kts") + } + apiFile(projectName = rootProjectDir.name) { + resolve("/examples/classes/GeneratedSources.dump") + } + // TODO: enable configuration cache back when we start skipping tasks correctly + runner(withConfigurationCache = false) { + arguments.add(":apiCheck") + } + } + runner.build().apply { + assertTaskSuccess(":apiCheck") + } + } +} diff --git a/src/functionalTest/kotlin/kotlinx/validation/test/MultiPlatformSingleJvmKlibTargetTest.kt b/src/functionalTest/kotlin/kotlinx/validation/test/MultiPlatformSingleJvmTargetTest.kt similarity index 77% rename from src/functionalTest/kotlin/kotlinx/validation/test/MultiPlatformSingleJvmKlibTargetTest.kt rename to src/functionalTest/kotlin/kotlinx/validation/test/MultiPlatformSingleJvmTargetTest.kt index dde1642..0e32ce3 100644 --- a/src/functionalTest/kotlin/kotlinx/validation/test/MultiPlatformSingleJvmKlibTargetTest.kt +++ b/src/functionalTest/kotlin/kotlinx/validation/test/MultiPlatformSingleJvmTargetTest.kt @@ -1,5 +1,5 @@ /* - * Copyright 2016-2021 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. */ @@ -7,10 +7,11 @@ package kotlinx.validation.test import kotlinx.validation.api.* import org.assertj.core.api.Assertions.assertThat +import org.gradle.testkit.runner.TaskOutcome import org.junit.Test import java.io.File -internal class MultiPlatformSingleJvmKlibTargetTest : BaseKotlinGradleTest() { +internal class MultiPlatformSingleJvmTargetTest : BaseKotlinGradleTest() { private fun BaseKotlinScope.createProjectHierarchyWithPluginOnRoot() { settingsGradleKts { resolve("/examples/gradle/settings/settings-name-testproject.gradle.kts") @@ -116,6 +117,45 @@ internal class MultiPlatformSingleJvmKlibTargetTest : BaseKotlinGradleTest() { } } + @Test + fun testApiDumpPassesForEmptyProject() { + val runner = test { + buildGradleKts { + resolve("/examples/gradle/base/multiplatformWithSingleJvmTarget.gradle.kts") + } + + runner { + arguments.add(":apiDump") + } + } + + runner.build().apply { + assertTaskSkipped(":jvmApiDump") + assertTaskUpToDate(":apiDump") + } + } + + @Test + fun testApiCheckPassesForEmptyProject() { + val runner = test { + buildGradleKts { + resolve("/examples/gradle/base/multiplatformWithSingleJvmTarget.gradle.kts") + } + + emptyApiFile(projectName = rootProjectDir.name) + + runner { + arguments.add(":apiCheck") + } + } + + runner.build().apply { + assertTaskSkipped(":jvmApiCheck") + assertTaskUpToDate(":apiCheck") + + } + } + private val jvmApiDump: File get() = rootProjectDir.resolve("$API_DIR/testproject.api") } diff --git a/src/functionalTest/resources/examples/classes/GeneratedSources.dump b/src/functionalTest/resources/examples/classes/GeneratedSources.dump new file mode 100644 index 0000000..f69e8d2 --- /dev/null +++ b/src/functionalTest/resources/examples/classes/GeneratedSources.dump @@ -0,0 +1,5 @@ +public final class Generated { + public fun ()V + public final fun helloCreator ()I +} + diff --git a/src/functionalTest/resources/examples/gradle/configuration/generatedSources/generatedJvmSources.gradle.kts b/src/functionalTest/resources/examples/gradle/configuration/generatedSources/generatedJvmSources.gradle.kts new file mode 100644 index 0000000..57f7ef1 --- /dev/null +++ b/src/functionalTest/resources/examples/gradle/configuration/generatedSources/generatedJvmSources.gradle.kts @@ -0,0 +1,21 @@ +abstract class GenerateSourcesTask : org.gradle.api.DefaultTask() { + @get:org.gradle.api.tasks.OutputDirectory + abstract val outputDirectory: org.gradle.api.file.DirectoryProperty + + @org.gradle.api.tasks.TaskAction + fun generate() { + outputDirectory.asFile.get().mkdirs() + outputDirectory.file("Generated.kt").get().asFile.writeText(""" + public class Generated { public fun helloCreator(): Int = 42 } + """.trimIndent()) + } +} + +val srcgen = project.tasks.register("generateSources", GenerateSourcesTask::class.java) +srcgen.configure { + outputDirectory.set(project.layout.buildDirectory.get().dir("generated").dir("kotlin")) +} + +project.sourceSets.getByName("main") { + kotlin.srcDir(srcgen) +} diff --git a/src/main/kotlin/BinaryCompatibilityValidatorPlugin.kt b/src/main/kotlin/BinaryCompatibilityValidatorPlugin.kt index 38efe30..8f9f6b0 100644 --- a/src/main/kotlin/BinaryCompatibilityValidatorPlugin.kt +++ b/src/main/kotlin/BinaryCompatibilityValidatorPlugin.kt @@ -219,9 +219,7 @@ private fun Project.configureKotlinCompilation( val apiBuild = task(targetConfig.apiTaskName("Build")) { // Do not enable task for empty umbrella modules - isEnabled = apiCheckEnabled(projectName, extension) - val hasSourcesPredicate = compilation.hasAnySourcesPredicate() - onlyIf { hasSourcesPredicate.get() } + isEnabled = apiCheckEnabled(projectName, extension) && compilation.hasAnySources() // '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" From f8e3ded412a7fda76d561456161fa0c1eaccfb51 Mon Sep 17 00:00:00 2001 From: Filipp Zhinkin Date: Thu, 4 Apr 2024 10:10:09 +0200 Subject: [PATCH 5/5] Version 0.15.0-Beta.2 --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 594ee5f..55e2c03 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ Binary compatibility validator is a Gradle plugin that can be added to your buil - in `build.gradle.kts` ```kotlin plugins { - id("org.jetbrains.kotlinx.binary-compatibility-validator") version "0.15.0-Beta.1" + id("org.jetbrains.kotlinx.binary-compatibility-validator") version "0.15.0-Beta.2" } ``` @@ -38,7 +38,7 @@ plugins { ```groovy plugins { - id 'org.jetbrains.kotlinx.binary-compatibility-validator' version '0.15.0-Beta.1' + id 'org.jetbrains.kotlinx.binary-compatibility-validator' version '0.15.0-Beta.2' } ```