Skip to content

Commit

Permalink
Merge branch 'develop'
Browse files Browse the repository at this point in the history
  • Loading branch information
fzhinkin committed Mar 20, 2024
2 parents 7124ff9 + 10f8e7e commit 218728f
Show file tree
Hide file tree
Showing 113 changed files with 6,819 additions and 173 deletions.
20 changes: 20 additions & 0 deletions Module.md
@@ -0,0 +1,20 @@
# Module binary-compatibility-validator

Binary compatibility validator allows dumping Kotlin library ABI (both for JVM libraries and KLibs)
that is public in the sense of Kotlin visibilities and ensures that the public ABI
wasn't changed in a way that makes this change binary incompatible.

# Package kotlinx.validation

Provides common declarations, Gradle plugin tasks and extensions.

# Package kotlinx.validation.api

Provides an API for dumping Kotlin Java libraries public ABI.

# Package kotlinx.validation.api.klib

Provides an API for dumping Kotlin libraries (KLibs) public ABI and managing resulting dumps.

**This package is experimental, both API and behaviour may change in the future. There are also no guarantees on preserving the behavior of the API until its
stabilization.**
61 changes: 58 additions & 3 deletions README.md
Expand Up @@ -2,6 +2,7 @@
[![JetBrains official project](https://jb.gg/badges/official.svg)](https://confluence.jetbrains.com/display/ALL/JetBrains+on+GitHub)
[![Maven Central](https://img.shields.io/maven-central/v/org.jetbrains.kotlinx/binary-compatibility-validator)](https://central.sonatype.com/search?q=org.jetbrains.kotlinx.binary-compatibility-validator)
[![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/)

# Binary compatibility validator

Expand All @@ -14,6 +15,7 @@ The tool allows dumping binary API of a JVM part of a Kotlin library that is pub
* [Tasks](#tasks)
* [Optional parameters](#optional-parameters)
* [Workflow](#workflow)
* [Experimental KLib ABI validation support](#experimental-klib-abi-validation-support)
* [What constitutes the public API](#what-constitutes-the-public-api)
* [Classes](#classes)
* [Members](#members)
Expand All @@ -33,15 +35,15 @@ 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.14.0"
id("org.jetbrains.kotlinx.binary-compatibility-validator") version "0.15.0-Beta.1"
}
```

- in `build.gradle`

```groovy
plugins {
id 'org.jetbrains.kotlinx.binary-compatibility-validator' version '0.14.0'
id 'org.jetbrains.kotlinx.binary-compatibility-validator' version '0.15.0-Beta.1'
}
```

Expand Down Expand Up @@ -146,7 +148,7 @@ apiValidation {
By default, binary compatibility validator analyzes project output class files from `build/classes` directory when building an API dump.
If you pack these classes into an output jar not in a regular way, for example, by excluding certain classes, applying `shadow` plugin, and so on,
the API dump built from the original class files may no longer reflect the resulting jar contents accurately.
In that case, it makes sense to use the resulting jar as an input of the `apuBuild` task:
In that case, it makes sense to use the resulting jar as an input of the `apiBuild` task:

Kotlin
```kotlin
Expand Down Expand Up @@ -180,6 +182,59 @@ When starting to validate your library public API, we recommend the following wo
the resulting diff in `.api` file should be verified: only signatures you expected to change should be changed.
* Commit the resulting `.api` diff along with code changes.

### Experimental KLib ABI validation support

The KLib validation support is experimental and is a subject to change (applies to both an API and the ABI dump format).
A project has to use Kotlin 1.9.20 or newer to use this feature.

To validate public ABI of a Kotlin library (KLib) corresponding option should be enabled explicitly:
```kotlin
apiValidation {
@OptIn(kotlinx.validation.ExperimentalBCVApi::class)
klib {
enabled = true
}
}
```

When enabled, KLib support adds additional dependencies to existing `apiDump` and `apiCheck` tasks.
Generate KLib ABI dumps are places alongside JVM dumps (in `api` subfolder, by default)
in files named `<project name>.klib.api`.
The dump file combines all dumps generated for individual targets with declarations specific to some targets being
annotated with corresponding target names.
During the validation phase, that file is compared to the dump extracted from the latest version of the library,
and any differences between these two files are reported as errors.

Currently, all options described in [Optional parameters](#optional-parameters) section are supported for klibs too.
The only caveat here is that all class names should be specified in the JVM-format,
like `package.name.ClassName$SubclassName`.

Please refer to a [design document](docs/design/KLibSupport.md) for details on the format and rationale behind the
current implementation.

#### KLib ABI dump generation and validation on Linux and Windows hosts

Currently, compilation to Apple-specific targets (like `iosArm64` or `watchosX86`) supported only on Apple hosts.
To ease the development on Windows and Linux hosts, binary compatibility validator does not validate ABI for targets
not supported on the current host, even if `.klib.api` file contains declarations for these targets.

This behavior could be altered to force an error when klibs for some targets could not be compiled:
```kotlin
apiValidation {
@OptIn(kotlinx.validation.ExperimentalBCVApi::class)
klib {
enabled = true
// treat a target being unsupported on a host as an error
strictValidation = true
}
}
```

When it comes to dump generation (`apiDump` task) on non-Apple hosts, binary compatibility validator attempts
to infer an ABI from dumps generated for supported targets and an old dump from project's `api` folder (if any).
Inferred dump may not match an actual dump,
and it is recommended to update a dump on hosts supporting all required targets, if possible.

# What constitutes the public API

### Classes
Expand Down
132 changes: 120 additions & 12 deletions api/binary-compatibility-validator.api
Expand Up @@ -5,11 +5,13 @@ public class kotlinx/validation/ApiValidationExtension {
public final fun getIgnoredClasses ()Ljava/util/Set;
public final fun getIgnoredPackages ()Ljava/util/Set;
public final fun getIgnoredProjects ()Ljava/util/Set;
public final fun getKlib ()Lkotlinx/validation/KlibValidationSettings;
public final fun getNonPublicMarkers ()Ljava/util/Set;
public final fun getPublicClasses ()Ljava/util/Set;
public final fun getPublicMarkers ()Ljava/util/Set;
public final fun getPublicPackages ()Ljava/util/Set;
public final fun getValidationDisabled ()Z
public final fun klib (Lkotlin/jvm/functions/Function1;)V
public final fun setAdditionalSourceSets (Ljava/util/Set;)V
public final fun setApiDumpDirectory (Ljava/lang/String;)V
public final fun setIgnoredClasses (Ljava/util/Set;)V
Expand All @@ -28,32 +30,59 @@ public final class kotlinx/validation/BinaryCompatibilityValidatorPlugin : org/g
public fun apply (Lorg/gradle/api/Project;)V
}

public abstract class kotlinx/validation/BuildTaskBase : org/gradle/api/DefaultTask {
public field outputApiFile Ljava/io/File;
public fun <init> ()V
public final fun getIgnoredClasses ()Ljava/util/Set;
public final fun getIgnoredPackages ()Ljava/util/Set;
public final fun getNonPublicMarkers ()Ljava/util/Set;
public final fun getOutputApiFile ()Ljava/io/File;
public final fun getPublicClasses ()Ljava/util/Set;
public final fun getPublicMarkers ()Ljava/util/Set;
public final fun getPublicPackages ()Ljava/util/Set;
public final fun setIgnoredClasses (Ljava/util/Set;)V
public final fun setIgnoredPackages (Ljava/util/Set;)V
public final fun setNonPublicMarkers (Ljava/util/Set;)V
public final fun setOutputApiFile (Ljava/io/File;)V
public final fun setPublicClasses (Ljava/util/Set;)V
public final fun setPublicMarkers (Ljava/util/Set;)V
public final fun setPublicPackages (Ljava/util/Set;)V
}

public abstract interface annotation class kotlinx/validation/ExperimentalBCVApi : java/lang/annotation/Annotation {
}

public abstract interface annotation class kotlinx/validation/ExternalApi : java/lang/annotation/Annotation {
}

public class kotlinx/validation/KotlinApiBuildTask : org/gradle/api/DefaultTask {
public class kotlinx/validation/KlibValidationSettings {
public fun <init> ()V
public final fun getEnabled ()Z
public final fun getSignatureVersion ()Lkotlinx/validation/api/klib/KlibSignatureVersion;
public final fun getStrictValidation ()Z
public final fun setEnabled (Z)V
public final fun setSignatureVersion (Lkotlinx/validation/api/klib/KlibSignatureVersion;)V
public final fun setStrictValidation (Z)V
}

public class kotlinx/validation/KotlinApiBuildTask : kotlinx/validation/BuildTaskBase {
public field inputDependencies Lorg/gradle/api/file/FileCollection;
public field outputApiDir Ljava/io/File;
public fun <init> ()V
public final fun getInputClassesDirs ()Lorg/gradle/api/file/FileCollection;
public final fun getInputDependencies ()Lorg/gradle/api/file/FileCollection;
public final fun getInputJar ()Lorg/gradle/api/file/RegularFileProperty;
public final fun getOutputApiDir ()Ljava/io/File;
public final fun setInputClassesDirs (Lorg/gradle/api/file/FileCollection;)V
public final fun setInputDependencies (Lorg/gradle/api/file/FileCollection;)V
public final fun setOutputApiDir (Ljava/io/File;)V
}

public class kotlinx/validation/KotlinApiCompareTask : org/gradle/api/DefaultTask {
public field apiBuildDir Ljava/io/File;
public field generatedApiFile Ljava/io/File;
public field projectApiFile Ljava/io/File;
public fun <init> (Lorg/gradle/api/model/ObjectFactory;)V
public final fun getApiBuildDir ()Ljava/io/File;
public final fun getDummyOutputFile ()Ljava/io/File;
public final fun getNonExistingProjectApiDir ()Ljava/lang/String;
public final fun getProjectApiDir ()Ljava/io/File;
public final fun setApiBuildDir (Ljava/io/File;)V
public final fun setNonExistingProjectApiDir (Ljava/lang/String;)V
public final fun setProjectApiDir (Ljava/io/File;)V
public final fun getGeneratedApiFile ()Ljava/io/File;
public final fun getProjectApiFile ()Ljava/io/File;
public final fun setGeneratedApiFile (Ljava/io/File;)V
public final fun setProjectApiFile (Ljava/io/File;)V
}

public final class kotlinx/validation/api/ClassBinarySignature {
Expand All @@ -79,3 +108,82 @@ public final class kotlinx/validation/api/KotlinSignaturesLoadingKt {
public static synthetic fun retainExplicitlyIncludedIfDeclared$default (Ljava/util/List;Ljava/util/Collection;Ljava/util/Collection;Ljava/util/Collection;ILjava/lang/Object;)Ljava/util/List;
}

public final class kotlinx/validation/api/klib/KlibDump {
public static final field Companion Lkotlinx/validation/api/klib/KlibDump$Companion;
public fun <init> ()V
public final fun copy ()Lkotlinx/validation/api/klib/KlibDump;
public final fun getTargets ()Ljava/util/Set;
public final fun merge (Ljava/io/File;Ljava/lang/String;)V
public final fun merge (Lkotlinx/validation/api/klib/KlibDump;)V
public static synthetic fun merge$default (Lkotlinx/validation/api/klib/KlibDump;Ljava/io/File;Ljava/lang/String;ILjava/lang/Object;)V
public final fun remove (Ljava/lang/Iterable;)V
public final fun retain (Ljava/lang/Iterable;)V
public final fun saveTo (Ljava/lang/Appendable;)V
}

public final class kotlinx/validation/api/klib/KlibDump$Companion {
public final fun from (Ljava/io/File;Ljava/lang/String;)Lkotlinx/validation/api/klib/KlibDump;
public static synthetic fun from$default (Lkotlinx/validation/api/klib/KlibDump$Companion;Ljava/io/File;Ljava/lang/String;ILjava/lang/Object;)Lkotlinx/validation/api/klib/KlibDump;
public final fun fromKlib (Ljava/io/File;Ljava/lang/String;Lkotlinx/validation/api/klib/KlibDumpFilters;)Lkotlinx/validation/api/klib/KlibDump;
public static synthetic fun fromKlib$default (Lkotlinx/validation/api/klib/KlibDump$Companion;Ljava/io/File;Ljava/lang/String;Lkotlinx/validation/api/klib/KlibDumpFilters;ILjava/lang/Object;)Lkotlinx/validation/api/klib/KlibDump;
}

public final class kotlinx/validation/api/klib/KlibDumpFilters {
public static final field Companion Lkotlinx/validation/api/klib/KlibDumpFilters$Companion;
public final fun getIgnoredClasses ()Ljava/util/Set;
public final fun getIgnoredPackages ()Ljava/util/Set;
public final fun getNonPublicMarkers ()Ljava/util/Set;
public final fun getSignatureVersion ()Lkotlinx/validation/api/klib/KlibSignatureVersion;
}

public final class kotlinx/validation/api/klib/KlibDumpFilters$Builder {
public fun <init> ()V
public final fun build ()Lkotlinx/validation/api/klib/KlibDumpFilters;
public final fun getIgnoredClasses ()Ljava/util/Set;
public final fun getIgnoredPackages ()Ljava/util/Set;
public final fun getNonPublicMarkers ()Ljava/util/Set;
public final fun getSignatureVersion ()Lkotlinx/validation/api/klib/KlibSignatureVersion;
public final fun setSignatureVersion (Lkotlinx/validation/api/klib/KlibSignatureVersion;)V
}

public final class kotlinx/validation/api/klib/KlibDumpFilters$Companion {
public final fun getDEFAULT ()Lkotlinx/validation/api/klib/KlibDumpFilters;
}

public final class kotlinx/validation/api/klib/KlibDumpFiltersKt {
public static final fun KLibDumpFilters (Lkotlin/jvm/functions/Function1;)Lkotlinx/validation/api/klib/KlibDumpFilters;
}

public final class kotlinx/validation/api/klib/KlibDumpKt {
public static final fun inferAbi (Lkotlinx/validation/api/klib/KlibTarget;Ljava/lang/Iterable;Lkotlinx/validation/api/klib/KlibDump;)Lkotlinx/validation/api/klib/KlibDump;
public static synthetic fun inferAbi$default (Lkotlinx/validation/api/klib/KlibTarget;Ljava/lang/Iterable;Lkotlinx/validation/api/klib/KlibDump;ILjava/lang/Object;)Lkotlinx/validation/api/klib/KlibDump;
public static final fun mergeFromKlib (Lkotlinx/validation/api/klib/KlibDump;Ljava/io/File;Ljava/lang/String;Lkotlinx/validation/api/klib/KlibDumpFilters;)V
public static synthetic fun mergeFromKlib$default (Lkotlinx/validation/api/klib/KlibDump;Ljava/io/File;Ljava/lang/String;Lkotlinx/validation/api/klib/KlibDumpFilters;ILjava/lang/Object;)V
public static final fun saveTo (Lkotlinx/validation/api/klib/KlibDump;Ljava/io/File;)V
}

public final class kotlinx/validation/api/klib/KlibSignatureVersion {
public static final field Companion Lkotlinx/validation/api/klib/KlibSignatureVersion$Companion;
public fun equals (Ljava/lang/Object;)Z
public fun hashCode ()I
public fun toString ()Ljava/lang/String;
}

public final class kotlinx/validation/api/klib/KlibSignatureVersion$Companion {
public final fun getLATEST ()Lkotlinx/validation/api/klib/KlibSignatureVersion;
public final fun of (I)Lkotlinx/validation/api/klib/KlibSignatureVersion;
}

public final class kotlinx/validation/api/klib/KlibTarget {
public static final field Companion Lkotlinx/validation/api/klib/KlibTarget$Companion;
public fun equals (Ljava/lang/Object;)Z
public final fun getConfigurableName ()Ljava/lang/String;
public final fun getTargetName ()Ljava/lang/String;
public fun hashCode ()I
public fun toString ()Ljava/lang/String;
}

public final class kotlinx/validation/api/klib/KlibTarget$Companion {
public final fun parse (Ljava/lang/String;)Lkotlinx/validation/api/klib/KlibTarget;
}

50 changes: 48 additions & 2 deletions build.gradle.kts
@@ -1,9 +1,13 @@
import kotlinx.kover.gradle.plugin.dsl.MetricType
import kotlinx.validation.build.mavenCentralMetadata
import kotlinx.validation.build.mavenRepositoryPublishing
import kotlinx.validation.build.signPublicationIfKeyPresent
import org.gradle.api.attributes.TestSuiteType.FUNCTIONAL_TEST
import org.jetbrains.dokka.gradle.DokkaTask
import org.jetbrains.dokka.gradle.DokkaTaskPartial
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
import org.jetbrains.kotlin.gradle.dsl.KotlinVersion
import java.net.URL

plugins {
kotlin("jvm")
Expand All @@ -14,6 +18,8 @@ plugins {
`maven-publish`
`jvm-test-suite`
id("org.jetbrains.kotlinx.binary-compatibility-validator")
alias(libs.plugins.kover)
alias(libs.plugins.dokka)
}

group = "org.jetbrains.kotlinx"
Expand Down Expand Up @@ -60,6 +66,7 @@ val createClasspathManifest = tasks.register("createClasspathManifest") {
dependencies {
implementation(gradleApi())
implementation(libs.kotlinx.metadata)
compileOnly(libs.kotlin.compiler.embeddable)
implementation(libs.ow2.asm)
implementation(libs.ow2.asmTree)
implementation(libs.javaDiffUtils)
Expand All @@ -76,7 +83,6 @@ dependencies {

tasks.compileKotlin {
compilerOptions {
freeCompilerArgs.add("-Xexplicit-api=strict")
allWarningsAsErrors.set(true)
@Suppress("DEPRECATION") // Compatibility with Gradle 7 requires Kotlin 1.4
languageVersion.set(KotlinVersion.KOTLIN_1_4)
Expand All @@ -85,7 +91,9 @@ tasks.compileKotlin {
// Suppressing "w: Language version 1.4 is deprecated and its support will be removed" message
// because LV=1.4 in practice is mandatory as it is a default language version in Gradle 7.0+ for users' kts scripts.
freeCompilerArgs.addAll(
"-Xsuppress-version-warnings"
"-Xexplicit-api=strict",
"-Xsuppress-version-warnings",
"-Xopt-in=kotlin.RequiresOptIn"
)
}
}
Expand All @@ -107,6 +115,7 @@ tasks.compileTestKotlin {
tasks.withType<Test>().configureEach {
systemProperty("overwrite.output", System.getProperty("overwrite.output", "false"))
systemProperty("testCasesClassesDirs", sourceSets.test.get().output.classesDirs.asPath)
systemProperty("kover.enabled", project.findProperty("kover.enabled")?.toString().toBoolean())
jvmArgs("-ea")
}

Expand Down Expand Up @@ -159,6 +168,7 @@ testing {
implementation(project())
implementation(libs.assertJ.core)
implementation(libs.kotlin.test)
implementation(libs.kotlin.compiler.embeddable)
}
}

Expand Down Expand Up @@ -195,3 +205,39 @@ testing {
tasks.withType<Sign>().configureEach {
onlyIf("only sign if signatory is present") { signatory?.keyId != null }
}

kover {
koverReport {
filters {
excludes {
packages("kotlinx.validation.test")
}
}
verify {
rule {
minBound(80, MetricType.BRANCH)
minBound(90, MetricType.LINE)
}
}
}
// Unfortunately, we can't test both configuration cache use and the test coverage
// simultaneously, so the coverage collection should be enabled explicitly (and that
// will disable configuration cache).
if (!project.findProperty("kover.enabled")?.toString().toBoolean()) {
disable()
}
}


tasks.withType<DokkaTask>().configureEach {
dokkaSourceSets.configureEach {
includes.from("Module.md")

sourceLink {
localDirectory.set(rootDir)
remoteUrl.set(URL("https://github.com/Kotlin/binary-compatibility-validator/tree/master"))
remoteLineSuffix.set("#L")
}
samples.from("src/test/kotlin/samples/KlibDumpSamples.kt")
}
}

0 comments on commit 218728f

Please sign in to comment.