diff --git a/api/binary-compatibility-validator.api b/api/binary-compatibility-validator.api index ea5804ca..ad8325d0 100644 --- a/api/binary-compatibility-validator.api +++ b/api/binary-compatibility-validator.api @@ -31,22 +31,13 @@ public final class kotlinx/validation/BinaryCompatibilityValidatorPlugin : org/g } 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 final fun getIgnoredClasses ()Lorg/gradle/api/provider/SetProperty; + public final fun getIgnoredPackages ()Lorg/gradle/api/provider/SetProperty; + public final fun getNonPublicMarkers ()Lorg/gradle/api/provider/SetProperty; + public final fun getPublicClasses ()Lorg/gradle/api/provider/SetProperty; + public final fun getPublicMarkers ()Lorg/gradle/api/provider/SetProperty; + public final fun getPublicPackages ()Lorg/gradle/api/provider/SetProperty; } public abstract interface annotation class kotlinx/validation/ExperimentalBCVApi : java/lang/annotation/Annotation { @@ -55,6 +46,12 @@ public abstract interface annotation class kotlinx/validation/ExperimentalBCVApi public abstract interface annotation class kotlinx/validation/ExternalApi : java/lang/annotation/Annotation { } +public final class kotlinx/validation/KlibDumpMetadata : java/io/Serializable { + public fun (Lkotlinx/validation/api/klib/KlibTarget;Lorg/gradle/api/file/RegularFileProperty;)V + public final fun getDumpFile ()Lorg/gradle/api/file/RegularFileProperty; + public final fun getTarget ()Lkotlinx/validation/api/klib/KlibTarget; +} + public class kotlinx/validation/KlibValidationSettings { public fun ()V public final fun getEnabled ()Z @@ -65,24 +62,52 @@ public class kotlinx/validation/KlibValidationSettings { public final fun setStrictValidation (Z)V } -public class kotlinx/validation/KotlinApiBuildTask : kotlinx/validation/BuildTaskBase { - public field inputDependencies Lorg/gradle/api/file/FileCollection; +public abstract class kotlinx/validation/KotlinApiBuildTask : kotlinx/validation/BuildTaskBase { 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 setInputClassesDirs (Lorg/gradle/api/file/FileCollection;)V - public final fun setInputDependencies (Lorg/gradle/api/file/FileCollection;)V + public abstract fun getInputClassesDirs ()Lorg/gradle/api/file/ConfigurableFileCollection; + public abstract fun getInputDependencies ()Lorg/gradle/api/file/ConfigurableFileCollection; + public abstract fun getInputJar ()Lorg/gradle/api/file/RegularFileProperty; + public abstract fun getOutputApiFile ()Lorg/gradle/api/file/RegularFileProperty; } public class kotlinx/validation/KotlinApiCompareTask : org/gradle/api/DefaultTask { - public field generatedApiFile Ljava/io/File; - public field projectApiFile Ljava/io/File; public fun ()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 final fun getGeneratedApiFile ()Lorg/gradle/api/file/RegularFileProperty; + public final fun getProjectApiFile ()Lorg/gradle/api/file/RegularFileProperty; +} + +public abstract class kotlinx/validation/KotlinKlibAbiBuildTask : kotlinx/validation/BuildTaskBase { + public fun ()V + public abstract fun getKlibFile ()Lorg/gradle/api/file/ConfigurableFileCollection; + public abstract fun getOutputAbiFile ()Lorg/gradle/api/file/RegularFileProperty; + public final fun getSignatureVersion ()Lorg/gradle/api/provider/Property; + public abstract fun getTarget ()Lorg/gradle/api/provider/Property; +} + +public abstract class kotlinx/validation/KotlinKlibExtractAbiTask : org/gradle/api/DefaultTask { + public fun ()V + public abstract fun getInputAbiFile ()Lorg/gradle/api/file/RegularFileProperty; + public abstract fun getOutputAbiFile ()Lorg/gradle/api/file/RegularFileProperty; + public abstract fun getRequiredTargets ()Lorg/gradle/api/provider/SetProperty; + public final fun getStrictValidation ()Lorg/gradle/api/provider/Property; +} + +public abstract class kotlinx/validation/KotlinKlibInferAbiTask : org/gradle/api/DefaultTask { + public fun ()V + public abstract fun getInputDumps ()Lorg/gradle/api/provider/SetProperty; + public abstract fun getOldMergedKlibDump ()Lorg/gradle/api/file/RegularFileProperty; + public abstract fun getOutputAbiFile ()Lorg/gradle/api/file/RegularFileProperty; + public abstract fun getTarget ()Lorg/gradle/api/provider/Property; +} + +public abstract class kotlinx/validation/KotlinKlibMergeAbiTask : org/gradle/api/DefaultTask { + public fun ()V + public abstract fun getDumps ()Lorg/gradle/api/provider/SetProperty; + public abstract fun getMergedApiFile ()Lorg/gradle/api/file/RegularFileProperty; +} + +public final class kotlinx/validation/_UtilsKt { + public static final fun toKlibTarget (Lorg/jetbrains/kotlin/gradle/plugin/KotlinTarget;)Lkotlinx/validation/api/klib/KlibTarget; } public final class kotlinx/validation/api/ClassBinarySignature { @@ -162,7 +187,7 @@ public final class kotlinx/validation/api/klib/KlibDumpKt { public static final fun saveTo (Lkotlinx/validation/api/klib/KlibDump;Ljava/io/File;)V } -public final class kotlinx/validation/api/klib/KlibSignatureVersion { +public final class kotlinx/validation/api/klib/KlibSignatureVersion : java/io/Serializable { public static final field Companion Lkotlinx/validation/api/klib/KlibSignatureVersion$Companion; public fun equals (Ljava/lang/Object;)Z public fun hashCode ()I @@ -174,7 +199,7 @@ public final class kotlinx/validation/api/klib/KlibSignatureVersion$Companion { public final fun of (I)Lkotlinx/validation/api/klib/KlibSignatureVersion; } -public final class kotlinx/validation/api/klib/KlibTarget { +public final class kotlinx/validation/api/klib/KlibTarget : java/io/Serializable { 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; diff --git a/src/functionalTest/kotlin/kotlinx/validation/test/DefaultConfigTests.kt b/src/functionalTest/kotlin/kotlinx/validation/test/DefaultConfigTests.kt index 3879d9f4..490632c7 100644 --- a/src/functionalTest/kotlin/kotlinx/validation/test/DefaultConfigTests.kt +++ b/src/functionalTest/kotlin/kotlinx/validation/test/DefaultConfigTests.kt @@ -26,8 +26,13 @@ internal class DefaultConfigTests : BaseKotlinGradleTest() { } } + val projectName = rootProjectDir.name runner.buildAndFail().apply { - assertTrue { output.contains("Please ensure that ':apiDump' was executed") } + Assertions.assertThat(output).contains( + "Expected file with API declarations 'api/$projectName.api' does not exist." + ).contains( + "Please ensure that ':apiDump' was executed in order to get an API dump to compare the build against" + ) assertTaskFailure(":apiCheck") } } diff --git a/src/functionalTest/kotlin/kotlinx/validation/test/JvmProjectTests.kt b/src/functionalTest/kotlin/kotlinx/validation/test/JvmProjectTests.kt index 026f3e59..1f9ef007 100644 --- a/src/functionalTest/kotlin/kotlinx/validation/test/JvmProjectTests.kt +++ b/src/functionalTest/kotlin/kotlinx/validation/test/JvmProjectTests.kt @@ -19,8 +19,7 @@ class JvmProjectTests : BaseKotlinGradleTest() { 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) { + runner { arguments.add(":apiDump") } } @@ -42,8 +41,7 @@ class JvmProjectTests : BaseKotlinGradleTest() { apiFile(projectName = rootProjectDir.name) { resolve("/examples/classes/GeneratedSources.dump") } - // TODO: enable configuration cache back when we start skipping tasks correctly - runner(withConfigurationCache = false) { + runner { arguments.add(":apiCheck") } } diff --git a/src/functionalTest/kotlin/kotlinx/validation/test/KlibVerificationTests.kt b/src/functionalTest/kotlin/kotlinx/validation/test/KlibVerificationTests.kt index b8ee412c..372f9a60 100644 --- a/src/functionalTest/kotlin/kotlinx/validation/test/KlibVerificationTests.kt +++ b/src/functionalTest/kotlin/kotlinx/validation/test/KlibVerificationTests.kt @@ -13,6 +13,8 @@ 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.jetbrains.kotlin.utils.addToStdlib.butIf +import org.junit.Assert import org.junit.Assume import org.junit.Test import java.io.File @@ -48,27 +50,32 @@ internal class KlibVerificationTests : BaseKotlinGradleTest() { 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") } @@ -451,7 +458,7 @@ internal class KlibVerificationTests : BaseKotlinGradleTest() { } @Test - fun `klibCheck if all klib-targets are unavailable`() { + fun `klibCheck should not fail if all klib-targets are unavailable`() { val runner = test { baseProjectSetting() addToSrcSet("/examples/classes/TopLevelDeclarations.kt") @@ -468,10 +475,32 @@ internal class KlibVerificationTests : BaseKotlinGradleTest() { } } + runner.build().apply { + assertTaskSuccess(":klibApiCheck") + } + } + + @Test + fun `klibCheck should fail with strict validation if all klib-targets are unavailable`() { + val runner = test { + 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") + } + 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 one enabled klib target, but none were found." - ) + assertTaskFailure(":klibApiExtractForValidation") } } @@ -580,8 +609,10 @@ internal class KlibVerificationTests : BaseKotlinGradleTest() { 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 updatedSourceFile = File( + this::class.java.getResource( + "/examples/classes/AnotherBuildConfigModified.kt" + )!!.toURI() ) val existingSource = runner.projectDir.resolve( "src/commonMain/kotlin/AnotherBuildConfig.kt" @@ -601,8 +632,10 @@ internal class KlibVerificationTests : BaseKotlinGradleTest() { 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 updatedSourceFile = File( + this::class.java.getResource( + "/examples/classes/AnotherBuildConfigLinuxArm64.kt" + )!!.toURI() ) val existingSource = runner.projectDir.resolve( "src/linuxArm64Main/kotlin/AnotherBuildConfigLinuxArm64.kt" @@ -626,8 +659,10 @@ internal class KlibVerificationTests : BaseKotlinGradleTest() { assertApiCheckPassed(runner.build()) // Update the source file by adding a declaration - val updatedSourceFile = File(this::class.java.getResource( - "/examples/classes/AnotherBuildConfigModified.kt")!!.toURI() + val updatedSourceFile = File( + this::class.java.getResource( + "/examples/classes/AnotherBuildConfigModified.kt" + )!!.toURI() ) val existingSource = runner.projectDir.resolve( "src/commonMain/kotlin/AnotherBuildConfig.kt" @@ -665,12 +700,29 @@ internal class KlibVerificationTests : BaseKotlinGradleTest() { runApiDump() } - runner.build().apply { + runner.withDebug(true).build().apply { assertTaskSkipped(":klibApiDump") } assertFalse(runner.projectDir.resolve("api").exists()) } + @Test + fun `apiDump should remove dump file if the project does not contain sources anymore`() { + val runner = test { + baseProjectSetting() + addToSrcSet("/examples/classes/AnotherBuildConfig.kt", sourceSet = "commonTest") + abiFile(projectName = "testproject") { + resolve("/examples/classes/AnotherBuildConfig.klib.dump") + } + runApiDump() + } + + runner.build().apply { + assertTaskSuccess(":klibApiDump") + } + assertFalse(runner.projectDir.resolve("api").resolve("testproject.klib.api").exists()) + } + @Test fun `apiDump should not fail if there is only one target`() { val runner = test { @@ -683,13 +735,18 @@ internal class KlibVerificationTests : BaseKotlinGradleTest() { } @Test - fun `apiCheck should not fail for empty project`() { + fun `apiCheck should fail for empty project`() { val runner = test { baseProjectSetting() addToSrcSet("/examples/classes/AnotherBuildConfig.kt", sourceSet = "commonTest") runApiCheck() } - runner.build() + runner.buildAndFail().apply { + assertTaskFailure(":klibApiExtractForValidation") + Assertions.assertThat(output).contains( + "File with project's API declarations 'api/testproject.klib.api' does not exist." + ) + } } @Test @@ -697,8 +754,7 @@ internal class KlibVerificationTests : BaseKotlinGradleTest() { 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) { + runner { arguments.add(":apiDump") } } @@ -713,11 +769,26 @@ internal class KlibVerificationTests : BaseKotlinGradleTest() { abiFile(projectName = "testproject") { resolve("/examples/classes/GeneratedSources.klib.dump") } - // TODO: enable configuration cache back when we start skipping tasks correctly - runner(withConfigurationCache = false) { + runner { arguments.add(":apiCheck") } } assertApiCheckPassed(runner.build()) } + + @Test + fun `apiCheck should fail after a source set was removed`() { + val runner = test { + baseProjectSetting() + addToSrcSet("/examples/classes/AnotherBuildConfig.kt", "linuxX64Main") + addToSrcSet("/examples/classes/AnotherBuildConfig.kt", "linuxArm64Main") + abiFile(projectName = "testproject") { + resolve("/examples/classes/AnotherBuildConfig.klib.dump") + } + runApiCheck() + } + runner.buildAndFail().apply { + assertTaskFailure(":klibApiCheck") + } + } } diff --git a/src/functionalTest/kotlin/kotlinx/validation/test/MultiPlatformSingleJvmTargetTest.kt b/src/functionalTest/kotlin/kotlinx/validation/test/MultiPlatformSingleJvmTargetTest.kt index 0e32ce33..94a2cac4 100644 --- a/src/functionalTest/kotlin/kotlinx/validation/test/MultiPlatformSingleJvmTargetTest.kt +++ b/src/functionalTest/kotlin/kotlinx/validation/test/MultiPlatformSingleJvmTargetTest.kt @@ -7,7 +7,6 @@ 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 diff --git a/src/main/kotlin/-Utils.kt b/src/main/kotlin/-Utils.kt new file mode 100644 index 00000000..e6e0049c --- /dev/null +++ b/src/main/kotlin/-Utils.kt @@ -0,0 +1,62 @@ +/* + * 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.api.klib.KlibTarget +import kotlinx.validation.api.klib.konanTargetNameMapping +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.tasks.* +import org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType +import org.jetbrains.kotlin.gradle.plugin.KotlinTarget +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 java.io.Serializable + +/** + * Converts [KotlinTarget] to a [KlibTarget]. + */ +public fun KotlinTarget.toKlibTarget(): KlibTarget = KlibTarget(extractUnderlyingTarget(this), targetName) + +private fun extractUnderlyingTarget(target: KotlinTarget): String { + 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}") + } +} + +/** + * Information about a generated klib dump. + */ +public class KlibDumpMetadata( + /** + * The target the dump was generated for. + */ + @get:Input + public val target: KlibTarget, + + /** + * Path to a resulting dump file. + * + * If a dump file was not generated for a particular [target] (which may + * 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. + */ + @get:InputFiles + @get:SkipWhenEmpty + @get:PathSensitive(PathSensitivity.RELATIVE) + public val dumpFile: RegularFileProperty +) : Serializable diff --git a/src/main/kotlin/BinaryCompatibilityValidatorPlugin.kt b/src/main/kotlin/BinaryCompatibilityValidatorPlugin.kt index a34191e8..c498aa3c 100644 --- a/src/main/kotlin/BinaryCompatibilityValidatorPlugin.kt +++ b/src/main/kotlin/BinaryCompatibilityValidatorPlugin.kt @@ -6,7 +6,6 @@ package kotlinx.validation import kotlinx.validation.api.klib.KlibTarget -import kotlinx.validation.api.klib.konanTargetNameMapping import org.gradle.api.* import org.gradle.api.plugins.* import org.gradle.api.provider.* @@ -14,8 +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.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 @@ -53,7 +50,6 @@ public class BinaryCompatibilityValidatorPlugin : Plugin { } } - @OptIn(ExperimentalBCVApi::class) private fun configureProject(project: Project, extension: ApiValidationExtension) { configureKotlinPlugin(project, extension) configureAndroidPlugin(project, extension) @@ -215,7 +211,7 @@ private fun Project.configureKotlinCompilation( val projectName = project.name val dumpFileName = project.jvmDumpFileName val apiDirProvider = targetConfig.apiDir - val apiBuildDir = apiDirProvider.map { layout.buildDirectory.asFile.get().resolve(it) } + val apiBuildDir = apiDirProvider.flatMap { f -> layout.buildDirectory.asFile.map { it.resolve(f) } } val apiBuild = task(targetConfig.apiTaskName("Build")) { // Do not enable task for empty umbrella modules @@ -225,19 +221,14 @@ private fun Project.configureKotlinCompilation( "Builds Kotlin API for 'main' compilations of $projectName. Complementary task and shouldn't be called manually" if (useOutput) { // Workaround for #4 - inputClassesDirs = - files(provider { if (isEnabled) compilation.output.classesDirs else emptyList() }) - inputDependencies = - files(provider { if (isEnabled) compilation.output.classesDirs else emptyList() }) + inputClassesDirs.from(compilation.output.classesDirs) } else { - inputClassesDirs = - files(provider { if (isEnabled) compilation.output.classesDirs else emptyList() }) - inputDependencies = - files(provider { if (isEnabled) compilation.compileDependencyFiles else emptyList() }) + inputClassesDirs.from(compilation.output.classesDirs) + inputDependencies.from(compilation.compileDependencyFiles) } - outputApiFile = apiBuildDir.get().resolve(dumpFileName) + outputApiFile.fileProvider(apiBuildDir.map { it.resolve(dumpFileName) }) } - configureCheckTasks(apiBuildDir, apiBuild, extension, targetConfig, commonApiDump, commonApiCheck) + configureCheckTasks(apiBuild, extension, targetConfig, commonApiDump, commonApiCheck) } internal val Project.sourceSets: SourceSetContainer @@ -262,7 +253,7 @@ private fun Project.configureApiTasks( ) { val projectName = project.name val dumpFileName = project.jvmDumpFileName - val apiBuildDir = targetConfig.apiDir.map { layout.buildDirectory.asFile.get().resolve(it) } + val apiBuildDir = targetConfig.apiDir.flatMap { f -> layout.buildDirectory.asFile.map { it.resolve(f) } } val sourceSetsOutputsProvider = project.provider { sourceSets .filter { it.name == SourceSet.MAIN_SOURCE_SET_NAME || it.name in extension.additionalSourceSets } @@ -274,18 +265,15 @@ private fun Project.configureApiTasks( // '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" - inputClassesDirs = files(provider { if (isEnabled) sourceSetsOutputsProvider.get() else emptyList() }) - inputDependencies = - files(provider { if (isEnabled) sourceSetsOutputsProvider.get() else emptyList() }) - outputApiFile = apiBuildDir.get().resolve(dumpFileName) + inputClassesDirs.from(sourceSetsOutputsProvider) + outputApiFile.fileProvider(apiBuildDir.map { it.resolve(dumpFileName) }) } - configureCheckTasks(apiBuildDir, apiBuild, extension, targetConfig) + configureCheckTasks(apiBuild, extension, targetConfig) } private fun Project.configureCheckTasks( - apiBuildDir: Provider, - apiBuild: TaskProvider<*>, + apiBuild: TaskProvider, extension: ApiValidationExtension, targetConfig: TargetConfig, commonApiDump: TaskProvider? = null, @@ -301,19 +289,17 @@ 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" - projectApiFile = apiCheckDir.get().resolve(jvmDumpFileName) - generatedApiFile = apiBuildDir.get().resolve(jvmDumpFileName) - dependsOn(apiBuild) + projectApiFile.fileProvider(apiCheckDir.map { it.resolve(jvmDumpFileName) }) + generatedApiFile.set(apiBuild.flatMap { it.outputApiFile }) } val dumpFileName = project.jvmDumpFileName - val apiDump = task(targetConfig.apiTaskName("Dump")) { + val apiDump = task(targetConfig.apiTaskName("Dump")) { isEnabled = apiCheckEnabled(projectName, extension) && apiBuild.map { it.enabled }.getOrElse(true) group = "other" description = "Syncs the API file for $projectName" - from = apiBuildDir.get().resolve(dumpFileName) - to = apiCheckDir.get().resolve(dumpFileName) - dependsOn(apiBuild) + from.set(apiBuild.flatMap { it.outputApiFile }) + to.fileProvider(apiCheckDir.map { it.resolve(dumpFileName) }) } commonApiDump?.configure { it.dependsOn(apiDump) } @@ -346,7 +332,7 @@ private const val KLIB_INFERRED_DUMPS_DIRECTORY = "klib-all" * 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 + * - `klibApiDump` ([SyncFile]) 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 @@ -354,8 +340,8 @@ private const val KLIB_INFERRED_DUMPS_DIRECTORY = "klib-all" * 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 + * instantiating [KotlinKlibInferAbiTask]); + * - `klibApiExtractForValidation` ([KotlinKlibExtractAbiTask]) 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 @@ -378,73 +364,57 @@ private class KlibValidationPipelineBuilder( TargetConfig(project, extension, KLIB_INFERRED_DUMPS_DIRECTORY, intermediateFilesConfig) val projectDir = project.projectDir - val klibApiDir = klibApiDirConfig?.map { - projectDir.resolve(it.apiDir.get()) + val klibApiDir = klibApiDirConfig?.flatMap { + it.apiDir.map { projectDir.resolve(it) } }!! - val projectBuildDir = project.layout.buildDirectory.asFile.get() - val klibMergeDir = projectBuildDir.resolve(klibDumpConfig.apiDir.get()) - val klibMergeInferredDir = projectBuildDir.resolve(klibInferDumpConfig.apiDir.get()) - val klibExtractedFileDir = klibMergeInferredDir.resolve("extracted") + val projectBuildDir = project.layout.buildDirectory.asFile + val klibMergeDir = projectBuildDir.flatMap { pd -> klibDumpConfig.apiDir.map { pd.resolve(it) } } + val klibMergeInferredDir = projectBuildDir.flatMap { pd -> klibInferDumpConfig.apiDir.map { pd.resolve(it) } } + val klibExtractedFileDir = klibMergeInferredDir.map { it.resolve("extracted") } val klibMerge = project.mergeKlibsUmbrellaTask(klibDumpConfig, klibMergeDir) val klibMergeInferred = project.mergeInferredKlibsUmbrellaTask(klibDumpConfig, klibMergeInferredDir) - val klibDump = project.dumpKlibsTask(klibDumpConfig, klibApiDir, klibMergeInferredDir) + val klibDump = project.dumpKlibsTask(klibDumpConfig) 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(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) + val klibCheck = project.checkKlibsTask(klibDumpConfig) + klibDump.configure { + it.from.set(klibMergeInferred.flatMap { it.mergedApiFile }) + val filename = project.klibDumpFileName + it.to.fileProvider(klibApiDir.map { it.resolve(filename) }) } klibCheck.configure { - it.dependsOn(klibExtractAbiForSupportedTargets) + it.projectApiFile.set(klibExtractAbiForSupportedTargets.flatMap { it.outputAbiFile }) + it.generatedApiFile.set(klibMerge.flatMap { it.mergedApiFile }) } - + commonApiDump.configure { it.dependsOn(klibDump) } + commonApiCheck.configure { it.dependsOn(klibCheck) } project.configureTargets(klibApiDir, klibMerge, klibMergeInferred) } - 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 a public KLib ABI against the golden value in ABI folder for " + - 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.checkKlibsTask(klibDumpConfig: TargetConfig) = + project.task(klibDumpConfig.apiTaskName("Check")) { + isEnabled = klibAbiCheckEnabled(project.name, extension) + group = "verification" + description = + "Checks signatures of a public KLib ABI against the golden value in ABI folder for ${project.name}" + } - private fun Project.dumpKlibsTask( - klibDumpConfig: TargetConfig, - klibApiDir: Provider, - klibMergeDir: File - ) = project.task(klibDumpConfig.apiTaskName("Dump")) { - isEnabled = klibAbiCheckEnabled(project.name, extension) - description = "Syncs the KLib ABI file for ${project.name}" - 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.dumpKlibsTask(klibDumpConfig: TargetConfig) = + project.task(klibDumpConfig.apiTaskName("Dump")) { + isEnabled = klibAbiCheckEnabled(project.name, extension) + description = "Syncs the KLib ABI file for ${project.name}" + group = "other" + onlyIf { + it as SyncFile + it.to.get().asFile.exists() || it.from.get().asFile.exists() + } + } private fun Project.extractAbi( klibDumpConfig: TargetConfig, klibApiDir: Provider, - klibOutputDir: File - ) = project.task( + klibOutputDir: Provider + ) = project.task( klibDumpConfig.apiTaskName("ExtractForValidation") ) { @@ -452,17 +422,15 @@ private class KlibValidationPipelineBuilder( 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 - 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() } + strictValidation.set(extension.klib.strictValidation) + requiredTargets.addAll(supportedTargets()) + inputAbiFile.fileProvider(klibApiDir.map { it.resolve(klibDumpFileName) }) + outputAbiFile.fileProvider(klibOutputDir.map { it.resolve(klibDumpFileName) }) } private fun Project.mergeInferredKlibsUmbrellaTask( klibDumpConfig: TargetConfig, - klibMergeDir: File, + klibMergeDir: Provider, ) = project.task( klibDumpConfig.apiTaskName("MergeInferred") ) @@ -471,23 +439,17 @@ private class KlibValidationPipelineBuilder( 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) - val hasCompilableTargets = project.hasCompilableTargetsPredicate() - onlyIf("There are no dumps to merge") { hasCompilableTargets.get() } + mergedApiFile.fileProvider(klibMergeDir.map { it.resolve(klibDumpFileName) }) } private fun Project.mergeKlibsUmbrellaTask( klibDumpConfig: TargetConfig, - klibMergeDir: File + klibMergeDir: Provider ) = project.task(klibDumpConfig.apiTaskName("Merge")) { isEnabled = klibAbiCheckEnabled(project.name, extension) description = "Merges multiple KLib ABI dump files generated for " + "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() } + mergedApiFile.fileProvider(klibMergeDir.map { it.resolve(klibDumpFileName) }) } fun Project.bannedTargets(): Set { @@ -510,25 +472,35 @@ private class KlibValidationPipelineBuilder( ) { val kotlin = project.kotlinMultiplatform - val supportedTargetsProvider = supportedTargets() + val generatedDumps = objects.setProperty(KlibDumpMetadata::class.java) + val inferredDumps = objects.setProperty(KlibDumpMetadata::class.java) + mergeTask.configure { + it.dumps.addAll(generatedDumps) + } + mergeInferredTask.configure { + it.dumps.addAll(generatedDumps) + it.dumps.addAll(inferredDumps) + } + kotlin.targets.matching { it.emitsKlib }.configureEach { currentTarget -> val mainCompilation = currentTarget.mainCompilationOrNull ?: return@configureEach + val target = currentTarget.toKlibTarget() 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 apiBuildDir = + targetConfig.apiDir.flatMap { f -> project.layout.buildDirectory.asFile.map { it.resolve(f) } } 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) { - 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) - } + val buildTargetAbi = configureKlibCompilation( + mainCompilation, extension, targetConfig, + target, apiBuildDir + ) + generatedDumps.add( + KlibDumpMetadata(target, + objects.fileProperty().also { it.set(buildTargetAbi.flatMap { it.outputAbiFile }) }) + ) return@configureEach } // If the target is unsupported, the regular merge task will only depend on a task complaining about @@ -540,24 +512,17 @@ 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( - mainCompilation, klibApiDir, targetConfig, - extractUnderlyingTarget(currentTarget), - apiBuildDir, supportedTargetsProvider + currentTarget.toKlibTarget(), + apiBuildDir + ) + proxy.configure { it.inputDumps.addAll(generatedDumps) } + inferredDumps.add( + KlibDumpMetadata(currentTarget.toKlibTarget(), + objects.fileProperty().also { + it.set(proxy.flatMap { it.outputAbiFile }) + }) ) - mergeInferredTask.configure { - it.addInput(targetName, apiBuildDir) - it.dependsOn(proxy) - } - } - mergeTask.configure { - it.doFirst { - if (supportedTargetsProvider.get().isEmpty()) { - throw IllegalStateException( - "KLib ABI dump/validation requires at least one enabled klib target, but none were found." - ) - } - } } } @@ -570,13 +535,12 @@ private class KlibValidationPipelineBuilder( } // Compilable targets supported by the host compiler - private fun Project.supportedTargets(): Provider> { + 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 @@ -584,40 +548,29 @@ private class KlibValidationPipelineBuilder( true } } - .map { KlibTarget(extractUnderlyingTarget(it), it.targetName).toString() } + .map { it.toKlibTarget() } .toSet() } } - // 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, extension: ApiValidationExtension, targetConfig: TargetConfig, - apiBuildDir: File + target: KlibTarget, + apiBuildDir: Provider ): 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) - 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" - klibFile = project.files(project.provider { compilation.output.classesDirs }) - compilationDependencies = project.files(project.provider { compilation.compileDependencyFiles }) - signatureVersion = SerializableSignatureVersion(extension.klib.signatureVersion) - outputApiFile = apiBuildDir.resolve(klibDumpFileName) + this.target.set(target) + klibFile.from(compilation.output.classesDirs) + signatureVersion.set(extension.klib.signatureVersion) + outputAbiFile.fileProvider(apiBuildDir.map { it.resolve(klibDumpFileName) }) } return buildTask } @@ -636,29 +589,20 @@ private class KlibValidationPipelineBuilder( } private fun Project.unsupportedTargetDumpProxy( - compilation: KotlinCompilation, klibApiDir: Provider, targetConfig: TargetConfig, - underlyingTarget: String, - apiBuildDir: File, - supportedTargets: Provider> - ): TaskProvider { + unsupportedTarget: KlibTarget, + apiBuildDir: Provider + ): TaskProvider { val targetName = targetConfig.targetName!! - return project.task(targetConfig.apiTaskName("Infer")) { + 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" - this.supportedTargets = supportedTargets - inputImageFile = klibApiDir.get().resolve(klibDumpFileName) - outputApiDir = apiBuildDir.toString() - outputFile = apiBuildDir.resolve(klibDumpFileName) - unsupportedTargetName = targetConfig.targetName - unsupportedTargetCanonicalName = underlyingTarget - dumpFileName = klibDumpFileName - dependsOn(project.tasks.withType(KotlinKlibAbiBuildTask::class.java)) + target.set(unsupportedTarget) + oldMergedKlibDump.fileProvider(klibApiDir.map { it.resolve(klibDumpFileName) }) + outputAbiFile.fileProvider(apiBuildDir.map { it.resolve(klibDumpFileName) }) } } } @@ -677,21 +621,6 @@ private val KotlinTarget.jvmBased: Boolean return platformType == KotlinPlatformType.jvm || platformType == KotlinPlatformType.androidJvm } -private fun extractUnderlyingTarget(target: KotlinTarget): String { - 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}") - } -} - private val Project.kotlinMultiplatform get() = extensions.getByName("kotlin") as KotlinMultiplatformExtension @@ -706,7 +635,3 @@ private val Project.klibDumpFileName: String 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/BuildTaskBase.kt b/src/main/kotlin/BuildTaskBase.kt index a042f2d8..e4dd551c 100644 --- a/src/main/kotlin/BuildTaskBase.kt +++ b/src/main/kotlin/BuildTaskBase.kt @@ -6,52 +6,42 @@ package kotlinx.validation import org.gradle.api.DefaultTask +import org.gradle.api.provider.SetProperty import org.gradle.api.tasks.Input import org.gradle.api.tasks.Internal -import org.gradle.api.tasks.OutputFile -import java.io.File public abstract class BuildTaskBase : DefaultTask() { private val extension = project.apiValidationExtensionOrNull - @OutputFile - public lateinit var outputApiFile: File + private fun stringSetProperty(provider: ApiValidationExtension.() -> Set): SetProperty { + return project.objects.setProperty(String::class.java).convention( + project.provider { + if (extension == null) { + emptySet() + } else { + provider(extension) + } + } + ) + } - private var _ignoredPackages: Set? = null @get:Input - public var ignoredPackages : Set - get() = _ignoredPackages ?: extension?.ignoredPackages ?: emptySet() - set(value) { _ignoredPackages = value } + public val ignoredPackages: SetProperty = stringSetProperty { ignoredPackages } - private var _nonPublicMarkes: Set? = null @get:Input - public var nonPublicMarkers : Set - get() = _nonPublicMarkes ?: extension?.nonPublicMarkers ?: emptySet() - set(value) { _nonPublicMarkes = value } + public val nonPublicMarkers: SetProperty = stringSetProperty { nonPublicMarkers } - private var _ignoredClasses: Set? = null @get:Input - public var ignoredClasses : Set - get() = _ignoredClasses ?: extension?.ignoredClasses ?: emptySet() - set(value) { _ignoredClasses = value } + public val ignoredClasses: SetProperty = stringSetProperty { ignoredClasses } - private var _publicPackages: Set? = null @get:Input - public var publicPackages: Set - get() = _publicPackages ?: extension?.publicPackages ?: emptySet() - set(value) { _publicPackages = value } + public val publicPackages: SetProperty = stringSetProperty { publicPackages } - private var _publicMarkers: Set? = null @get:Input - public var publicMarkers: Set - get() = _publicMarkers ?: extension?.publicMarkers ?: emptySet() - set(value) { _publicMarkers = value} + public val publicMarkers: SetProperty = stringSetProperty { publicMarkers } - private var _publicClasses: Set? = null @get:Input - public var publicClasses: Set - get() = _publicClasses ?: extension?.publicClasses ?: emptySet() - set(value) { _publicClasses = value } + public val publicClasses: SetProperty = stringSetProperty { publicClasses } @get:Internal internal val projectName = project.name diff --git a/src/main/kotlin/KotlinApiBuildTask.kt b/src/main/kotlin/KotlinApiBuildTask.kt index 70881de4..7e3c1a7a 100644 --- a/src/main/kotlin/KotlinApiBuildTask.kt +++ b/src/main/kotlin/KotlinApiBuildTask.kt @@ -12,54 +12,59 @@ import org.gradle.api.tasks.* import java.util.jar.JarFile import javax.inject.Inject -public open class KotlinApiBuildTask @Inject constructor( +public abstract class KotlinApiBuildTask @Inject constructor( ) : BuildTaskBase() { + @get:OutputFile + public abstract val outputApiFile: RegularFileProperty - @InputFiles - @Optional - @PathSensitive(PathSensitivity.RELATIVE) - public var inputClassesDirs: FileCollection? = null + @get:InputFiles + @get:Optional + @get:PathSensitive(PathSensitivity.RELATIVE) + public abstract val inputClassesDirs: ConfigurableFileCollection - @InputFile - @Optional - @PathSensitive(PathSensitivity.RELATIVE) - public val inputJar: RegularFileProperty = this.project.objects.fileProperty() + @get:InputFile + @get:Optional + @get:PathSensitive(PathSensitivity.RELATIVE) + public abstract val inputJar: RegularFileProperty - @InputFiles - @PathSensitive(PathSensitivity.RELATIVE) - public lateinit var inputDependencies: FileCollection + @get:InputFiles + @get:Optional + @get:PathSensitive(PathSensitivity.RELATIVE) + public abstract val inputDependencies: ConfigurableFileCollection @TaskAction internal fun generate() { - outputApiFile.delete() - outputApiFile.parentFile.mkdirs() - val inputClassesDirs = inputClassesDirs val signatures = when { // inputJar takes precedence if specified inputJar.isPresent -> JarFile(inputJar.get().asFile).use { it.loadApiFromJvmClasses() } - inputClassesDirs != null -> + + inputClassesDirs.any() -> inputClassesDirs.asFileTree.asSequence() .filter { !it.isDirectory && it.name.endsWith(".class") && !it.name.startsWith("META-INF/") } .map { it.inputStream() } .loadApiFromJvmClasses() + else -> throw GradleException("KotlinApiBuildTask should have either inputClassesDirs, or inputJar property set") } - val publicPackagesNames = signatures.extractAnnotatedPackages(publicMarkers.map(::replaceDots).toSet()) - val ignoredPackagesNames = signatures.extractAnnotatedPackages(nonPublicMarkers.map(::replaceDots).toSet()) + val publicPackagesNames = signatures.extractAnnotatedPackages(publicMarkers.get().map(::replaceDots).toSet()) + val ignoredPackagesNames = + signatures.extractAnnotatedPackages(nonPublicMarkers.get().map(::replaceDots).toSet()) val filteredSignatures = signatures - .retainExplicitlyIncludedIfDeclared(publicPackages + publicPackagesNames, - publicClasses, publicMarkers) - .filterOutNonPublic(ignoredPackages + ignoredPackagesNames, ignoredClasses) - .filterOutAnnotated(nonPublicMarkers.map(::replaceDots).toSet()) + .retainExplicitlyIncludedIfDeclared( + publicPackages.get() + publicPackagesNames, + publicClasses.get(), publicMarkers.get() + ) + .filterOutNonPublic(ignoredPackages.get() + ignoredPackagesNames, ignoredClasses.get()) + .filterOutAnnotated(nonPublicMarkers.get().map(::replaceDots).toSet()) - outputApiFile.bufferedWriter().use { writer -> + outputApiFile.asFile.get().bufferedWriter().use { writer -> filteredSignatures.dump(writer) } } diff --git a/src/main/kotlin/KotlinApiCompareTask.kt b/src/main/kotlin/KotlinApiCompareTask.kt index 72399b6a..37845f4b 100644 --- a/src/main/kotlin/KotlinApiCompareTask.kt +++ b/src/main/kotlin/KotlinApiCompareTask.kt @@ -10,68 +10,68 @@ import com.github.difflib.UnifiedDiffUtils import java.io.* import javax.inject.Inject import org.gradle.api.* +import org.gradle.api.file.RegularFileProperty import org.gradle.api.tasks.* -public open class KotlinApiCompareTask @Inject constructor(): DefaultTask() { +public open class KotlinApiCompareTask : DefaultTask() { - @InputFiles - @PathSensitive(PathSensitivity.RELATIVE) - public lateinit var projectApiFile: File + @get:InputFiles // don't fail the task if file does not exist, instead print custom error message from verify() + @get:PathSensitive(PathSensitivity.RELATIVE) + public val projectApiFile: RegularFileProperty = project.objects.fileProperty() - @InputFiles - public lateinit var generatedApiFile: File + @get:InputFiles // don't fail the task if file does not exist, instead print custom error message from verify() + @get:PathSensitive(PathSensitivity.RELATIVE) + public val generatedApiFile: RegularFileProperty = project.objects.fileProperty() private val projectName = project.name - private val rootDir = project.rootProject.rootDir + private val rootDir = project.rootDir @TaskAction internal fun verify() { - 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 + val projectApiFile = projectApiFile.get().asFile + val generatedApiFile = generatedApiFile.get().asFile if (!projectApiFile.exists()) { - error("File ${projectApiFile.name} is missing from ${projectApiDir.relativeDirPath()}, please run " + - ":$subject:apiDump task to generate one") + error( + "Expected file with API declarations '${projectApiFile.relativeTo(rootDir)}' does not exist.\n" + + "Please ensure that ':apiDump' was executed in order to get " + + "an API dump to compare the build against" + ) } if (!generatedApiFile.exists()) { - error("File ${generatedApiFile.name} is missing from dump results.") + error( + "Expected file with generated API declarations '${generatedApiFile.relativeTo(rootDir)}'" + + " does not exist." + ) } - // Normalize case-sensitivity val diffSet = mutableSetOf() val diff = compareFiles(projectApiFile, generatedApiFile) if (diff != null) diffSet.add(diff) if (diffSet.isNotEmpty()) { val diffText = diffSet.joinToString("\n\n") - error("API check failed for project $subject.\n$diffText\n\n You can run :$subject:apiDump task to overwrite API declarations") + val subject = projectName + error( + "API check failed for project $subject.\n$diffText\n\n" + + "You can run :$subject:apiDump task to overwrite API declarations" + ) } } - private fun File.relativeDirPath(): String { - return toRelativeString(rootDir) + File.separator - } - private fun compareFiles(checkFile: File, builtFile: File): String? { val checkText = checkFile.readText() val builtText = builtFile.readText() - // We don't compare full text because newlines on Windows & Linux/macOS are different + // We don't compare a full text because newlines on Windows & Linux/macOS are different val checkLines = checkText.lines() val builtLines = builtText.lines() if (checkLines == builtLines) return null val patch = DiffUtils.diff(checkLines, builtLines) - val diff = UnifiedDiffUtils.generateUnifiedDiff(checkFile.toString(), builtFile.toString(), checkLines, patch, 3) + val diff = + UnifiedDiffUtils.generateUnifiedDiff(checkFile.toString(), builtFile.toString(), checkLines, patch, 3) return diff.joinToString("\n") } } diff --git a/src/main/kotlin/KotlinKlibAbiBuildTask.kt b/src/main/kotlin/KotlinKlibAbiBuildTask.kt index c591c632..29552beb 100644 --- a/src/main/kotlin/KotlinKlibAbiBuildTask.kt +++ b/src/main/kotlin/KotlinKlibAbiBuildTask.kt @@ -5,66 +5,66 @@ 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 -import org.gradle.api.tasks.Optional -import org.gradle.api.tasks.TaskAction -import java.io.Serializable - -internal class SerializableSignatureVersion(val version: Int) : Serializable { - constructor(version: KlibSignatureVersion) : this(version.version) - - fun toKlibSignatureVersion(): KlibSignatureVersion = KlibSignatureVersion(version) -} +import kotlinx.validation.api.klib.* +import org.gradle.api.file.ConfigurableFileCollection +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.provider.Property +import org.gradle.api.tasks.* /** * Generates a text file with a KLib ABI dump for a single klib. */ -internal abstract class KotlinKlibAbiBuildTask : BuildTaskBase() { +@CacheableTask +public abstract class KotlinKlibAbiBuildTask : BuildTaskBase() { /** - * Path to a klib to dump. + * Collection consisting of a single path to a compiled klib (either file, or directory). + * + * By the end of the compilation process, there might be no klib file emitted, + * for example, when there are no sources in a project in general, + * or for a target in particular. + * The lack of a compiled klib file is not considered as an error, and + * instead causes the ask being skipped. */ - @InputFiles - lateinit var klibFile: FileCollection + @get:InputFiles + @get:SkipWhenEmpty + @get:PathSensitive(PathSensitivity.RELATIVE) + public abstract val klibFile: ConfigurableFileCollection /** - * Bind this task with a klib compilation. + * Refer to [KlibValidationSettings.signatureVersion] for details. */ - @InputFiles - lateinit var compilationDependencies: FileCollection + @get:Input + public val signatureVersion: Property = + project.objects.property(KlibSignatureVersion::class.java) + .convention(KlibSignatureVersion.LATEST) /** - * Refer to [KlibValidationSettings.signatureVersion] for details. + * A target [klibFile] was compiled for. */ - @Optional @get:Input - var signatureVersion: SerializableSignatureVersion = SerializableSignatureVersion(KlibSignatureVersion.LATEST) + public abstract val target: Property /** - * Name of a target [klibFile] was compiled for. + * A path to the resulting dump file. */ - @Input - lateinit var target: String + @get:OutputFile + public abstract val outputAbiFile: RegularFileProperty @OptIn(ExperimentalBCVApi::class) @TaskAction internal fun generate() { - outputApiFile.delete() - outputApiFile.parentFile.mkdirs() + val outputFile = outputAbiFile.asFile.get() + outputFile.delete() + outputFile.parentFile.mkdirs() - 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() + val dump = KlibDump.fromKlib(klibFile.singleFile, target.get().configurableName, KLibDumpFilters { + ignoredClasses.addAll(this@KotlinKlibAbiBuildTask.ignoredClasses.get()) + ignoredPackages.addAll(this@KotlinKlibAbiBuildTask.ignoredPackages.get()) + nonPublicMarkers.addAll(this@KotlinKlibAbiBuildTask.nonPublicMarkers.get()) + signatureVersion = this@KotlinKlibAbiBuildTask.signatureVersion.get() }) - dump.saveTo(outputApiFile) + dump.saveTo(outputFile) } } diff --git a/src/main/kotlin/KotlinKlibExtractAbiTask.kt b/src/main/kotlin/KotlinKlibExtractAbiTask.kt new file mode 100644 index 00000000..2722f7a6 --- /dev/null +++ b/src/main/kotlin/KotlinKlibExtractAbiTask.kt @@ -0,0 +1,77 @@ +/* + * 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.api.klib.KlibDump +import kotlinx.validation.api.klib.KlibTarget +import kotlinx.validation.api.klib.saveTo +import org.gradle.api.DefaultTask +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.provider.Property +import org.gradle.api.provider.SetProperty +import org.gradle.api.tasks.* + +/** + * Extracts dump for targets supported by the host compiler from a merged API dump stored in a project. + * + * If some targets the dump stored in a project directory was generated for are not supported by the host compiler, + * only supported tasks could be extracted for further validation. + */ +@CacheableTask +public abstract class KotlinKlibExtractAbiTask : DefaultTask() { + /** + * Merged KLib dump that should be filtered by this task. + */ + @get:InputFiles // don't fail the task if file does not exist, instead print custom error message from generate() + @get:PathSensitive(PathSensitivity.RELATIVE) + public abstract val inputAbiFile: RegularFileProperty + + /** + * List of the targets that the resulting dump should contain. + */ + @get:Input + public abstract val requiredTargets: SetProperty + + /** + * Refer to [KlibValidationSettings.strictValidation] for details. + */ + @get:Input + public val strictValidation: Property = project.objects.property(Boolean::class.java).convention(false) + + /** + * A path to the resulting dump file. + */ + @get:OutputFile + public abstract val outputAbiFile: RegularFileProperty + + private val rootDir = project.rootDir + + @OptIn(ExperimentalBCVApi::class) + @TaskAction + internal fun generate() { + val inputFile = inputAbiFile.asFile.get() + if (!inputFile.exists()) { + error("File with project's API declarations '${inputFile.relativeTo(rootDir)}' does not exist.\n" + + "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.") + } + val dump = KlibDump.from(inputFile) + val enabledTargets = requiredTargets.get().map(KlibTarget::targetName).toSet() + // 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.get()) { + throw IllegalStateException( + "Validation could not be performed as some targets are not available " + + "and the strictValidation mode was enabled." + ) + } + dump.remove(targetsToRemove) + dump.saveTo(outputAbiFile.asFile.get()) + } +} diff --git a/src/main/kotlin/KotlinKlibExtractSupportedTargetsAbiTask.kt b/src/main/kotlin/KotlinKlibExtractSupportedTargetsAbiTask.kt deleted file mode 100644 index 58298a18..00000000 --- a/src/main/kotlin/KotlinKlibExtractSupportedTargetsAbiTask.kt +++ /dev/null @@ -1,68 +0,0 @@ -/* - * 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.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.* -import java.io.File - -/** - * Extracts dump for targets supported by the host compiler from a merged API dump stored in a project. - */ -internal abstract class KotlinKlibExtractSupportedTargetsAbiTask : DefaultTask() { - @get:Internal - internal val projectName = project.name - - /** - * Merged KLib dump that should be filtered by this task. - */ - @InputFiles - lateinit var inputAbiFile: File - - /** - * A path to the resulting dump file. - */ - @OutputFile - lateinit var outputAbiFile: File - - /** - * Provider returning targets supported by the host compiler. - */ - @get:Input - lateinit var supportedTargets: Provider> - - /** - * Refer to [KlibValidationSettings.strictValidation] for details. - */ - @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 = 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( - "Validation could not be performed as some targets are not available " + - "and the strictValidation mode was enabled." - ) - } - dump.remove(targetsToRemove) - dump.saveTo(outputAbiFile) - } -} diff --git a/src/main/kotlin/KotlinKlibInferAbiForUnsupportedTargetTask.kt b/src/main/kotlin/KotlinKlibInferAbiTask.kt similarity index 65% rename from src/main/kotlin/KotlinKlibInferAbiForUnsupportedTargetTask.kt rename to src/main/kotlin/KotlinKlibInferAbiTask.kt index af0228f0..889b33ec 100644 --- a/src/main/kotlin/KotlinKlibInferAbiForUnsupportedTargetTask.kt +++ b/src/main/kotlin/KotlinKlibInferAbiTask.kt @@ -8,9 +8,10 @@ package kotlinx.validation import kotlinx.validation.api.klib.* import kotlinx.validation.api.klib.TargetHierarchy import org.gradle.api.DefaultTask -import org.gradle.api.provider.Provider +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.io.File /** * Task infers a possible KLib ABI dump for an unsupported target. @@ -22,64 +23,45 @@ 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. */ -internal abstract class KotlinKlibInferAbiForUnsupportedTargetTask : DefaultTask() { - @get:Internal - internal val projectName = project.name - - /** - * The name of a target to infer a dump for. - */ - @Input - lateinit var unsupportedTargetName: String - +@CacheableTask +public abstract class KotlinKlibInferAbiTask : DefaultTask() { /** * The name of a target to infer a dump for. */ - @Input - lateinit var unsupportedTargetCanonicalName: String + @get:Input + public abstract val target: Property /** - * A root directory containing dumps successfully generated for each supported target. - * It is assumed that this directory contains subdirectories named after targets. + * Newly created dumps that will be used for ABI inference. */ - @InputFiles - lateinit var outputApiDir: String - - /** - * Set of all supported targets. - */ - @Input - lateinit var supportedTargets: Provider> + @get:Nested + public abstract val inputDumps: SetProperty /** * Previously generated merged ABI dump file, the golden image every dump should be verified against. */ - @InputFiles - lateinit var inputImageFile: File - - /** - * The name of a dump file. - */ - @Input - lateinit var dumpFileName: String + @get:InputFiles + @get:PathSensitive(PathSensitivity.RELATIVE) + public abstract val oldMergedKlibDump: RegularFileProperty /** * A path to an inferred dump file. */ - @OutputFile - lateinit var outputFile: File + @get:OutputFile + public abstract val outputAbiFile: RegularFileProperty @OptIn(ExperimentalBCVApi::class) @TaskAction internal fun generate() { - val unsupportedTarget = KlibTarget(unsupportedTargetCanonicalName, unsupportedTargetName) - val supportedTargetNames = supportedTargets.get().map { KlibTarget.parse(it) }.toSet() + val availableDumps = inputDumps.get().map { + it.target to it.dumpFile.asFile.get() + }.filter { it.second.exists() }.toMap() // 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 matchingTargets = findMatchingTargets(availableDumps.keys, target.get()) // Load dumps that are a good fit for inference val supportedTargetDumps = matchingTargets.map { target -> - val dumpFile = File(outputApiDir).parentFile.resolve(target.configurableName).resolve(dumpFileName) + val dumpFile = availableDumps[target]!! KlibDump.from(dumpFile, target.configurableName).also { check(it.targets.single() == target) } @@ -87,25 +69,26 @@ internal abstract class KotlinKlibInferAbiForUnsupportedTargetTask : DefaultTask // Load an old dump, if any var image: KlibDump? = null - if (inputImageFile.exists()) { - if (inputImageFile.length() > 0L) { - image = KlibDump.from(inputImageFile) + val oldDumpFile = oldMergedKlibDump.asFile.get() + if (oldDumpFile.exists()) { + if (oldDumpFile.length() > 0L) { + image = KlibDump.from(oldDumpFile) } else { logger.warn( - "Project's ABI file exists, but empty: $inputImageFile. " + + "Project's ABI file exists, but empty: $oldDumpFile. " + "The file will be ignored during ABI dump inference for the unsupported target " + - unsupportedTarget + target.get() ) } } - inferAbi(unsupportedTarget, supportedTargetDumps, image).saveTo(outputFile) + inferAbi(target.get(), supportedTargetDumps, image).saveTo(outputAbiFile.asFile.get()) logger.warn( - "An ABI dump for target $unsupportedTarget was inferred from the ABI generated for the following targets " + + "An ABI dump for target ${target.get()} 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. " + + "Inferred dump may not reflect an actual ABI for the target ${target.get()}. " + "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 27ce0f27..8596ba69 100644 --- a/src/main/kotlin/KotlinKlibMergeAbiTask.kt +++ b/src/main/kotlin/KotlinKlibMergeAbiTask.kt @@ -8,57 +8,40 @@ package kotlinx.validation import kotlinx.validation.api.klib.KlibDump import kotlinx.validation.api.klib.saveTo import org.gradle.api.DefaultTask +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.provider.SetProperty import org.gradle.api.tasks.* -import java.io.File /** * Merges multiple individual KLib ABI dumps into a single merged dump. */ -internal abstract class KotlinKlibMergeAbiTask : DefaultTask() { - private val targetToFile = mutableMapOf() - - @get:Internal - internal val projectName = project.name - - /** - * Set of targets whose dumps should be merged. - */ - @get:Input - 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 - +@CacheableTask +public abstract class KotlinKlibMergeAbiTask : DefaultTask() { /** - * A path to a resulting merged dump. + * 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. */ - @OutputFile - lateinit var mergedFile: File + @get:Nested + public abstract val dumps: SetProperty /** - * The name of a dump file. + * A path to a resulting merged dump file. */ - @Input - lateinit var dumpFileName: String - - internal fun addInput(target: String, file: File) { - targetToFile[target] = file - } + @get:OutputFile + public abstract val mergedApiFile: RegularFileProperty @OptIn(ExperimentalBCVApi::class) @TaskAction internal fun merge() { KlibDump().apply { - targetToFile.forEach { (targetName, dumpDir) -> - val dumpFile = dumpDir.resolve(dumpFileName) + dumps.get().forEach { dump -> + val dumpFile = dump.dumpFile.asFile.get() if (dumpFile.exists()) { - merge(dumpFile, targetName) + merge(dumpFile, dump.target.configurableName) } } - }.saveTo(mergedFile) + }.saveTo(mergedApiFile.asFile.get()) } } diff --git a/src/main/kotlin/CopyFile.kt b/src/main/kotlin/SyncFile.kt similarity index 51% rename from src/main/kotlin/CopyFile.kt rename to src/main/kotlin/SyncFile.kt index df9f6d79..fba06d10 100644 --- a/src/main/kotlin/CopyFile.kt +++ b/src/main/kotlin/SyncFile.kt @@ -6,10 +6,9 @@ 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 org.gradle.api.file.RegularFileProperty +import org.gradle.api.tasks.* +import org.gradle.work.DisableCachingByDefault import java.nio.file.Files import java.nio.file.StandardCopyOption @@ -17,15 +16,23 @@ import java.nio.file.StandardCopyOption // 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 +@DisableCachingByDefault(because = "No computations, only copying files") +internal abstract class SyncFile : DefaultTask() { + @get:InputFiles + @get:PathSensitive(PathSensitivity.RELATIVE) + abstract val from: RegularFileProperty - @OutputFile - lateinit var to: File + @get:OutputFile + abstract val to: RegularFileProperty @TaskAction fun copy() { - Files.copy(from.toPath(), to.toPath(), StandardCopyOption.REPLACE_EXISTING) + val fromFile = from.asFile.get() + val toFile = to.asFile.get() + if (fromFile.exists()) { + fromFile.copyTo(toFile, overwrite = true) + } else { + Files.deleteIfExists(toFile.toPath()) + } } } diff --git a/src/main/kotlin/api/klib/KlibSignatureVersion.kt b/src/main/kotlin/api/klib/KlibSignatureVersion.kt index b9c50e55..45ad7c4d 100644 --- a/src/main/kotlin/api/klib/KlibSignatureVersion.kt +++ b/src/main/kotlin/api/klib/KlibSignatureVersion.kt @@ -5,7 +5,9 @@ package kotlinx.validation.api.klib -public class KlibSignatureVersion internal constructor(internal val version: Int) { +import java.io.Serializable + +public class KlibSignatureVersion internal constructor(internal val version: Int) : Serializable { public companion object { public fun of(value: Int): KlibSignatureVersion { @@ -16,6 +18,9 @@ public class KlibSignatureVersion internal constructor(internal val version: Int } public val LATEST: KlibSignatureVersion = KlibSignatureVersion(Int.MIN_VALUE) + + @JvmStatic + private val serialVersionUID: Long = 1 } override fun equals(other: Any?): Boolean { diff --git a/src/main/kotlin/api/klib/KlibTarget.kt b/src/main/kotlin/api/klib/KlibTarget.kt index bfcc78da..37f30bfc 100644 --- a/src/main/kotlin/api/klib/KlibTarget.kt +++ b/src/main/kotlin/api/klib/KlibTarget.kt @@ -5,6 +5,8 @@ package kotlinx.validation.api.klib +import java.io.Serializable + /** * Target name consisting of two parts: a [configurableName] that could be configured by a user, and an [targetName] @@ -25,7 +27,7 @@ public class KlibTarget internal constructor( * Usually, it's the same name as [targetName]. */ public val configurableName: String -) { +) : Serializable { init { require(!configurableName.contains(".")) { "Configurable name can't contain the '.' character: $configurableName" @@ -54,6 +56,9 @@ public class KlibTarget internal constructor( } return KlibTarget(parts[0], parts[1]) } + + @JvmStatic + private val serialVersionUID: Long = 1 }