diff --git a/README.md b/README.md index 6e173157..cc26af4e 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,20 @@ [![License](https://img.shields.io/github/license/Kotlin/binary-compatibility-validator)](LICENSE.TXT) [![KDoc link](https://img.shields.io/badge/API_reference-KDoc-blue)](https://kotlin.github.io/binary-compatibility-validator/) +# Support for this plugin has been discontinued + +> [!WARNING] +> The development of a separate binary compatibility validator Gradle plugin has been discontinued, +> and all its functionality will be moved to Kotlin Gradle Plugin starting from the [`2.2.0` release](https://kotlinlang.org/docs/whatsnew22.html#binary-compatibility-validation-included-in-kotlin-gradle-plugin). +> +> As part of the migration, the code of the current plugin has been migrated to [the Kotlin repository](https://github.com/JetBrains/kotlin/tree/master/libraries/tools/abi-validation), +> as well as issues migrated to [the Kotlin project in YouTrack](https://youtrack.jetbrains.com/issues/KT?q=subsystems:%20%7BTools.%20BCV%7D,%20%7BTools.%20Gradle.%20BCV%7D). +> +> This plugin is frozen from changes, no new features or minor bugfixes will be added to it. +> +> The functionality of working with the ABI in Kotlin Gradle Plugin is in an experimental state now, +> so it is recommended to continue using this plugin in production projects until KGP API stabilization. + # Binary compatibility validator The tool allows dumping binary API of a JVM part of a Kotlin library that is public in the sense of Kotlin visibilities and ensures that the public binary API wasn't changed in a way that makes this change binary incompatible. @@ -37,7 +51,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.17.0" + id("org.jetbrains.kotlinx.binary-compatibility-validator") version "0.18.0" } ``` @@ -45,7 +59,7 @@ plugins { ```groovy plugins { - id 'org.jetbrains.kotlinx.binary-compatibility-validator' version '0.17.0' + id 'org.jetbrains.kotlinx.binary-compatibility-validator' version '0.18.0' } ``` diff --git a/build.gradle.kts b/build.gradle.kts index ca84df73..2296343a 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,4 +1,4 @@ -import kotlinx.kover.gradle.plugin.dsl.MetricType +import kotlinx.kover.gradle.plugin.dsl.CoverageUnit import kotlinx.validation.build.mavenCentralMetadata import kotlinx.validation.build.mavenRepositoryPublishing import kotlinx.validation.build.signPublicationIfKeyPresent @@ -215,7 +215,7 @@ tasks.withType().configureEach { } kover { - koverReport { + reports { filters { excludes { packages("kotlinx.validation.test") @@ -223,8 +223,8 @@ kover { } verify { rule { - minBound(80, MetricType.BRANCH) - minBound(90, MetricType.LINE) + minBound(80, CoverageUnit.BRANCH) + minBound(90, CoverageUnit.LINE) } } } diff --git a/gradle.properties b/gradle.properties index 948cb450..54e40fdc 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,3 +1,3 @@ -version=0.17.1-SNAPSHOT +version=0.19.0-SNAPSHOT kotlin.stdlib.default.dependency=false diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index bf4e6684..22bdfcbb 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -4,9 +4,8 @@ kotlin = "1.9.22" javaDiffUtils = "4.12" junit = "5.9.2" -kotest = "5.5.5" -kotlinx-bcv = "0.13.1" -ow2Asm = "9.7.1" +kotlinx-bcv = "0.17.0" +ow2Asm = "9.8" dokka = "1.9.20" gradlePluginPublishPlugin = "1.1.0" @@ -44,5 +43,5 @@ gradlePlugin-android = { module = "com.android.tools.build:gradle", version.ref [plugins] -kover = { id = "org.jetbrains.kotlinx.kover", version = "0.7.5" } +kover = { id = "org.jetbrains.kotlinx.kover", version = "0.9.1" } dokka = { id = "org.jetbrains.dokka", version.ref = "dokka" } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index e6aba251..a7a990ab 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists +distributionSha256Sum=c16d517b50dd28b3f5838f0e844b7520b8f1eb610f2f29de7e4e04a1b7c9c79b distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-all.zip networkTimeout=10000 validateDistributionUrl=true diff --git a/src/functionalTest/kotlin/kotlinx/validation/test/KlibVerificationTests.kt b/src/functionalTest/kotlin/kotlinx/validation/test/KlibVerificationTests.kt index b47fe4e4..64d1df78 100644 --- a/src/functionalTest/kotlin/kotlinx/validation/test/KlibVerificationTests.kt +++ b/src/functionalTest/kotlin/kotlinx/validation/test/KlibVerificationTests.kt @@ -850,4 +850,25 @@ internal class KlibVerificationTests : BaseKotlinGradleTest() { .contains("+// Targets: [linuxArm64]") } } + + @Test + fun `check cross compilation support`() { + Assume.assumeFalse(HostManager().isEnabled(KonanTarget.MACOS_ARM64)) + + val runner = test { + settingsGradleKts { + resolve("/examples/gradle/settings/settings-name-testproject.gradle.kts") + } + buildGradleKts { + resolve("/examples/gradle/base/withNativePluginAndCrossCompilation.gradle.kts") + } + additionalBuildConfig("/examples/gradle/configuration/appleTargets/targets.gradle.kts") + addToSrcSet("/examples/classes/TopLevelDeclarations.kt") + runner { + arguments.addAll(listOf(":apiDump", "-Pkotlin.native.enableKlibsCrossCompilation=true")) + } + } + + checkKlibDump(runner.build(), "/examples/classes/TopLevelDeclarations.klib.all.dump") + } } diff --git a/src/functionalTest/resources/examples/gradle/base/withNativePluginAndCrossCompilation.gradle.kts b/src/functionalTest/resources/examples/gradle/base/withNativePluginAndCrossCompilation.gradle.kts new file mode 100644 index 00000000..5ad94c2a --- /dev/null +++ b/src/functionalTest/resources/examples/gradle/base/withNativePluginAndCrossCompilation.gradle.kts @@ -0,0 +1,38 @@ +/* + * Copyright 2016-2025 JetBrains s.r.o. + * Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file. + */ + +plugins { + kotlin("multiplatform") version "2.1.0" + id("org.jetbrains.kotlinx.binary-compatibility-validator") +} + +repositories { + mavenCentral() +} + +kotlin { + linuxX64() + linuxArm64() + mingwX64() + androidNativeArm32() + androidNativeArm64() + androidNativeX64() + androidNativeX86() + + sourceSets { + val commonMain by getting + val commonTest by getting { + dependencies { + implementation(kotlin("stdlib")) + implementation(kotlin("test-common")) + implementation(kotlin("test-annotations-common")) + } + } + } +} + +apiValidation { + klib.enabled = true +} diff --git a/src/main/kotlin/BinaryCompatibilityValidatorPlugin.kt b/src/main/kotlin/BinaryCompatibilityValidatorPlugin.kt index 7cd2af31..c117b3ea 100644 --- a/src/main/kotlin/BinaryCompatibilityValidatorPlugin.kt +++ b/src/main/kotlin/BinaryCompatibilityValidatorPlugin.kt @@ -17,7 +17,6 @@ import org.jetbrains.kotlin.gradle.plugin.* import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget import org.jetbrains.kotlin.konan.target.HostManager import org.jetbrains.kotlin.library.abi.ExperimentalLibraryAbiReader -import org.jetbrains.kotlin.library.abi.LibraryAbiReader import java.io.* import java.util.* @@ -320,6 +319,7 @@ private inline fun Project.task( private const val BANNED_TARGETS_PROPERTY_NAME = "binary.compatibility.validator.klib.targets.disabled.for.testing" private const val KLIB_DUMPS_DIRECTORY = "klib" private const val KLIB_INFERRED_DUMPS_DIRECTORY = "klib-all" +private const val ENABLE_CROSS_COMPILATION_PROPERTY_NAME = "kotlin.native.enableKlibsCrossCompilation" /** * KLib ABI dump validation and dump extraction consists of multiple steps that extracts and transforms dumps for klibs. @@ -464,7 +464,7 @@ private class KlibValidationPipelineBuilder( } fun Project.bannedTargets(): Set { - val prop = project.properties[BANNED_TARGETS_PROPERTY_NAME] as String? + val prop = project.findProperty(BANNED_TARGETS_PROPERTY_NAME)?.toString() prop ?: return emptySet() return prop.split(",").map { it.trim() }.toSet().also { if (it.isNotEmpty()) { @@ -544,10 +544,14 @@ private class KlibValidationPipelineBuilder( private fun Project.targetIsSupported(target: KotlinTarget): Boolean { if (bannedTargets().contains(target.targetName)) return false - return when (target) { - is KotlinNativeTarget -> HostManager().isEnabled(target.konanTarget) - else -> true + if (target !is KotlinNativeTarget || HostManager().isEnabled(target.konanTarget)) { + return true } + + // Starting from Kotlin 2.1.0, cross compilation could be enabled via property + if (!isKgpVersionAtLeast2_1(getKotlinPluginVersion())) return false + + return (project.findProperty(ENABLE_CROSS_COMPILATION_PROPERTY_NAME) as String?).toBoolean() } // Compilable targets not supported by the host compiler @@ -770,3 +774,11 @@ private var Configuration.isCanBeDeclaredCompat: Boolean isCanBeDeclared = value } } + +private fun isKgpVersionAtLeast2_1(kgpVersion: String): Boolean { + val parts = kgpVersion.split('.') + if (parts.size < 2) return false + val major = parts[0].toIntOrNull() ?: return false + val minor = parts[1].toIntOrNull() ?: return false + return major > 2 || (major == 2 && minor >= 1) +} diff --git a/src/main/kotlin/api/AsmMetadataLoading.kt b/src/main/kotlin/api/AsmMetadataLoading.kt index b7a4d303..f106c1de 100644 --- a/src/main/kotlin/api/AsmMetadataLoading.kt +++ b/src/main/kotlin/api/AsmMetadataLoading.kt @@ -39,7 +39,7 @@ internal fun ClassNode.isEffectivelyPublic(classVisibility: ClassVisibility?) = private val ClassNode.innerClassNode: InnerClassNode? get() = innerClasses.singleOrNull { it.name == name } -private fun ClassNode.isLocal() = outerMethod != null +private fun ClassNode.isLocal() = outerClass != null // using outerMethod is unreliable, because even for local classes outerMethod can sometimes be null private fun ClassNode.isInner() = innerClassNode != null private fun ClassNode.isWhenMappings() = isSynthetic(access) && name.endsWith("\$WhenMappings") private fun ClassNode.isSyntheticAnnotationClass() = isSynthetic(access) && name.contains("\$annotationImpl\$") diff --git a/src/test/kotlin/tests/PrecompiledCasesTest.kt b/src/test/kotlin/tests/PrecompiledCasesTest.kt new file mode 100644 index 00000000..c1cec4f7 --- /dev/null +++ b/src/test/kotlin/tests/PrecompiledCasesTest.kt @@ -0,0 +1,40 @@ +/* + * Copyright 2016-2025 JetBrains s.r.o. + * Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file. + */ + +package kotlinx.validation.api.tests + +import kotlinx.validation.api.* +import org.junit.* +import org.junit.rules.TestName +import java.io.File +import java.nio.file.Path +import kotlin.io.path.ExperimentalPathApi +import kotlin.io.path.walk + +class PrecompiledCasesTest { + + companion object { + val baseOutputPath = File("src/test/resources/precompiled") + } + + @Rule + @JvmField + val testName = TestName() + + @Test fun parcelable() { snapshotAPIAndCompare(testName.methodName) } + + @OptIn(ExperimentalPathApi::class) + private fun snapshotAPIAndCompare(testClassRelativePath: String, nonPublicMarkers: Set = emptySet()) { + val testClasses = baseOutputPath.toPath().walk().map(Path::toFile).toList() + check(testClasses.isNotEmpty()) { "No class files are found in path: $baseOutputPath" } + + val testClassStreams = testClasses.asSequence().filter { it.name.endsWith(".class") }.map { it.inputStream() } + val classes = testClassStreams.loadApiFromJvmClasses() + val additionalPackages = classes.extractAnnotatedPackages(nonPublicMarkers) + val api = classes.filterOutNonPublic(nonPublicPackages = additionalPackages).filterOutAnnotated(nonPublicMarkers) + val target = baseOutputPath.resolve(testClassRelativePath).resolve(testName.methodName + ".txt") + api.dumpAndCompareWith(target) + } +} diff --git a/src/test/resources/precompiled/parcelable/Country$Creator.class b/src/test/resources/precompiled/parcelable/Country$Creator.class new file mode 100644 index 00000000..a2adb565 Binary files /dev/null and b/src/test/resources/precompiled/parcelable/Country$Creator.class differ diff --git a/src/test/resources/precompiled/parcelable/parcelable.txt b/src/test/resources/precompiled/parcelable/parcelable.txt new file mode 100644 index 00000000..e69de29b