Repository: Kotlin/binary-compatibility-validator (kotlin)

Issue:
JVM/Android-only modules create empty klib.api file on 0.15.0-Beta.3
On `0.15.0-Beta.3`, the plugin generates an empty `klib.api` if the [module only supports Android/JVM targets](https://github.com/coil-kt/coil/blob/main/coil-network-okhttp/build.gradle.kts#L16). This causes the binary compatibility check to fail with the error `Project ABI file XYZ.klib.api is empty.`. The release notes mention adding modules to `ignoredProjects`, but I believe that also disables checking the `android/XYZ.api` and `jvm/XYZ.api` files. I have the new klib validation support enabled with `strictValidation = true`:

```kotlin
klib {
    enabled = true
    strictValidation = true
}
```

To reproduce:

Run `./gradlew apiDump` and then `./gradlew apiCheck` in [this project](https://github.com/coil-kt/coil/pull/2343). I can make a smaller repro project if needed.

Pull request:
After #243 resolution, JVM/JVM on KMP/KMP projects validation behavior remained different for projects without non-test sources or without published API:
- For JVM projects (of all kinds), we generate empty dump files and verify that the dump remained empty on `apiCheck` execution;
- For KMP KLib-targets, dump generation is skipped for such projects; the validation fails due to a missing dump file.

This PR addresses #246 and aligns KMP validation behavior for empty projects with how JVM ABI validation works for them.

Patch:
diff --git a/src/main/kotlin/-Utils.kt b/src/main/kotlin/-Utils.kt
index e6e0049c..98ee94e8 100644
--- a/src/main/kotlin/-Utils.kt
+++ b/src/main/kotlin/-Utils.kt
@@ -53,10 +53,10 @@ public class KlibDumpMetadata(
      * happen for an empty project, a project having only test targets,
      * or a project that has no sources for a particular target),
      * [KlibDumpMetadata] will not force an error.
-     * Instead, a dependent task will be skipped.
+     * It's up to [KlibDumpMetadata] users to check if a [dumpFile] exists and
+     * perform a required action accordingly.
      */
     @get:InputFiles
-    @get:SkipWhenEmpty
     @get:PathSensitive(PathSensitivity.RELATIVE)
     public val dumpFile: RegularFileProperty
 ) : Serializable
diff --git a/src/main/kotlin/KotlinKlibExtractAbiTask.kt b/src/main/kotlin/KotlinKlibExtractAbiTask.kt
index 33b3b3e5..18920da4 100644
--- a/src/main/kotlin/KotlinKlibExtractAbiTask.kt
+++ b/src/main/kotlin/KotlinKlibExtractAbiTask.kt
@@ -13,6 +13,8 @@ import org.gradle.api.file.RegularFileProperty
 import org.gradle.api.provider.Property
 import org.gradle.api.provider.SetProperty
 import org.gradle.api.tasks.*
+import java.nio.file.Files
+import java.nio.file.StandardCopyOption
 
 /**
  * Extracts dump for targets supported by the host compiler from a merged API dump stored in a project.
@@ -58,7 +60,8 @@ public abstract class KotlinKlibExtractAbiTask : DefaultTask() {
                     "Please ensure that ':apiDump' was executed in order to get API dump to compare the build against")
         }
         if (inputFile.length() == 0L) {
-            error("Project ABI file ${inputFile.relativeTo(rootDir)} is empty.")
+            Files.copy(inputFile.toPath(), outputAbiFile.asFile.get().toPath(), StandardCopyOption.REPLACE_EXISTING)
+            return
         }
         val dump = KlibDump.from(inputFile)
         val unsupportedTargets = targetsToRemove.get().map(KlibTarget::targetName).toSet()
diff --git a/src/main/kotlin/KotlinKlibMergeAbiTask.kt b/src/main/kotlin/KotlinKlibMergeAbiTask.kt
index 8596ba69..0a136338 100644
--- a/src/main/kotlin/KotlinKlibMergeAbiTask.kt
+++ b/src/main/kotlin/KotlinKlibMergeAbiTask.kt
@@ -20,8 +20,7 @@ public abstract class KotlinKlibMergeAbiTask : DefaultTask() {
     /**
      * Dumps to merge.
      *
-     * If a file referred by [KlibDumpMetadata.dumpFile] does not exist, it will be ignored and corresponding
-     * target will not be mentioned in the resulting merged dump.
+     * If there is no dump for a particular target, its [KlibDumpMetadata.dumpFile] won't exist.
      */
     @get:Nested
     public abstract val dumps: SetProperty<KlibDumpMetadata>


Test patch:
diff --git a/src/functionalTest/kotlin/kotlinx/validation/test/KlibVerificationTests.kt b/src/functionalTest/kotlin/kotlinx/validation/test/KlibVerificationTests.kt
index 47b5972c..64e1ebb8 100644
--- a/src/functionalTest/kotlin/kotlinx/validation/test/KlibVerificationTests.kt
+++ b/src/functionalTest/kotlin/kotlinx/validation/test/KlibVerificationTests.kt
@@ -700,14 +700,16 @@ internal class KlibVerificationTests : BaseKotlinGradleTest() {
             runApiDump()
         }
 
-        runner.withDebug(true).build().apply {
-            assertTaskSkipped(":klibApiDump")
+        runner.build().apply {
+            assertTaskSuccess(":klibApiDump")
         }
-        assertFalse(runner.projectDir.resolve("api").exists())
+        val apiDumpFile = rootProjectAbiDump("testproject")
+        assertTrue(apiDumpFile.exists())
+        assertTrue(apiDumpFile.readText().isEmpty())
     }
 
     @Test
-    fun `apiDump should remove dump file if the project does not contain sources anymore`() {
+    fun `apiDump should dump empty file if the project does not contain sources anymore`() {
         val runner = test {
             baseProjectSetting()
             addToSrcSet("/examples/classes/AnotherBuildConfig.kt", sourceSet = "commonTest")
@@ -720,7 +722,9 @@ internal class KlibVerificationTests : BaseKotlinGradleTest() {
         runner.build().apply {
             assertTaskSuccess(":klibApiDump")
         }
-        assertFalse(runner.projectDir.resolve("api").resolve("testproject.klib.api").exists())
+        val dumpFile = rootProjectAbiDump("testproject")
+        assertTrue(dumpFile.exists())
+        assertTrue(dumpFile.readText().isEmpty())
     }
 
     @Test
@@ -735,7 +739,7 @@ internal class KlibVerificationTests : BaseKotlinGradleTest() {
     }
 
     @Test
-    fun `apiCheck should fail for empty project`() {
+    fun `apiCheck should fail for empty project without a dump file`() {
         val runner = test {
             baseProjectSetting()
             addToSrcSet("/examples/classes/AnotherBuildConfig.kt", sourceSet = "commonTest")
@@ -749,6 +753,21 @@ internal class KlibVerificationTests : BaseKotlinGradleTest() {
         }
     }
 
+    @Test
+    fun `apiCheck should not fail for empty project with an empty dump file`() {
+        val runner = test {
+            baseProjectSetting()
+            addToSrcSet("/examples/classes/AnotherBuildConfig.kt", sourceSet = "commonTest")
+            abiFile("testproject") {
+                // empty dump file
+            }
+            runApiCheck()
+        }
+        runner.build().apply {
+            assertTaskSuccess(":klibApiCheck")
+        }
+    }
+
     @Test
     fun `apiDump for a project with generated sources only`() {
         val runner = test {
diff --git a/src/functionalTest/kotlin/kotlinx/validation/test/MultipleJvmTargetsTest.kt b/src/functionalTest/kotlin/kotlinx/validation/test/MultipleJvmTargetsTest.kt
index a142bd58..46781e59 100644
--- a/src/functionalTest/kotlin/kotlinx/validation/test/MultipleJvmTargetsTest.kt
+++ b/src/functionalTest/kotlin/kotlinx/validation/test/MultipleJvmTargetsTest.kt
@@ -124,8 +124,6 @@ internal class MultipleJvmTargetsTest : BaseKotlinGradleTest() {
             assertTaskSuccess(":jvmApiDump")
             assertTaskSuccess(":anotherJvmApiDump")
 
-            System.err.println(output)
-
             val anotherExpectedApi = readFileList("/examples/classes/Subsub1Class.dump")
             assertThat(anotherApiDump.readText()).isEqualToIgnoringNewLines(anotherExpectedApi)