diff --git a/Module.md b/Module.md new file mode 100644 index 00000000..d234220c --- /dev/null +++ b/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.** diff --git a/README.md b/README.md index 4a75f8d6..92f803c5 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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) @@ -33,7 +35,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.14.0" + id("org.jetbrains.kotlinx.binary-compatibility-validator") version "0.15.0-Beta.1" } ``` @@ -41,7 +43,7 @@ plugins { ```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' } ``` @@ -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 @@ -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 `.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 diff --git a/api/binary-compatibility-validator.api b/api/binary-compatibility-validator.api index f226963e..07d3bc3d 100644 --- a/api/binary-compatibility-validator.api +++ b/api/binary-compatibility-validator.api @@ -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 @@ -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 ()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 ()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 ()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 (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 { @@ -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 ()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 ()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; +} + diff --git a/build.gradle.kts b/build.gradle.kts index c70df01c..087f27e0 100644 --- a/build.gradle.kts +++ b/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") @@ -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" @@ -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) @@ -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) @@ -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" ) } } @@ -107,6 +115,7 @@ tasks.compileTestKotlin { tasks.withType().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") } @@ -159,6 +168,7 @@ testing { implementation(project()) implementation(libs.assertJ.core) implementation(libs.kotlin.test) + implementation(libs.kotlin.compiler.embeddable) } } @@ -195,3 +205,39 @@ testing { tasks.withType().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().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") + } +} diff --git a/docs/design/KLibSupport.md b/docs/design/KLibSupport.md new file mode 100644 index 00000000..1eac44ff --- /dev/null +++ b/docs/design/KLibSupport.md @@ -0,0 +1,253 @@ +The document describes assumptions that led to the current KLib ABI validation implementation. + +### Motivation and assumptions + +Just like JVM class files, Kotlin/Native libraries (a.k.a. KLibs) comes with a binary compatibility guarantees +allowing library developers evolve the library without a fear to break projects compiled against previous versions of +the library (but unlike the JVM, these guarantees are not yet finalized at the time this document was written). + +For the JVM, the Binary compatibility validator allows to check if some binary incompatible changes were made and +review what actually changed. For KLibs, there is no such a tool, and it seems reasonable to extend the BCV with KLib +validation support. + +There are several assumptions based on the experience of supporting +various multiplatform libraries that drive the KLib validation design: +* Multiplatform libraries usually have both JVM and native targets, so instead of introducing some different +unrelated tool/plugin it seems reasonable to extend the existing plugin and provide an experience similar to what +users have now for JVM libraries so that users can verify compatibility for both kinds of targets. +* BCV not only provides a way to verify public ABI changes, but it also allows to check how the public API surface +changed: developers could simply look at the dump file's history in SVC or review the change in a code-review system; +* Projects may have multiple JVM targets, but usually there is only a single target with a single dump file; +At the same time, multiplatform projects have a dozen of different native targets (like `linuxX64`, `iosArm64`, +`macosArm64`, to name a few), so there should be a way to manage dumps for all the targets. +* Usually, even if a project has multiple native targets, the public ABI exposed by corresponding klibs is either +the same or contains only a small number of differences. +* Not all targets could be compiled on every host: currently, the cross-compilation have some limitations (namely, +it's impossible to build Apple-targets on non macOs-host), and it's unlikely that someday it would be possible to build +Apple-specific sources (i.e., not just common sources that need to be compiled to some iosArm64-klib) +on non Apple-hosts. KLib validation requires a klib, so there should be a way to facilitate klib ABI dumping and +validation on something different from macOs (consider a multiplatform project where a developer adds a class +or a function to the common source set, it seems reasonable to expect that it could be done on any host). +* KLibs are emitted not only for native targets, but also for JS and Wasm targets. +* There are scenarios when a klib has to be compiled on a corresponding host platform (i.e., mingw target on Windows +and linux target on Linux), and also there are scenarios when using a Gradle plugin is not an option, +so there should be a way to use BCV for KLibs even in such scenarios. + +### Merged KLib dumps + +Assuming that the library's public ABI does not differ significantly between targets, it seems reasonable to merge +all the dumps into a single file where each declaration is annotated with the targets it belongs to. That will minimize +the number of new files to store in a project and will significantly simplify review of the changes. + +The KLib dump is a text format ([see examples in the Kotlin repo](https://github.com/JetBrains/kotlin/blob/master/compiler/util-klib-abi/ReadMe.md)). +For the merged dumps, we can simply extend it by adding special comments with a list of targets, +like `// Targets: [iosArm64, linuxX64, wasm]`. + +Such targets lists could be long and hard to read, so to simplify a process of reviewing changes in a dump we can +replace explicit target names with group aliases corresponding to groups of targets from the +[default hierarchy template](https://kotlinlang.org/docs/multiplatform-hierarchy.html#see-the-full-hierarchy-template). +Then, a long list of all native targets will become `[native]` and, for example, all Android-native targets will +become simply `[androidNative]`. +Of course, the merged dump file should include the mapping between an alias and actual target names, it could be placed +in a file's header. + +Here's a brief example of such a merged dump file: +``` +// Klib ABI Dump +// Targets: [androidNativeArm32, androidNativeArm64, androidNativeX64, androidNativeX86, linuxArm64, linuxX64, mingwX64] +// Alias: linux => [linuxArm64, linuxX64] +// Rendering settings: +// - Signature version: 2 +// - Show manifest properties: false +// - Show declarations: true + +// Library unique name: +final class org.different.pack/BuildConfig { // org.different.pack/BuildConfig|null[0] + constructor () // org.different.pack/BuildConfig.|(){}[0] + final fun f1(): kotlin/Int // org.different.pack/BuildConfig.f1|f1(){}[0] + // Targets: [mingwX64] + final val p1 // org.different.pack/BuildConfig.p1|{}p1[0] + final fun (): kotlin/Int // org.different.pack/BuildConfig.p1.|(){}[0] +} +// Targets: [linux] +final fun (org.different.pack/BuildConfig).org.different.pack/linuxSpecific(): kotlin/Int // org.different.pack/linuxSpecific|linuxSpecific@org.different.pack.BuildConfig(){}[0] +``` + +The first line is just a header to check during the parsing. + +The next line (`// Targets: ...`) contains list of all targets for which dumps were merged. +It seems to be excessive to write all the targets each declaration belongs to as the majority +of declarations have the same targets as their parent declarations, and in the case of top level +declarations, until it is mentioned explicitly, the list of targets is the same as the file's target list. + +So, the class `BuildConfig` have the same targets as the whole file, but `linuxSpecific`-function +is presented only on `linux`-targets (`// Targets: [linux]`). + +The next line declares a target alias (`// Alias: => []`). There are only one alias (`linux`). +Aliases are generated for target groups corresponding to groups from a default hierarchy, that could appear in a file +and consist of more than a single target. +For instance, there's no `androidNative` group alias as there are no declarations having only +corresponding android-native targets and there is no group for `mingwX64` +as there would no other targets in such a group. +After that, a regular KLib ABI dump header continues, with an exception to some declarations being +annotated with `// Targets`. + +So, the dump could be interpreted as follows: +- the dump was merged from individual KLib ABI dumps generated for the following targets: +`androidNativeArm32`, `androidNativeArm64`, `androidNativeX64`, `androidNativeX86`, `linuxArm64`, `linuxX64`, +`mingwX64`; +- the class `BuildConfig`, its constructor and `f1`-functions are all exposed by klibs for all targets, +but its property `p1` (and the corresponding getter) is declared only for the `mingwX64`-target; +- an extension function `BuildConfig.linuxSpecific` is declared only for `linuxX64` and `linuxArm64` targets. + +### Working with dumps on hosts that does not support cross-compilation for all the targets + +If a host does not support cross-compilation for all the targets (it's a Linux or Windows host), then +there's no way to both dump and validate klibs for all targets configured for a project. + +When it comes to validation, the simplest approach seems to be ignoring unsupported targets (and printing +a warning about it) and validation the ABI only for targets supported by the host compiler. +To do that, corresponding klibs should be built first, and then their ABI should be dumped and merged. +After that, the merged dump stored in the repository should be filtered so that only the declarations for supported +targets are left. Finally, a newly generated dump could be compared with the filtered "golden" dump the same way +dumps are compared for the JVM. + +The things are a bit more complicated when it comes to updating the "golden" dump as if only dumps for supported targets +are merged, then the resulting dump file will cause validation failure on the host where all targets +are available (the dump won't contain declaration for Apple-targets, it won't even mention these targets, so when +the ABI validation takes a place on macOs-host, it'll fail). + +It seems like there are two ways to handle such a scenario: +- when updating a dump, assume that the ABI for unsupported targets remained the same and update only the ABI +for supported targets; +- try to guess (or infer) the ABI for unsupported targets by looking at the ABI dumped for supported targets. + +Both approaches have some shortcomings: +- with the first one, as long as the ABI changes, an updated dump will always be incorrect as it won't reflect +changes for unsupported targets; +- with the second approach, a "guessed" dump may be incorrect (of course, it depends on how we "guess"). + +By guessing or inferring a dump for unsupported targets, the following procedure is assumed: +- walk up the target hierarchy starting from the unsupported target until a group +consisting of at least one supported target is found; +- assume that declarations shared by all targets in the group found during the previous step are common +to all group members (including unsupported targets); +- generate a dump for unsupported targets by combining this newly generated "common" ABI with all declarations +specific to the unsupported target extracted from an old dump. + +The higher the hierarchy we go, the larger the group of targets should be, so the ABI shared by all these targets +should be "more common" (lowering changes of including some target-specific declarations). On the other hand, +if unsupported targets have some target-specific declarations, then it's likely that the targets closer to them in +the hierarchy are also having these declarations. + +Here's an example of walking up the target hierarchy for `iosArm64` target on a Linux host for a project +having only Apple-targets and WasmJs-target: +- `iosArm64` is unsupported, let's try any `ios` target; +- all `ios` targets are unavailable, let's try any `apple` target; +- all `apple` targets are unavailable, let's try any `native` target; +- all `native` targets are unavailable, let's try any target; +- `wasmJs` target is available, lets use its dump. + +The table below summarizes different scenarios and how the two different approaches handle them. + + +| Scenario | Ignore unsupported target | | Guess ABI for unsupported targets | | +|-----------------------------------------------------------------------------------|------------------------------------------------------------------------------------|------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------| +| | **How it’ll be updated?** | **Valid?** | **How it’ll be updated?** | **Valid?** | +| **KMP-project without A target-specific ABI** | | | | | +| Change common declaration | An Apple-ABI will remain unchanged, but for all other targets the ABI will differ. | No ❌ | The declaration will be declared in all supported targets, we’ll consider it as common and will also add to unsupported (apple) targets without any changes, old apple-ABI will be replaced with the updated one | Yes 👌 | +| Move a declaration from the common sources to every single target-specific source set | An Apple-ABI will remain unchanged, but for all other targets the ABI will differ. | No ❌ | The declaration will be declared in all supported targets, we’ll consider it as common and will also add to unsupported (apple) targets without any changes, old apple-ABI will be replaced with the updated one | Yes 👌 | +| **KMP-project with an Apple-specific ABI** | | | | | +| Change a common declaration | Old version of the declaration will remain in the Apple-ABI dump. |No ❌ | The declaration will be considered common as all supported targets’ dumps will have it, old apple-specific parts of the ABI will remain untouched|Yes 👌| +| Change an Apple-specific declaration | An Apple-ABI dump won’t get any updates | No ❌ | An Apple-specific declarations won’t be updated | No ❌| +| **KMP-project with a linux-spefic ABI** | | | | | +| Change a common declaration | Old version of the declaration will remain in the Apple-ABI dump. | No ❌ | The declaration will be considered common as all supported targets’ dumps will have it, old apple-specific parts of the ABI will remain untouched | Yes 👌| +| Change a Linux-specific declaration | An Apple-ABI will remain unchanged, but for all other targets the ABI will differ. | Yes 👌 | An Aplple-ABI will remain untouched | Yes 👌| +| **KMP-project with a non-Apple specific ABI (i.e. linux+mingw+js+wasm-specific)** | | | | | +| Change a declaration for all non-Apple targets | An Apple ABI will remain untouched | Yes 👌 | Tha changed ABI will be considered as common (as it came from all the supported targets) and we’ll mark it as an ABI available on Apple targets too. | No ❌| +| **Module having only Apple-specific ABI** | | | | | +| Change an ABI | No dumps available, the process will fail (or we can just avoid any updates) | No ❌ | No dumps available, the process will fail (or we can just avoid any updates) | No ❌| + + +The "guessing" approach should succeed it a broader spectrum of scenarios, so we decided to use it. + +### Target name representation + +Target name grouping and ABI dump inference described above heavily rely on target name. +Everything works fine with default names unless a user decides to rename a target: +```kotlin +kotiln { + macosArm64("macos") + linuxArm64("linux") + iosArm64() +} +``` +There are two main issues related to the renaming: +- target's name could no longer be found among targets constituting a target hierarchy (on the BCV side, not the KGP); +- new target's name may clash with existing group names or other target names. + +However, a klib's manifest contains all the information required to find an actual underlying target (but it does +not contain a custom name, though). And the same info could be included in a textual klib dump. + +To overcome the issue, it is proposed to represent target name as a tuple consisting of a "visible" +configurable target name and an underlying target name: `targetName.canfigurableName`. + +For the example mentioned above, target such fully qualified target names are: +- `macosArm64.macos` for `macosArm64("macos")`; +- `linuxArm64.linux` for `linuxArm64("linux")`; +- `iosArm64.iosArm64` for `iosArm64()`. + +By default, when the visible and canonical names are the same, only one of them could be specified, so +`iosArm64.iosArm64` could be shortened to `iosArm64`. + +Given such compound names, we can correctly perform grouping and inferring by relying only on the underlying canonical +target name, not on the visible one. + +### Programmatic API for KLib ABI validation + +To support scenarios, when Gradle plugin could not be used (like in the case of Kotlin stdlib), +or when the way users build klibs is different from "compile all the klibs here and now", the programmatic API +should be provided. The API should simply provide an entry point to all the functionality used by the plugin. + +Initially, it does make sense to provide an API allowing to implement the same functionality as Gradle Plugin does. +It also seems reasonable to provide an abstraction layer over the Kotlin compiler's API that dumps a klib so that +when needed, we could alter the dumping procedure without waiting for a Kotlin release. + +There are a few entities that should be exposed for now, namely: +- a config affecting how a klib dump will look like (`KLibDumpFilters`); +- a class representing a dump (`KlibDump`) and allowing to perform some actions on it, +namely, load, save, merge and, also infer; +- a few supplementary classes, like `KlibTarget` and `KlibSignatureVersion` to give a better and more meaningful +representation for entities that otherwise would be strings or numbers. + +There are not some many options that affect a resulting dump, so for the beginning `KLibDumpFilters` may include only +`nonPublicMarkers`, `ignoredPackages` and `ignoredClasses` to reflect what could be configured through +`kotlinx.validation.ApiValidationExtension`, and, also a `signatureVersion` (represented by a dedicated class). +The latter is only required to handle potential klib signature versions update in the future, so by default simply +the latest version should be used. + +As a side note, in the Gradle plugin, `nonPublicMarkers`, `ignoredPackages` and `ignoredClasses` should treat values as +regular Java class (or package) names, so that users who already use the BCV could enable KLib validation and everything +continues works correctly, without any config updates. So for simplicity, API should threat these values the same way. + +The main scenarios for KLib dumps we have in the plugin right now are: +- merging multiple dumps together; +- extracting declarations for a subset of targets stored in the merged dump; +- updating the merged dump with an updated dump for one or several targets; +- inferring a dump for an unsupported target. + +To cover these scenarios, the following operations are proposed for the `KlibDump`: +- `merge`, that combines several dumps together; +- `remove` and `retain` operations that either removes all the specified targets (along with declarations) from a dump, +or, contrary, retain only specified targets; +- `save`, that converts a dump back into textual form; +- `infer`, that infers the dump for unsupported targets. + +Loading a dump extracted from a klib using compiler API into `KlibDump` and then converting it back to a textual dump +will produce a file that won't be bitwise identical. To hide this inconsistency, it's proposed to always convert a klib +dump into `KlibDump` when creating a new dump (i.e. there will be no intermediate step that will extract from a klib +into a textual form using the compiler API, that then should be loaded into `KlibDump`). So yet another operation that +should `KlibDump` should have is `mergeFromKlib`, that will create a dump and merge it directly to `KlibDump` given +a klib file and an optional `KLibDumpFilters`. + +All the API will be explicitly marked as experimental, so we could freely change it in the future. diff --git a/gradle.properties b/gradle.properties index 441f00ea..96d8944d 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,3 +1,3 @@ -version=0.14.0-SNAPSHOT +version=0.15.0-SNAPSHOT kotlin.stdlib.default.dependency=false diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index c2c0ded2..9c867c6f 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,12 +1,13 @@ [versions] -kotlin = "1.8.20" +kotlin = "1.9.22" javaDiffUtils = "4.12" junit = "5.9.2" kotest = "5.5.5" kotlinx-bcv = "0.13.1" ow2Asm = "9.6" +dokka = "1.9.20" gradlePluginPublishPlugin = "1.1.0" androidGradlePlugin = "7.2.2" @@ -30,6 +31,7 @@ junit-jupiter = { module = "org.junit.jupiter:junit-jupiter", version.ref = "jun assertJ-core = { module = "org.assertj:assertj-core", version = "3.18.1" } kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" } +kotlin-compiler-embeddable = { module = "org.jetbrains.kotlin:kotlin-compiler-embeddable", version.ref = "kotlin" } ## endregion @@ -39,3 +41,8 @@ gradlePlugin-pluginPublishing = { module = "com.gradle.publish:plugin-publish-pl gradlePlugin-kotlin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" } gradlePlugin-android = { module = "com.android.tools.build:gradle", version.ref = "androidGradlePlugin" } ## endregion + +[plugins] + +kover = { id = "org.jetbrains.kotlinx.kover", version = "0.7.5" } +dokka = { id = "org.jetbrains.dokka", version.ref = "dokka" } diff --git a/src/functionalTest/kotlin/kotlinx/validation/api/BaseKotlinGradleTest.kt b/src/functionalTest/kotlin/kotlinx/validation/api/BaseKotlinGradleTest.kt index 929f08cb..d60db33d 100644 --- a/src/functionalTest/kotlin/kotlinx/validation/api/BaseKotlinGradleTest.kt +++ b/src/functionalTest/kotlin/kotlinx/validation/api/BaseKotlinGradleTest.kt @@ -17,4 +17,8 @@ public open class BaseKotlinGradleTest { internal val rootProjectDir: File get() = testProjectDir.root internal val rootProjectApiDump: File get() = rootProjectDir.resolve("$API_DIR/${rootProjectDir.name}.api") + + internal fun rootProjectAbiDump(project: String = rootProjectDir.name): File { + return rootProjectDir.resolve("$API_DIR/$project.klib.api") + } } diff --git a/src/functionalTest/kotlin/kotlinx/validation/api/TestDsl.kt b/src/functionalTest/kotlin/kotlinx/validation/api/TestDsl.kt index 6c83d163..42f3c914 100644 --- a/src/functionalTest/kotlin/kotlinx/validation/api/TestDsl.kt +++ b/src/functionalTest/kotlin/kotlinx/validation/api/TestDsl.kt @@ -12,8 +12,10 @@ import org.intellij.lang.annotations.Language public val API_DIR: String = ApiValidationExtension().apiDumpDirectory +private val koverEnabled: Boolean = System.getProperty("kover.enabled").toBoolean() + internal fun BaseKotlinGradleTest.test( - gradleVersion: String = "7.4.2", + gradleVersion: String = "8.5", injectPluginClasspath: Boolean = true, fn: BaseKotlinScope.() -> Unit ): GradleRunner { @@ -38,14 +40,19 @@ internal fun BaseKotlinGradleTest.test( .withPluginClasspath() .withArguments(baseKotlinScope.runner.arguments) .withGradleVersion(gradleVersion) + + if (koverEnabled) { + // In debug mode, tests will be running inside the same JVM. + // That will allow collection coverage info by the Kover. + runner.withDebug(true) + } + if (injectPluginClasspath) { // The hack dating back to https://docs.gradle.org/6.0/userguide/test_kit.html#sub:test-kit-classpath-injection // Currently, some tests won't work without it because some classes are missing on the classpath. runner.addPluginTestRuntimeClasspath() } return runner - // disabled because of: https://github.com/gradle/gradle/issues/6862 - // .withDebug(baseKotlinScope.runner.debug) } /** @@ -116,6 +123,23 @@ internal fun FileContainer.apiFile(projectName: String, fn: AppendableScope.() - } } +/** + * Shortcut for creating a `api//.klib.api` descriptor using [file][FileContainer.file] + */ +internal fun FileContainer.abiFile(projectName: String, target: String, fn: AppendableScope.() -> Unit) { + dir(API_DIR) { + dir(target) { + file("$projectName.klib.api", fn) + } + } +} + +internal fun FileContainer.abiFile(projectName: String, fn: AppendableScope.() -> Unit) { + dir(API_DIR) { + file("$projectName.klib.api", fn) + } +} + // not using default argument in apiFile for clarity in tests (explicit "empty" in the name) /** * Shortcut for creating an empty `api/.api` descriptor by using [file][FileContainer.file] @@ -165,7 +189,13 @@ internal class AppendableScope(val filePath: String) { } internal class Runner { - val arguments: MutableList = mutableListOf("--configuration-cache") + val arguments: MutableList = mutableListOf().apply { + if (!koverEnabled) { + // Configuration cache is incompatible with javaagents being enabled for Gradle + // See https://github.com/gradle/gradle/issues/25979 + add("--configuration-cache") + } + } } internal fun readFileList(@Language("file-reference") fileName: String): String { @@ -182,3 +212,29 @@ private fun GradleRunner.addPluginTestRuntimeClasspath() = apply { val pluginClasspath = pluginClasspath + cpResource.readLines().map { File(it) } withPluginClasspath(pluginClasspath) } + +internal val commonNativeTargets = listOf( + "linuxX64", + "linuxArm64", + "mingwX64", + "androidNativeArm32", + "androidNativeArm64", + "androidNativeX64", + "androidNativeX86" +) + +internal val appleNativeTarget = listOf( + "macosX64", + "macosArm64", + "iosX64", + "iosArm64", + "iosSimulatorArm64", + "tvosX64", + "tvosArm64", + "tvosSimulatorArm64", + "watchosArm32", + "watchosArm64", + "watchosX64", + "watchosSimulatorArm64", + "watchosDeviceArm64", +) diff --git a/src/functionalTest/kotlin/kotlinx/validation/test/IgnoredClassesTests.kt b/src/functionalTest/kotlin/kotlinx/validation/test/IgnoredClassesTests.kt index e2f1a3ef..06b3f86c 100644 --- a/src/functionalTest/kotlin/kotlinx/validation/test/IgnoredClassesTests.kt +++ b/src/functionalTest/kotlin/kotlinx/validation/test/IgnoredClassesTests.kt @@ -15,6 +15,7 @@ import kotlinx.validation.api.resolve import kotlinx.validation.api.runner import kotlinx.validation.api.test import org.assertj.core.api.Assertions +import org.junit.Ignore import org.junit.Test import kotlin.test.assertTrue diff --git a/src/functionalTest/kotlin/kotlinx/validation/test/JavaVersionsCompatibilityTest.kt b/src/functionalTest/kotlin/kotlinx/validation/test/JavaVersionsCompatibilityTest.kt index 10ef82b5..15b15aaf 100644 --- a/src/functionalTest/kotlin/kotlinx/validation/test/JavaVersionsCompatibilityTest.kt +++ b/src/functionalTest/kotlin/kotlinx/validation/test/JavaVersionsCompatibilityTest.kt @@ -9,9 +9,19 @@ import kotlinx.validation.api.* import kotlinx.validation.api.buildGradleKts import kotlinx.validation.api.resolve import kotlinx.validation.api.test +import org.gradle.testkit.runner.GradleRunner +import org.junit.Assume import org.junit.Test class JavaVersionsCompatibilityTest : BaseKotlinGradleTest() { + private fun skipInDebug(runner: GradleRunner) { + Assume.assumeFalse( + "The test requires a separate Gradle process as it uses a different JVM version, " + + "so it could not be executed with debug turned on.", + runner.isDebug + ) + } + private fun checkCompatibility(useMaxVersion: Boolean) { val runner = test(gradleVersion = "8.5", injectPluginClasspath = false) { buildGradleKts { @@ -33,6 +43,8 @@ class JavaVersionsCompatibilityTest : BaseKotlinGradleTest() { } } + skipInDebug(runner) + runner.build().apply { assertTaskSuccess(":apiCheck") } @@ -59,6 +71,8 @@ class JavaVersionsCompatibilityTest : BaseKotlinGradleTest() { } } + skipInDebug(runner) + runner.build().apply { assertTaskSuccess(":apiCheck") } diff --git a/src/functionalTest/kotlin/kotlinx/validation/test/KlibVerificationTests.kt b/src/functionalTest/kotlin/kotlinx/validation/test/KlibVerificationTests.kt new file mode 100644 index 00000000..63198ed7 --- /dev/null +++ b/src/functionalTest/kotlin/kotlinx/validation/test/KlibVerificationTests.kt @@ -0,0 +1,636 @@ +/* + * Copyright 2016-2023 JetBrains s.r.o. + * Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file. + */ + +package kotlinx.validation.test + +import kotlinx.validation.api.* +import kotlinx.validation.api.buildGradleKts +import kotlinx.validation.api.resolve +import kotlinx.validation.api.test +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.junit.Assume +import org.junit.Test +import java.io.File +import java.nio.file.Files +import java.nio.file.Paths +import kotlin.test.assertTrue + +internal const val BANNED_TARGETS_PROPERTY_NAME = "binary.compatibility.validator.klib.targets.disabled.for.testing" + +private fun KlibVerificationTests.checkKlibDump( + buildResult: BuildResult, + expectedDumpFileName: String, + projectName: String = "testproject", + dumpTask: String = ":apiDump" +) { + buildResult.assertTaskSuccess(dumpTask) + + val generatedDump = rootProjectAbiDump(projectName) + assertTrue(generatedDump.exists(), "There are no dumps generated for KLibs") + + val expected = readFileList(expectedDumpFileName) + + Assertions.assertThat(generatedDump.readText()).isEqualToIgnoringNewLines(expected) +} + +internal class KlibVerificationTests : BaseKotlinGradleTest() { + private fun BaseKotlinScope.baseProjectSetting() { + settingsGradleKts { + resolve("/examples/gradle/settings/settings-name-testproject.gradle.kts") + } + buildGradleKts { + 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") + } + + @Test + fun `apiDump for native targets`() { + val runner = test { + baseProjectSetting() + addToSrcSet("/examples/classes/TopLevelDeclarations.kt") + runApiDump() + } + + checkKlibDump(runner.build(), "/examples/classes/TopLevelDeclarations.klib.with.linux.dump") + } + + @Test + fun `apiCheck for native targets`() { + val runner = test { + baseProjectSetting() + + addToSrcSet("/examples/classes/TopLevelDeclarations.kt") + + abiFile(projectName = "testproject") { + resolve("/examples/classes/TopLevelDeclarations.klib.dump") + } + + runApiCheck() + } + + assertApiCheckPassed(runner.build()) + } + + @Test + fun `apiCheck for native targets should fail when a class is not in a dump`() { + val runner = test { + baseProjectSetting() + addToSrcSet("/examples/classes/BuildConfig.kt") + abiFile(projectName = "testproject") { + resolve("/examples/classes/Empty.klib.dump") + } + runApiCheck() + } + + runner.buildAndFail().apply { + Assertions.assertThat(output) + .contains("+final class com.company/BuildConfig { // com.company/BuildConfig|null[0]") + tasks.filter { it.path.endsWith("ApiCheck") } + .forEach { + assertTaskFailure(it.path) + } + } + } + + @Test + fun `apiDump should include target-specific sources`() { + val runner = test { + baseProjectSetting() + addToSrcSet("/examples/classes/AnotherBuildConfig.kt") + addToSrcSet("/examples/classes/AnotherBuildConfigLinuxArm64.kt", "linuxArm64Main") + runApiDump() + } + + runner.build().apply { + checkKlibDump( + this, + "/examples/classes/AnotherBuildConfigLinuxArm64Extra.klib.dump" + ) + } + } + + @Test + fun `apiDump with native targets along with JVM target`() { + val runner = test { + baseProjectSetting() + additionalBuildConfig("/examples/gradle/base/enableJvmInWithNativePlugin.gradle.kts") + addToSrcSet("/examples/classes/AnotherBuildConfig.kt") + runApiDump() + } + + runner.build().apply { + checkKlibDump(this, "/examples/classes/AnotherBuildConfig.klib.dump") + + val jvmApiDump = rootProjectDir.resolve("$API_DIR/testproject.api") + assertTrue(jvmApiDump.exists(), "No API dump for JVM") + + val jvmExpected = readFileList("/examples/classes/AnotherBuildConfig.dump") + Assertions.assertThat(jvmApiDump.readText()).isEqualToIgnoringNewLines(jvmExpected) + } + } + + @Test + fun `apiDump should ignore a class listed in ignoredClasses`() { + val runner = test { + baseProjectSetting() + additionalBuildConfig("/examples/gradle/configuration/ignoredClasses/oneValidFullyQualifiedClass.gradle.kts") + addToSrcSet("/examples/classes/BuildConfig.kt") + addToSrcSet("/examples/classes/AnotherBuildConfig.kt") + runApiDump() + } + + checkKlibDump(runner.build(), "/examples/classes/AnotherBuildConfig.klib.dump") + } + + @Test + fun `apiDump should succeed if a class listed in ignoredClasses is not found`() { + val runner = test { + baseProjectSetting() + additionalBuildConfig("/examples/gradle/configuration/ignoredClasses/oneValidFullyQualifiedClass.gradle.kts") + addToSrcSet("/examples/classes/AnotherBuildConfig.kt") + runApiDump() + } + + checkKlibDump(runner.build(), "/examples/classes/AnotherBuildConfig.klib.dump") + } + + @Test + fun `apiDump should ignore all entities from a package listed in ingoredPackages`() { + val runner = test { + baseProjectSetting() + additionalBuildConfig("/examples/gradle/configuration/ignoredPackages/oneValidPackage.gradle.kts") + addToSrcSet("/examples/classes/BuildConfig.kt") + addToSrcSet("/examples/classes/AnotherBuildConfig.kt") + addToSrcSet("/examples/classes/SubPackage.kt") + runApiDump() + } + + checkKlibDump(runner.build(), "/examples/classes/AnotherBuildConfig.klib.dump") + } + + @Test + fun `apiDump should ignore all entities annotated with non-public markers`() { + val runner = test { + baseProjectSetting() + additionalBuildConfig("/examples/gradle/configuration/nonPublicMarkers/klib.gradle.kts") + addToSrcSet("/examples/classes/HiddenDeclarations.kt") + addToSrcSet("/examples/classes/NonPublicMarkers.kt") + runApiDump() + } + + checkKlibDump(runner.build(), "/examples/classes/HiddenDeclarations.klib.dump") + } + + @Test + fun `apiDump should not dump subclasses excluded via ignoredClasses`() { + val runner = test { + baseProjectSetting() + additionalBuildConfig("/examples/gradle/configuration/ignoreSubclasses/ignore.gradle.kts") + addToSrcSet("/examples/classes/Subclasses.kt") + runApiDump() + } + + checkKlibDump(runner.build(), "/examples/classes/Subclasses.klib.dump") + } + + @Test + fun `apiCheck for native targets using v1 signatures`() { + val runner = test { + baseProjectSetting() + additionalBuildConfig("/examples/gradle/configuration/signatures/v1.gradle.kts") + addToSrcSet("/examples/classes/TopLevelDeclarations.kt") + + abiFile(projectName = "testproject") { + resolve("/examples/classes/TopLevelDeclarations.klib.v1.dump") + } + + runApiCheck() + } + + assertApiCheckPassed(runner.build()) + } + + @Test + fun `apiDump for native targets should fail when using invalid signature version`() { + val runner = test { + baseProjectSetting() + additionalBuildConfig("/examples/gradle/configuration/signatures/invalid.gradle.kts") + addToSrcSet("/examples/classes/TopLevelDeclarations.kt") + runApiDump() + } + + runner.buildAndFail().apply { + Assertions.assertThat(output).contains("Unsupported KLib signature version '100500'") + } + } + + @Test + fun `apiDump should work for Apple-targets`() { + Assume.assumeTrue(HostManager().isEnabled(KonanTarget.MACOS_ARM64)) + val runner = test { + baseProjectSetting() + additionalBuildConfig("/examples/gradle/configuration/appleTargets/targets.gradle.kts") + addToSrcSet("/examples/classes/TopLevelDeclarations.kt") + runApiDump() + } + + checkKlibDump(runner.build(), "/examples/classes/TopLevelDeclarations.klib.all.dump") + } + + @Test + fun `apiCheck should work for Apple-targets`() { + Assume.assumeTrue(HostManager().isEnabled(KonanTarget.MACOS_ARM64)) + val runner = test { + baseProjectSetting() + additionalBuildConfig("/examples/gradle/configuration/appleTargets/targets.gradle.kts") + addToSrcSet("/examples/classes/TopLevelDeclarations.kt") + abiFile(projectName = "testproject") { + resolve("/examples/classes/TopLevelDeclarations.klib.all.dump") + } + runApiCheck() + } + + assertApiCheckPassed(runner.build()) + } + + @Test + fun `apiCheck should not fail if a target is not supported`() { + val runner = test { + baseProjectSetting() + addToSrcSet("/examples/classes/TopLevelDeclarations.kt") + abiFile(projectName = "testproject") { + resolve("/examples/classes/TopLevelDeclarations.klib.dump") + } + runner { + arguments.add("-P$BANNED_TARGETS_PROPERTY_NAME=linuxArm64") + arguments.add(":apiCheck") + } + } + + assertApiCheckPassed(runner.build()) + } + + @Test + fun `apiCheck should ignore unsupported targets by default`() { + val runner = test { + baseProjectSetting() + 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") + arguments.add(":apiCheck") + } + } + + assertApiCheckPassed(runner.build()) + } + + @Test + fun `apiCheck should fail for unsupported targets with strict mode turned on`() { + 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") + arguments.add(":apiCheck") + } + } + + runner.buildAndFail().apply { + assertTaskFailure(":klibApiExtractForValidation") + } + } + + @Test + fun `klibDump should infer a dump for unsupported target from similar enough target`() { + val runner = test { + baseProjectSetting() + addToSrcSet("/examples/classes/TopLevelDeclarations.kt") + addToSrcSet("/examples/classes/AnotherBuildConfigLinuxArm64.kt", "linuxArm64Main") + runner { + arguments.add("-P$BANNED_TARGETS_PROPERTY_NAME=linuxArm64") + arguments.add(":klibApiDump") + } + } + + checkKlibDump( + runner.build(), "/examples/classes/TopLevelDeclarations.klib.with.linux.dump", + dumpTask = ":klibApiDump" + ) + } + + @Test + fun `infer a dump for a target with custom name`() { + val runner = test { + settingsGradleKts { + resolve("/examples/gradle/settings/settings-name-testproject.gradle.kts") + } + buildGradleKts { + resolve("/examples/gradle/base/withNativePluginAndNoTargets.gradle.kts") + } + additionalBuildConfig("/examples/gradle/configuration/grouping/clashingTargetNames.gradle.kts") + addToSrcSet("/examples/classes/TopLevelDeclarations.kt") + addToSrcSet("/examples/classes/AnotherBuildConfigLinuxArm64.kt", "linuxMain") + runner { + arguments.add("-P$BANNED_TARGETS_PROPERTY_NAME=linux") + arguments.add(":klibApiDump") + } + } + + checkKlibDump( + runner.build(), "/examples/classes/TopLevelDeclarations.klib.with.guessed.linux.dump", + dumpTask = ":klibApiDump" + ) + } + + @Test + fun `klibDump should fail when the only target in the project is disabled`() { + val runner = test { + settingsGradleKts { + resolve("/examples/gradle/settings/settings-name-testproject.gradle.kts") + } + buildGradleKts { + resolve("/examples/gradle/base/withNativePluginAndSingleTarget.gradle.kts") + } + addToSrcSet("/examples/classes/TopLevelDeclarations.kt") + addToSrcSet("/examples/classes/AnotherBuildConfigLinuxArm64.kt", "linuxArm64Main") + runner { + arguments.add("-P$BANNED_TARGETS_PROPERTY_NAME=linuxArm64") + arguments.add(":klibApiDump") + } + } + + runner.buildAndFail().apply { + assertTaskFailure(":linuxArm64ApiInfer") + Assertions.assertThat(output).contains( + "The target linuxArm64 is not supported by the host compiler " + + "and there are no targets similar to linuxArm64 to infer a dump from it." + ) + } + } + + @Test + fun `klibDump if all klib-targets are unavailable`() { + val runner = test { + baseProjectSetting() + addToSrcSet("/examples/classes/TopLevelDeclarations.kt") + runner { + arguments.add( + "-P$BANNED_TARGETS_PROPERTY_NAME=linuxArm64,linuxX64,mingwX64," + + "androidNativeArm32,androidNativeArm64,androidNativeX64,androidNativeX86" + ) + arguments.add(":klibApiDump") + } + } + + runner.buildAndFail().apply { + Assertions.assertThat(output).contains( + "is not supported by the host compiler and there are no targets similar to" + ) + } + } + + @Test + fun `klibCheck if all klib-targets are unavailable`() { + val runner = test { + baseProjectSetting() + 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." + ) + } + } + + @Test + fun `target name clashing with a group name`() { + val runner = test { + settingsGradleKts { + resolve("/examples/gradle/settings/settings-name-testproject.gradle.kts") + } + buildGradleKts { + resolve("/examples/gradle/base/withNativePluginAndNoTargets.gradle.kts") + resolve("/examples/gradle/configuration/grouping/clashingTargetNames.gradle.kts") + } + addToSrcSet("/examples/classes/AnotherBuildConfig.kt") + addToSrcSet("/examples/classes/AnotherBuildConfigLinuxArm64.kt", "linuxArm64Main") + kotlin("AnotherBuildConfigLinuxX64.kt", "linuxMain") { + resolve("/examples/classes/AnotherBuildConfigLinuxArm64.kt") + } + runner { + arguments.add(":klibApiDump") + } + } + + checkKlibDump( + runner.build(), "/examples/classes/AnotherBuildConfig.klib.clash.dump", + dumpTask = ":klibApiDump" + ) + } + + @Test + fun `target name grouping with custom target names`() { + val runner = test { + settingsGradleKts { + resolve("/examples/gradle/settings/settings-name-testproject.gradle.kts") + } + buildGradleKts { + resolve("/examples/gradle/base/withNativePluginAndNoTargets.gradle.kts") + resolve("/examples/gradle/configuration/grouping/customTargetNames.gradle.kts") + } + addToSrcSet("/examples/classes/AnotherBuildConfig.kt") + runner { + arguments.add(":klibApiDump") + } + } + + checkKlibDump( + runner.build(), "/examples/classes/AnotherBuildConfig.klib.custom.dump", + dumpTask = ":klibApiDump" + ) + } + + @Test + fun `target name grouping`() { + val runner = test { + baseProjectSetting() + addToSrcSet("/examples/classes/AnotherBuildConfig.kt") + addToSrcSet("/examples/classes/AnotherBuildConfigLinuxArm64.kt", "linuxArm64Main") + kotlin("AnotherBuildConfigLinuxX64.kt", "linuxX64Main") { + resolve("/examples/classes/AnotherBuildConfigLinuxArm64.kt") + } + runner { + arguments.add(":klibApiDump") + } + } + + checkKlibDump( + runner.build(), "/examples/classes/AnotherBuildConfigLinux.klib.grouping.dump", + dumpTask = ":klibApiDump" + ) + } + + @Test + fun `apiDump should work with web targets`() { + val runner = test { + baseProjectSetting() + additionalBuildConfig("/examples/gradle/configuration/nonNativeKlibTargets/targets.gradle.kts") + addToSrcSet("/examples/classes/AnotherBuildConfig.kt") + runApiDump() + } + + checkKlibDump(runner.build(), "/examples/classes/AnotherBuildConfig.klib.web.dump") + } + + @Test + fun `apiCheck should work with web targets`() { + val runner = test { + baseProjectSetting() + additionalBuildConfig("/examples/gradle/configuration/nonNativeKlibTargets/targets.gradle.kts") + addToSrcSet("/examples/classes/AnotherBuildConfig.kt") + abiFile(projectName = "testproject") { + resolve("/examples/classes/AnotherBuildConfig.klib.web.dump") + } + runApiCheck() + } + + assertApiCheckPassed(runner.build()) + } + + @Test + fun `check dump is updated on added declaration`() { + val runner = test { + baseProjectSetting() + addToSrcSet("/examples/classes/AnotherBuildConfig.kt") + runApiDump() + } + 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 existingSource = runner.projectDir.resolve( + "src/commonMain/kotlin/AnotherBuildConfig.kt" + ) + Files.write(existingSource.toPath(), updatedSourceFile.readBytes()) + + checkKlibDump(runner.build(), "/examples/classes/AnotherBuildConfigModified.klib.dump") + } + + @Test + fun `check dump is updated on a declaration added to some source sets`() { + val runner = test { + baseProjectSetting() + addToSrcSet("/examples/classes/AnotherBuildConfig.kt") + runApiDump() + } + 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 existingSource = runner.projectDir.resolve( + "src/linuxArm64Main/kotlin/AnotherBuildConfigLinuxArm64.kt" + ) + existingSource.parentFile.mkdirs() + Files.write(existingSource.toPath(), updatedSourceFile.readBytes()) + + checkKlibDump(runner.build(), "/examples/classes/AnotherBuildConfigLinuxArm64Extra.klib.dump") + } + + @Test + fun `re-validate dump after sources updated`() { + val runner = test { + baseProjectSetting() + addToSrcSet("/examples/classes/AnotherBuildConfig.kt") + abiFile(projectName = "testproject") { + resolve("/examples/classes/AnotherBuildConfig.klib.dump") + } + runApiCheck() + } + assertApiCheckPassed(runner.build()) + + // Update the source file by adding a declaration + val updatedSourceFile = File(this::class.java.getResource( + "/examples/classes/AnotherBuildConfigModified.kt")!!.toURI() + ) + val existingSource = runner.projectDir.resolve( + "src/commonMain/kotlin/AnotherBuildConfig.kt" + ) + Files.write(existingSource.toPath(), updatedSourceFile.readBytes()) + + runner.buildAndFail().apply { + assertTaskFailure(":klibApiCheck") + } + } + + @Test + fun `validation should fail on target rename`() { + val runner = test { + baseProjectSetting() + addToSrcSet("/examples/classes/AnotherBuildConfig.kt") + abiFile(projectName = "testproject") { + resolve("/examples/classes/AnotherBuildConfig.klib.renamedTarget.dump") + } + runApiCheck() + } + runner.buildAndFail().apply { + Assertions.assertThat(output).contains( + " -// Targets: [androidNativeArm32, androidNativeArm64, androidNativeX64, " + + "androidNativeX86, linuxArm64.linux, linuxX64, mingwX64]" + ) + } + } +} diff --git a/src/functionalTest/kotlin/kotlinx/validation/test/MixedMarkersTest.kt b/src/functionalTest/kotlin/kotlinx/validation/test/MixedMarkersTest.kt index 0c89f317..e97e664a 100644 --- a/src/functionalTest/kotlin/kotlinx/validation/test/MixedMarkersTest.kt +++ b/src/functionalTest/kotlin/kotlinx/validation/test/MixedMarkersTest.kt @@ -31,7 +31,7 @@ class MixedMarkersTest : BaseKotlinGradleTest() { } } - runner.withDebug(true).build().apply { + runner.build().apply { assertTaskSuccess(":apiCheck") } } diff --git a/src/functionalTest/kotlin/kotlinx/validation/test/MultiPlatformSingleJvmTargetTest.kt b/src/functionalTest/kotlin/kotlinx/validation/test/MultiPlatformSingleJvmKlibTargetTest.kt similarity index 97% rename from src/functionalTest/kotlin/kotlinx/validation/test/MultiPlatformSingleJvmTargetTest.kt rename to src/functionalTest/kotlin/kotlinx/validation/test/MultiPlatformSingleJvmKlibTargetTest.kt index f141fa83..dde16426 100644 --- a/src/functionalTest/kotlin/kotlinx/validation/test/MultiPlatformSingleJvmTargetTest.kt +++ b/src/functionalTest/kotlin/kotlinx/validation/test/MultiPlatformSingleJvmKlibTargetTest.kt @@ -10,7 +10,7 @@ import org.assertj.core.api.Assertions.assertThat import org.junit.Test import java.io.File -internal class MultiPlatformSingleJvmTargetTest : BaseKotlinGradleTest() { +internal class MultiPlatformSingleJvmKlibTargetTest : BaseKotlinGradleTest() { private fun BaseKotlinScope.createProjectHierarchyWithPluginOnRoot() { settingsGradleKts { resolve("/examples/gradle/settings/settings-name-testproject.gradle.kts") diff --git a/src/functionalTest/kotlin/kotlinx/validation/test/NonPublicMarkersTest.kt b/src/functionalTest/kotlin/kotlinx/validation/test/NonPublicMarkersTest.kt index 689d9384..4bb804b7 100644 --- a/src/functionalTest/kotlin/kotlinx/validation/test/NonPublicMarkersTest.kt +++ b/src/functionalTest/kotlin/kotlinx/validation/test/NonPublicMarkersTest.kt @@ -38,6 +38,39 @@ class NonPublicMarkersTest : BaseKotlinGradleTest() { } } + @Test + @Ignore("https://youtrack.jetbrains.com/issue/KT-62259") + fun testIgnoredMarkersOnPropertiesForNativeTargets() { + val runner = test { + settingsGradleKts { + resolve("/examples/gradle/settings/settings-name-testproject.gradle.kts") + } + + buildGradleKts { + resolve("/examples/gradle/base/withNativePlugin.gradle.kts") + resolve("/examples/gradle/configuration/nonPublicMarkers/markers.gradle.kts") + } + + kotlin("Properties.kt", sourceSet = "commonMain") { + resolve("/examples/classes/Properties.kt") + } + + commonNativeTargets.forEach { + abiFile(projectName = "testproject", target = it) { + resolve("/examples/classes/Properties.klib.dump") + } + } + + runner { + arguments.add(":apiCheck") + } + } + + runner.build().apply { + assertTaskSuccess(":apiCheck") + } + } + @Test fun testFiltrationByPackageLevelAnnotations() { val runner = test { diff --git a/src/functionalTest/kotlin/kotlinx/validation/test/PublicMarkersTest.kt b/src/functionalTest/kotlin/kotlinx/validation/test/PublicMarkersTest.kt index cf4963b8..9a0b9dcf 100644 --- a/src/functionalTest/kotlin/kotlinx/validation/test/PublicMarkersTest.kt +++ b/src/functionalTest/kotlin/kotlinx/validation/test/PublicMarkersTest.kt @@ -41,6 +41,41 @@ class PublicMarkersTest : BaseKotlinGradleTest() { } } + runner.build().apply { + assertTaskSuccess(":apiCheck") + } + } + + // Public markers are not supported in KLIB ABI dumps + @Test + fun testPublicMarkersForNativeTargets() { + val runner = test { + settingsGradleKts { + resolve("/examples/gradle/settings/settings-name-testproject.gradle.kts") + } + + buildGradleKts { + resolve("/examples/gradle/base/withNativePlugin.gradle.kts") + resolve("/examples/gradle/configuration/publicMarkers/markers.gradle.kts") + } + + kotlin("ClassWithPublicMarkers.kt", sourceSet = "commonMain") { + resolve("/examples/classes/ClassWithPublicMarkers.kt") + } + + kotlin("ClassInPublicPackage.kt", sourceSet = "commonMain") { + resolve("/examples/classes/ClassInPublicPackage.kt") + } + + abiFile(projectName = "testproject") { + resolve("/examples/classes/ClassWithPublicMarkers.klib.dump") + } + + runner { + arguments.add(":apiCheck") + } + } + runner.withDebug(true).build().apply { assertTaskSuccess(":apiCheck") } diff --git a/src/functionalTest/resources/examples/classes/AnotherBuildConfig.klib.clash.dump b/src/functionalTest/resources/examples/classes/AnotherBuildConfig.klib.clash.dump new file mode 100644 index 00000000..78f09cea --- /dev/null +++ b/src/functionalTest/resources/examples/classes/AnotherBuildConfig.klib.clash.dump @@ -0,0 +1,17 @@ +// Klib ABI Dump +// Targets: [androidNativeArm32, androidNativeArm64, androidNativeX64, androidNativeX86, linuxArm64, linuxX64.linux, mingwX64] +// Alias: linux => [linuxArm64, linuxX64.linux] +// Rendering settings: +// - Signature version: 2 +// - Show manifest properties: true +// - Show declarations: true + +// Library unique name: +final class org.different.pack/BuildConfig { // org.different.pack/BuildConfig|null[0] + constructor () // org.different.pack/BuildConfig.|(){}[0] + final fun f1(): kotlin/Int // org.different.pack/BuildConfig.f1|f1(){}[0] + final val p1 // org.different.pack/BuildConfig.p1|{}p1[0] + final fun (): kotlin/Int // org.different.pack/BuildConfig.p1.|(){}[0] +} +// Targets: [linux] +final fun (org.different.pack/BuildConfig).org.different.pack/linuxArm64Specific(): kotlin/Int // org.different.pack/linuxArm64Specific|linuxArm64Specific@org.different.pack.BuildConfig(){}[0] diff --git a/src/functionalTest/resources/examples/classes/AnotherBuildConfig.klib.custom.dump b/src/functionalTest/resources/examples/classes/AnotherBuildConfig.klib.custom.dump new file mode 100644 index 00000000..1adbdac8 --- /dev/null +++ b/src/functionalTest/resources/examples/classes/AnotherBuildConfig.klib.custom.dump @@ -0,0 +1,14 @@ +// Klib ABI Dump +// Targets: [linuxX64.linuxA, linuxX64.linuxB] +// Rendering settings: +// - Signature version: 2 +// - Show manifest properties: true +// - Show declarations: true + +// Library unique name: +final class org.different.pack/BuildConfig { // org.different.pack/BuildConfig|null[0] + constructor () // org.different.pack/BuildConfig.|(){}[0] + final fun f1(): kotlin/Int // org.different.pack/BuildConfig.f1|f1(){}[0] + final val p1 // org.different.pack/BuildConfig.p1|{}p1[0] + final fun (): kotlin/Int // org.different.pack/BuildConfig.p1.|(){}[0] +} diff --git a/src/functionalTest/resources/examples/classes/AnotherBuildConfig.klib.dump b/src/functionalTest/resources/examples/classes/AnotherBuildConfig.klib.dump new file mode 100644 index 00000000..9511bef9 --- /dev/null +++ b/src/functionalTest/resources/examples/classes/AnotherBuildConfig.klib.dump @@ -0,0 +1,14 @@ +// Klib ABI Dump +// Targets: [androidNativeArm32, androidNativeArm64, androidNativeX64, androidNativeX86, linuxArm64, linuxX64, mingwX64] +// Rendering settings: +// - Signature version: 2 +// - Show manifest properties: true +// - Show declarations: true + +// Library unique name: +final class org.different.pack/BuildConfig { // org.different.pack/BuildConfig|null[0] + constructor () // org.different.pack/BuildConfig.|(){}[0] + final fun f1(): kotlin/Int // org.different.pack/BuildConfig.f1|f1(){}[0] + final val p1 // org.different.pack/BuildConfig.p1|{}p1[0] + final fun (): kotlin/Int // org.different.pack/BuildConfig.p1.|(){}[0] +} diff --git a/src/functionalTest/resources/examples/classes/AnotherBuildConfig.klib.renamedTarget.dump b/src/functionalTest/resources/examples/classes/AnotherBuildConfig.klib.renamedTarget.dump new file mode 100644 index 00000000..8b84f00e --- /dev/null +++ b/src/functionalTest/resources/examples/classes/AnotherBuildConfig.klib.renamedTarget.dump @@ -0,0 +1,14 @@ +// Klib ABI Dump +// Targets: [androidNativeArm32, androidNativeArm64, androidNativeX64, androidNativeX86, linuxArm64.linux, linuxX64, mingwX64] +// Rendering settings: +// - Signature version: 2 +// - Show manifest properties: true +// - Show declarations: true + +// Library unique name: +final class org.different.pack/BuildConfig { // org.different.pack/BuildConfig|null[0] + constructor () // org.different.pack/BuildConfig.|(){}[0] + final fun f1(): kotlin/Int // org.different.pack/BuildConfig.f1|f1(){}[0] + final val p1 // org.different.pack/BuildConfig.p1|{}p1[0] + final fun (): kotlin/Int // org.different.pack/BuildConfig.p1.|(){}[0] +} diff --git a/src/functionalTest/resources/examples/classes/AnotherBuildConfig.klib.web.dump b/src/functionalTest/resources/examples/classes/AnotherBuildConfig.klib.web.dump new file mode 100644 index 00000000..bebc349d --- /dev/null +++ b/src/functionalTest/resources/examples/classes/AnotherBuildConfig.klib.web.dump @@ -0,0 +1,14 @@ +// Klib ABI Dump +// Targets: [androidNativeArm32, androidNativeArm64, androidNativeX64, androidNativeX86, js, linuxArm64, linuxX64, mingwX64, wasmJs, wasmWasi] +// Rendering settings: +// - Signature version: 2 +// - Show manifest properties: true +// - Show declarations: true + +// Library unique name: +final class org.different.pack/BuildConfig { // org.different.pack/BuildConfig|null[0] + constructor () // org.different.pack/BuildConfig.|(){}[0] + final fun f1(): kotlin/Int // org.different.pack/BuildConfig.f1|f1(){}[0] + final val p1 // org.different.pack/BuildConfig.p1|{}p1[0] + final fun (): kotlin/Int // org.different.pack/BuildConfig.p1.|(){}[0] +} diff --git a/src/functionalTest/resources/examples/classes/AnotherBuildConfigLinux.klib.grouping.dump b/src/functionalTest/resources/examples/classes/AnotherBuildConfigLinux.klib.grouping.dump new file mode 100644 index 00000000..f67ac443 --- /dev/null +++ b/src/functionalTest/resources/examples/classes/AnotherBuildConfigLinux.klib.grouping.dump @@ -0,0 +1,17 @@ +// Klib ABI Dump +// Targets: [androidNativeArm32, androidNativeArm64, androidNativeX64, androidNativeX86, linuxArm64, linuxX64, mingwX64] +// Alias: linux => [linuxArm64, linuxX64] +// Rendering settings: +// - Signature version: 2 +// - Show manifest properties: true +// - Show declarations: true + +// Library unique name: +final class org.different.pack/BuildConfig { // org.different.pack/BuildConfig|null[0] + constructor () // org.different.pack/BuildConfig.|(){}[0] + final fun f1(): kotlin/Int // org.different.pack/BuildConfig.f1|f1(){}[0] + final val p1 // org.different.pack/BuildConfig.p1|{}p1[0] + final fun (): kotlin/Int // org.different.pack/BuildConfig.p1.|(){}[0] +} +// Targets: [linux] +final fun (org.different.pack/BuildConfig).org.different.pack/linuxArm64Specific(): kotlin/Int // org.different.pack/linuxArm64Specific|linuxArm64Specific@org.different.pack.BuildConfig(){}[0] diff --git a/src/functionalTest/resources/examples/classes/AnotherBuildConfigLinuxArm64.kt b/src/functionalTest/resources/examples/classes/AnotherBuildConfigLinuxArm64.kt new file mode 100644 index 00000000..f5352c0b --- /dev/null +++ b/src/functionalTest/resources/examples/classes/AnotherBuildConfigLinuxArm64.kt @@ -0,0 +1,8 @@ +/* + * Copyright 2016-2023 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 org.different.pack + +fun BuildConfig.linuxArm64Specific(): Int = 42 diff --git a/src/functionalTest/resources/examples/classes/AnotherBuildConfigLinuxArm64Extra.klib.dump b/src/functionalTest/resources/examples/classes/AnotherBuildConfigLinuxArm64Extra.klib.dump new file mode 100644 index 00000000..20292d7e --- /dev/null +++ b/src/functionalTest/resources/examples/classes/AnotherBuildConfigLinuxArm64Extra.klib.dump @@ -0,0 +1,16 @@ +// Klib ABI Dump +// Targets: [androidNativeArm32, androidNativeArm64, androidNativeX64, androidNativeX86, linuxArm64, linuxX64, mingwX64] +// Rendering settings: +// - Signature version: 2 +// - Show manifest properties: true +// - Show declarations: true + +// Library unique name: +final class org.different.pack/BuildConfig { // org.different.pack/BuildConfig|null[0] + constructor () // org.different.pack/BuildConfig.|(){}[0] + final fun f1(): kotlin/Int // org.different.pack/BuildConfig.f1|f1(){}[0] + final val p1 // org.different.pack/BuildConfig.p1|{}p1[0] + final fun (): kotlin/Int // org.different.pack/BuildConfig.p1.|(){}[0] +} +// Targets: [linuxArm64] +final fun (org.different.pack/BuildConfig).org.different.pack/linuxArm64Specific(): kotlin/Int // org.different.pack/linuxArm64Specific|linuxArm64Specific@org.different.pack.BuildConfig(){}[0] diff --git a/src/functionalTest/resources/examples/classes/AnotherBuildConfigModified.klib.dump b/src/functionalTest/resources/examples/classes/AnotherBuildConfigModified.klib.dump new file mode 100644 index 00000000..75eb66b2 --- /dev/null +++ b/src/functionalTest/resources/examples/classes/AnotherBuildConfigModified.klib.dump @@ -0,0 +1,15 @@ +// Klib ABI Dump +// Targets: [androidNativeArm32, androidNativeArm64, androidNativeX64, androidNativeX86, linuxArm64, linuxX64, mingwX64] +// Rendering settings: +// - Signature version: 2 +// - Show manifest properties: true +// - Show declarations: true + +// Library unique name: +final class org.different.pack/BuildConfig { // org.different.pack/BuildConfig|null[0] + constructor () // org.different.pack/BuildConfig.|(){}[0] + final fun f1(): kotlin/Int // org.different.pack/BuildConfig.f1|f1(){}[0] + final fun f2(): kotlin/Int // org.different.pack/BuildConfig.f2|f2(){}[0] + final val p1 // org.different.pack/BuildConfig.p1|{}p1[0] + final fun (): kotlin/Int // org.different.pack/BuildConfig.p1.|(){}[0] +} diff --git a/src/functionalTest/resources/examples/classes/AnotherBuildConfigModified.kt b/src/functionalTest/resources/examples/classes/AnotherBuildConfigModified.kt new file mode 100644 index 00000000..8165117b --- /dev/null +++ b/src/functionalTest/resources/examples/classes/AnotherBuildConfigModified.kt @@ -0,0 +1,14 @@ +/* + * Copyright 2016-2020 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 org.different.pack + +public class BuildConfig { + public val p1 = 1 + + public fun f1() = p1 + + public fun f2() = p1 +} diff --git a/src/functionalTest/resources/examples/classes/ClassWithPublicMarkers.klib.dump b/src/functionalTest/resources/examples/classes/ClassWithPublicMarkers.klib.dump new file mode 100644 index 00000000..b1e8f295 --- /dev/null +++ b/src/functionalTest/resources/examples/classes/ClassWithPublicMarkers.klib.dump @@ -0,0 +1,45 @@ +// Klib ABI Dump +// Targets: [androidNativeArm32, androidNativeArm64, androidNativeX64, androidNativeX86, linuxArm64, linuxX64, mingwX64] +// Alias: androidNative => [androidNativeArm32, androidNativeArm64, androidNativeX64, androidNativeX86] +// Alias: linux => [linuxArm64, linuxX64] +// Rendering settings: +// - Signature version: 2 +// - Show manifest properties: true +// - Show declarations: true + +// Library unique name: +final class foo.api/ClassInPublicPackage { // foo.api/ClassInPublicPackage|null[0] + constructor () // foo.api/ClassInPublicPackage.|(){}[0] + final class Inner { // foo.api/ClassInPublicPackage.Inner|null[0] + constructor () // foo.api/ClassInPublicPackage.Inner.|(){}[0] + } +} +final class foo/ClassWithPublicMarkers { // foo/ClassWithPublicMarkers|null[0] + constructor () // foo/ClassWithPublicMarkers.|(){}[0] + final class MarkedClass { // foo/ClassWithPublicMarkers.MarkedClass|null[0] + constructor () // foo/ClassWithPublicMarkers.MarkedClass.|(){}[0] + final val bar1 // foo/ClassWithPublicMarkers.MarkedClass.bar1|{}bar1[0] + final fun (): kotlin/Int // foo/ClassWithPublicMarkers.MarkedClass.bar1.|(){}[0] + } + final class NotMarkedClass { // foo/ClassWithPublicMarkers.NotMarkedClass|null[0] + constructor () // foo/ClassWithPublicMarkers.NotMarkedClass.|(){}[0] + } + final var bar1 // foo/ClassWithPublicMarkers.bar1|{}bar1[0] + final fun (): kotlin/Int // foo/ClassWithPublicMarkers.bar1.|(){}[0] + final fun (kotlin/Int) // foo/ClassWithPublicMarkers.bar1.|(kotlin.Int){}[0] + final var bar2 // foo/ClassWithPublicMarkers.bar2|{}bar2[0] + final fun (): kotlin/Int // foo/ClassWithPublicMarkers.bar2.|(){}[0] + final fun (kotlin/Int) // foo/ClassWithPublicMarkers.bar2.|(kotlin.Int){}[0] + final var notMarkedPublic // foo/ClassWithPublicMarkers.notMarkedPublic|{}notMarkedPublic[0] + final fun (): kotlin/Int // foo/ClassWithPublicMarkers.notMarkedPublic.|(){}[0] + final fun (kotlin/Int) // foo/ClassWithPublicMarkers.notMarkedPublic.|(kotlin.Int){}[0] +} +open annotation class foo/PublicClass : kotlin/Annotation { // foo/PublicClass|null[0] + constructor () // foo/PublicClass.|(){}[0] +} +open annotation class foo/PublicField : kotlin/Annotation { // foo/PublicField|null[0] + constructor () // foo/PublicField.|(){}[0] +} +open annotation class foo/PublicProperty : kotlin/Annotation { // foo/PublicProperty|null[0] + constructor () // foo/PublicProperty.|(){}[0] +} diff --git a/src/functionalTest/resources/examples/classes/Empty.klib.dump b/src/functionalTest/resources/examples/classes/Empty.klib.dump new file mode 100644 index 00000000..40583d92 --- /dev/null +++ b/src/functionalTest/resources/examples/classes/Empty.klib.dump @@ -0,0 +1,6 @@ +// Klib ABI Dump +// Targets: [mingwX64] +// Rendering settings: +// - Signature version: 2 +// - Show manifest properties: true +// - Show declarations: true diff --git a/src/functionalTest/resources/examples/classes/HiddenDeclarations.klib.dump b/src/functionalTest/resources/examples/classes/HiddenDeclarations.klib.dump new file mode 100644 index 00000000..bed1c06f --- /dev/null +++ b/src/functionalTest/resources/examples/classes/HiddenDeclarations.klib.dump @@ -0,0 +1,11 @@ +// Klib ABI Dump +// Targets: [androidNativeArm32, androidNativeArm64, androidNativeX64, androidNativeX86, linuxArm64, linuxX64, mingwX64] +// Rendering settings: +// - Signature version: 2 +// - Show manifest properties: true +// - Show declarations: true + +// Library unique name: +final class examples.classes/VC { // examples.classes/VC|null[0] + final var prop // examples.classes/VC.prop|{}prop[0] +} diff --git a/src/functionalTest/resources/examples/classes/HiddenDeclarations.kt b/src/functionalTest/resources/examples/classes/HiddenDeclarations.kt new file mode 100644 index 00000000..702ed05a --- /dev/null +++ b/src/functionalTest/resources/examples/classes/HiddenDeclarations.kt @@ -0,0 +1,43 @@ +/* + * Copyright 2016-2023 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 examples.classes + +import annotations.* + +@HiddenFunction +public fun hidden() = Unit + +@HiddenProperty +public val v: Int = 42 + +@HiddenClass +public class HC + +public class VC @HiddenCtor constructor() { + @HiddenProperty + public val v: Int = 42 + + public var prop: Int = 0 + @HiddenGetter + get() = field + @HiddenSetter + set(value) { + field = value + } + + @HiddenProperty + public var fullyHiddenProp: Int = 0 + + @HiddenFunction + public fun m() = Unit +} + +@HiddenClass +public class HiddenOuterClass { + public class HiddenInnerClass { + + } +} diff --git a/src/functionalTest/resources/examples/classes/NonPublicMarkers.kt b/src/functionalTest/resources/examples/classes/NonPublicMarkers.kt new file mode 100644 index 00000000..fdf82887 --- /dev/null +++ b/src/functionalTest/resources/examples/classes/NonPublicMarkers.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2016-2023 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 annotations + +@HiddenClass +@Target(AnnotationTarget.CLASS) +annotation class HiddenClass + +@HiddenClass +@Target(AnnotationTarget.FUNCTION) +annotation class HiddenFunction + +@HiddenClass +@Target(AnnotationTarget.CONSTRUCTOR) +annotation class HiddenCtor + +@HiddenClass +@Target(AnnotationTarget.PROPERTY) +annotation class HiddenProperty + +@HiddenClass +@Target(AnnotationTarget.FIELD) +annotation class HiddenField + +@HiddenClass +@Target(AnnotationTarget.PROPERTY_GETTER) +annotation class HiddenGetter + +@HiddenClass +@Target(AnnotationTarget.PROPERTY_SETTER) +annotation class HiddenSetter diff --git a/src/functionalTest/resources/examples/classes/Properties.klib.dump b/src/functionalTest/resources/examples/classes/Properties.klib.dump new file mode 100644 index 00000000..6359372b --- /dev/null +++ b/src/functionalTest/resources/examples/classes/Properties.klib.dump @@ -0,0 +1,17 @@ +// Klib ABI Dump +// Targets: [androidNativeArm32, androidNativeArm64, androidNativeX64, androidNativeX86, linuxArm64, linuxX64, mingwX64] +// Rendering settings: +// - Signature version: 2 +// - Show manifest properties: true +// - Show declarations: true + +// Library unique name: +final class foo/ClassWithProperties { // foo/ClassWithProperties|null[0] + constructor () // foo/ClassWithProperties.|(){}[0] +} +open annotation class foo/HiddenField : kotlin/Annotation { // foo/HiddenField|null[0] + constructor () // foo/HiddenField.|(){}[0] +} +open annotation class foo/HiddenProperty : kotlin/Annotation { // foo/HiddenProperty|null[0] + constructor () // foo/HiddenProperty.|(){}[0] +} diff --git a/src/functionalTest/resources/examples/classes/SubPackage.kt b/src/functionalTest/resources/examples/classes/SubPackage.kt new file mode 100644 index 00000000..c5a298b5 --- /dev/null +++ b/src/functionalTest/resources/examples/classes/SubPackage.kt @@ -0,0 +1,10 @@ +/* + * Copyright 2016-2023 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 com.company.division + +public class ClassWithinSubPackage { +} + diff --git a/src/functionalTest/resources/examples/classes/Subclasses.dump b/src/functionalTest/resources/examples/classes/Subclasses.dump new file mode 100644 index 00000000..04cb1523 --- /dev/null +++ b/src/functionalTest/resources/examples/classes/Subclasses.dump @@ -0,0 +1,7 @@ +public final class subclasses/A { + public fun ()V +} + +public final class subclasses/A$D { + public fun ()V +} diff --git a/src/functionalTest/resources/examples/classes/Subclasses.klib.dump b/src/functionalTest/resources/examples/classes/Subclasses.klib.dump new file mode 100644 index 00000000..e13fa3f6 --- /dev/null +++ b/src/functionalTest/resources/examples/classes/Subclasses.klib.dump @@ -0,0 +1,14 @@ +// Klib ABI Dump +// Targets: [androidNativeArm32, androidNativeArm64, androidNativeX64, androidNativeX86, linuxArm64, linuxX64, mingwX64] +// Rendering settings: +// - Signature version: 2 +// - Show manifest properties: true +// - Show declarations: true + +// Library unique name: +final class subclasses/A { // subclasses/A|null[0] + constructor () // subclasses/A.|(){}[0] + final class D { // subclasses/A.D|null[0] + constructor () // subclasses/A.D.|(){}[0] + } +} diff --git a/src/functionalTest/resources/examples/classes/Subclasses.kt b/src/functionalTest/resources/examples/classes/Subclasses.kt new file mode 100644 index 00000000..7f3f6392 --- /dev/null +++ b/src/functionalTest/resources/examples/classes/Subclasses.kt @@ -0,0 +1,16 @@ +/* + * Copyright 2016-2023 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 subclasses + +public class A { + public class B { + public class C + } + + public class D { + + } +} diff --git a/src/functionalTest/resources/examples/classes/TopLevelDeclarations.klib.all.dump b/src/functionalTest/resources/examples/classes/TopLevelDeclarations.klib.all.dump new file mode 100644 index 00000000..aa08591a --- /dev/null +++ b/src/functionalTest/resources/examples/classes/TopLevelDeclarations.klib.all.dump @@ -0,0 +1,67 @@ +// Klib ABI Dump +// Targets: [androidNativeArm32, androidNativeArm64, androidNativeX64, androidNativeX86, iosArm64, iosSimulatorArm64, iosX64, linuxArm64, linuxX64, macosArm64, macosX64, mingwX64, tvosArm64, tvosSimulatorArm64, tvosX64, watchosArm32, watchosArm64, watchosDeviceArm64, watchosSimulatorArm64, watchosX64] +// Rendering settings: +// - Signature version: 2 +// - Show manifest properties: true +// - Show declarations: true + +// Library unique name: +abstract class examples.classes/AC { // examples.classes/AC|null[0] + abstract fun a() // examples.classes/AC.a|a(){}[0] + constructor () // examples.classes/AC.|(){}[0] + final fun b() // examples.classes/AC.b|b(){}[0] +} +abstract interface examples.classes/I // examples.classes/I|null[0] +final class examples.classes/C { // examples.classes/C|null[0] + constructor (kotlin/Any) // examples.classes/C.|(kotlin.Any){}[0] + final fun m() // examples.classes/C.m|m(){}[0] + final val v // examples.classes/C.v|{}v[0] + final fun (): kotlin/Any // examples.classes/C.v.|(){}[0] +} +final class examples.classes/D { // examples.classes/D|null[0] + constructor (kotlin/Int) // examples.classes/D.|(kotlin.Int){}[0] + final fun component1(): kotlin/Int // examples.classes/D.component1|component1(){}[0] + final fun copy(kotlin/Int =...): examples.classes/D // examples.classes/D.copy|copy(kotlin.Int){}[0] + final fun equals(kotlin/Any?): kotlin/Boolean // examples.classes/D.equals|equals(kotlin.Any?){}[0] + final fun hashCode(): kotlin/Int // examples.classes/D.hashCode|hashCode(){}[0] + final fun toString(): kotlin/String // examples.classes/D.toString|toString(){}[0] + final val x // examples.classes/D.x|{}x[0] + final fun (): kotlin/Int // examples.classes/D.x.|(){}[0] +} +final class examples.classes/Outer { // examples.classes/Outer|null[0] + constructor () // examples.classes/Outer.|(){}[0] + final class Nested { // examples.classes/Outer.Nested|null[0] + constructor () // examples.classes/Outer.Nested.|(){}[0] + final inner class Inner { // examples.classes/Outer.Nested.Inner|null[0] + constructor () // examples.classes/Outer.Nested.Inner.|(){}[0] + } + } +} +final const val examples.classes/con // examples.classes/con|{}con[0] + final fun (): kotlin/String // examples.classes/con.|(){}[0] +final enum class examples.classes/E : kotlin/Enum { // examples.classes/E|null[0] + enum entry A // examples.classes/E.A|null[0] + enum entry B // examples.classes/E.B|null[0] + enum entry C // examples.classes/E.C|null[0] + final fun valueOf(kotlin/String): examples.classes/E // examples.classes/E.valueOf|valueOf#static(kotlin.String){}[0] + final fun values(): kotlin/Array // examples.classes/E.values|values#static(){}[0] + final val entries // examples.classes/E.entries|#static{}entries[0] + final fun (): kotlin.enums/EnumEntries // examples.classes/E.entries.|#static(){}[0] +} +final fun <#A: kotlin/Any?> examples.classes/consume(#A) // examples.classes/consume|consume(0:0){0§}[0] +final fun examples.classes/testFun(): kotlin/Int // examples.classes/testFun|testFun(){}[0] +final inline fun examples.classes/testInlineFun() // examples.classes/testInlineFun|testInlineFun(){}[0] +final object examples.classes/O // examples.classes/O|null[0] +final val examples.classes/l // examples.classes/l|{}l[0] + final fun (): kotlin/Long // examples.classes/l.|(){}[0] +final var examples.classes/r // examples.classes/r|{}r[0] + final fun (): kotlin/Float // examples.classes/r.|(){}[0] + final fun (kotlin/Float) // examples.classes/r.|(kotlin.Float){}[0] +open annotation class examples.classes/A : kotlin/Annotation { // examples.classes/A|null[0] + constructor () // examples.classes/A.|(){}[0] +} +open class examples.classes/OC { // examples.classes/OC|null[0] + constructor () // examples.classes/OC.|(){}[0] + final fun c() // examples.classes/OC.c|c(){}[0] + open fun o(): kotlin/Int // examples.classes/OC.o|o(){}[0] +} diff --git a/src/functionalTest/resources/examples/classes/TopLevelDeclarations.klib.dump b/src/functionalTest/resources/examples/classes/TopLevelDeclarations.klib.dump new file mode 100644 index 00000000..c7bb38f6 --- /dev/null +++ b/src/functionalTest/resources/examples/classes/TopLevelDeclarations.klib.dump @@ -0,0 +1,67 @@ +// Klib ABI Dump +// Targets: [androidNativeArm32, androidNativeArm64, androidNativeX64, androidNativeX86, linuxArm64, linuxX64, mingwX64] +// Rendering settings: +// - Signature version: 2 +// - Show manifest properties: true +// - Show declarations: true + +// Library unique name: +abstract class examples.classes/AC { // examples.classes/AC|null[0] + abstract fun a() // examples.classes/AC.a|a(){}[0] + constructor () // examples.classes/AC.|(){}[0] + final fun b() // examples.classes/AC.b|b(){}[0] +} +abstract interface examples.classes/I // examples.classes/I|null[0] +final class examples.classes/C { // examples.classes/C|null[0] + constructor (kotlin/Any) // examples.classes/C.|(kotlin.Any){}[0] + final fun m() // examples.classes/C.m|m(){}[0] + final val v // examples.classes/C.v|{}v[0] + final fun (): kotlin/Any // examples.classes/C.v.|(){}[0] +} +final class examples.classes/D { // examples.classes/D|null[0] + constructor (kotlin/Int) // examples.classes/D.|(kotlin.Int){}[0] + final fun component1(): kotlin/Int // examples.classes/D.component1|component1(){}[0] + final fun copy(kotlin/Int =...): examples.classes/D // examples.classes/D.copy|copy(kotlin.Int){}[0] + final fun equals(kotlin/Any?): kotlin/Boolean // examples.classes/D.equals|equals(kotlin.Any?){}[0] + final fun hashCode(): kotlin/Int // examples.classes/D.hashCode|hashCode(){}[0] + final fun toString(): kotlin/String // examples.classes/D.toString|toString(){}[0] + final val x // examples.classes/D.x|{}x[0] + final fun (): kotlin/Int // examples.classes/D.x.|(){}[0] +} +final class examples.classes/Outer { // examples.classes/Outer|null[0] + constructor () // examples.classes/Outer.|(){}[0] + final class Nested { // examples.classes/Outer.Nested|null[0] + constructor () // examples.classes/Outer.Nested.|(){}[0] + final inner class Inner { // examples.classes/Outer.Nested.Inner|null[0] + constructor () // examples.classes/Outer.Nested.Inner.|(){}[0] + } + } +} +final const val examples.classes/con // examples.classes/con|{}con[0] + final fun (): kotlin/String // examples.classes/con.|(){}[0] +final enum class examples.classes/E : kotlin/Enum { // examples.classes/E|null[0] + enum entry A // examples.classes/E.A|null[0] + enum entry B // examples.classes/E.B|null[0] + enum entry C // examples.classes/E.C|null[0] + final fun valueOf(kotlin/String): examples.classes/E // examples.classes/E.valueOf|valueOf#static(kotlin.String){}[0] + final fun values(): kotlin/Array // examples.classes/E.values|values#static(){}[0] + final val entries // examples.classes/E.entries|#static{}entries[0] + final fun (): kotlin.enums/EnumEntries // examples.classes/E.entries.|#static(){}[0] +} +final fun <#A: kotlin/Any?> examples.classes/consume(#A) // examples.classes/consume|consume(0:0){0§}[0] +final fun examples.classes/testFun(): kotlin/Int // examples.classes/testFun|testFun(){}[0] +final inline fun examples.classes/testInlineFun() // examples.classes/testInlineFun|testInlineFun(){}[0] +final object examples.classes/O // examples.classes/O|null[0] +final val examples.classes/l // examples.classes/l|{}l[0] + final fun (): kotlin/Long // examples.classes/l.|(){}[0] +final var examples.classes/r // examples.classes/r|{}r[0] + final fun (): kotlin/Float // examples.classes/r.|(){}[0] + final fun (kotlin/Float) // examples.classes/r.|(kotlin.Float){}[0] +open annotation class examples.classes/A : kotlin/Annotation { // examples.classes/A|null[0] + constructor () // examples.classes/A.|(){}[0] +} +open class examples.classes/OC { // examples.classes/OC|null[0] + constructor () // examples.classes/OC.|(){}[0] + final fun c() // examples.classes/OC.c|c(){}[0] + open fun o(): kotlin/Int // examples.classes/OC.o|o(){}[0] +} diff --git a/src/functionalTest/resources/examples/classes/TopLevelDeclarations.klib.unsup.dump b/src/functionalTest/resources/examples/classes/TopLevelDeclarations.klib.unsup.dump new file mode 100644 index 00000000..9764aec3 --- /dev/null +++ b/src/functionalTest/resources/examples/classes/TopLevelDeclarations.klib.unsup.dump @@ -0,0 +1,67 @@ +// Klib ABI Dump +// Targets: [androidNativeArm32, androidNativeArm64, androidNativeX64, androidNativeX86, linuxX64, mingwX64] +// Rendering settings: +// - Signature version: 2 +// - Show manifest properties: true +// - Show declarations: true + +// Library unique name: +abstract class examples.classes/AC { // examples.classes/AC|null[0] + abstract fun a() // examples.classes/AC.a|a(){}[0] + constructor () // examples.classes/AC.|(){}[0] + final fun b() // examples.classes/AC.b|b(){}[0] +} +abstract interface examples.classes/I // examples.classes/I|null[0] +final class examples.classes/C { // examples.classes/C|null[0] + constructor (kotlin/Any) // examples.classes/C.|(kotlin.Any){}[0] + final fun m() // examples.classes/C.m|m(){}[0] + final val v // examples.classes/C.v|{}v[0] + final fun (): kotlin/Any // examples.classes/C.v.|(){}[0] +} +final class examples.classes/D { // examples.classes/D|null[0] + constructor (kotlin/Int) // examples.classes/D.|(kotlin.Int){}[0] + final fun component1(): kotlin/Int // examples.classes/D.component1|component1(){}[0] + final fun copy(kotlin/Int =...): examples.classes/D // examples.classes/D.copy|copy(kotlin.Int){}[0] + final fun equals(kotlin/Any?): kotlin/Boolean // examples.classes/D.equals|equals(kotlin.Any?){}[0] + final fun hashCode(): kotlin/Int // examples.classes/D.hashCode|hashCode(){}[0] + final fun toString(): kotlin/String // examples.classes/D.toString|toString(){}[0] + final val x // examples.classes/D.x|{}x[0] + final fun (): kotlin/Int // examples.classes/D.x.|(){}[0] +} +final class examples.classes/Outer { // examples.classes/Outer|null[0] + constructor () // examples.classes/Outer.|(){}[0] + final class Nested { // examples.classes/Outer.Nested|null[0] + constructor () // examples.classes/Outer.Nested.|(){}[0] + final inner class Inner { // examples.classes/Outer.Nested.Inner|null[0] + constructor () // examples.classes/Outer.Nested.Inner.|(){}[0] + } + } +} +final const val examples.classes/con // examples.classes/con|{}con[0] + final fun (): kotlin/String // examples.classes/con.|(){}[0] +final enum class examples.classes/E : kotlin/Enum { // examples.classes/E|null[0] + enum entry A // examples.classes/E.A|null[0] + enum entry B // examples.classes/E.B|null[0] + enum entry C // examples.classes/E.C|null[0] + final fun valueOf(kotlin/String): examples.classes/E // examples.classes/E.valueOf|valueOf#static(kotlin.String){}[0] + final fun values(): kotlin/Array // examples.classes/E.values|values#static(){}[0] + final val entries // examples.classes/E.entries|#static{}entries[0] + final fun (): kotlin.enums/EnumEntries // examples.classes/E.entries.|#static(){}[0] +} +final fun <#A: kotlin/Any?> examples.classes/consume(#A) // examples.classes/consume|consume(0:0){0§}[0] +final fun examples.classes/testFun(): kotlin/Int // examples.classes/testFun|testFun(){}[0] +final inline fun examples.classes/testInlineFun() // examples.classes/testInlineFun|testInlineFun(){}[0] +final object examples.classes/O // examples.classes/O|null[0] +final val examples.classes/l // examples.classes/l|{}l[0] + final fun (): kotlin/Long // examples.classes/l.|(){}[0] +final var examples.classes/r // examples.classes/r|{}r[0] + final fun (): kotlin/Float // examples.classes/r.|(){}[0] + final fun (kotlin/Float) // examples.classes/r.|(kotlin.Float){}[0] +open annotation class examples.classes/A : kotlin/Annotation { // examples.classes/A|null[0] + constructor () // examples.classes/A.|(){}[0] +} +open class examples.classes/OC { // examples.classes/OC|null[0] + constructor () // examples.classes/OC.|(){}[0] + final fun c() // examples.classes/OC.c|c(){}[0] + open fun o(): kotlin/Int // examples.classes/OC.o|o(){}[0] +} diff --git a/src/functionalTest/resources/examples/classes/TopLevelDeclarations.klib.v1.dump b/src/functionalTest/resources/examples/classes/TopLevelDeclarations.klib.v1.dump new file mode 100644 index 00000000..9442fd64 --- /dev/null +++ b/src/functionalTest/resources/examples/classes/TopLevelDeclarations.klib.v1.dump @@ -0,0 +1,67 @@ +// Klib ABI Dump +// Targets: [androidNativeArm32, androidNativeArm64, androidNativeX64, androidNativeX86, linuxArm64, linuxX64, mingwX64] +// Rendering settings: +// - Signature version: 1 +// - Show manifest properties: true +// - Show declarations: true + +// Library unique name: +abstract class examples.classes/AC { // examples.classes/AC|null[0] + abstract fun a() // examples.classes/AC.a|-4432112437378250461[0] + constructor () // examples.classes/AC.|-5645683436151566731[0] + final fun b() // examples.classes/AC.b|4789657038926421504[0] +} +abstract interface examples.classes/I // examples.classes/I|null[0] +final class examples.classes/C { // examples.classes/C|null[0] + constructor (kotlin/Any) // examples.classes/C.|4518179880532599055[0] + final fun m() // examples.classes/C.m|-1029306787563722981[0] + final val v // examples.classes/C.v|138869847852828796[0] + final fun (): kotlin/Any // examples.classes/C.v.|4964732996156868941[0] +} +final class examples.classes/D { // examples.classes/D|null[0] + constructor (kotlin/Int) // examples.classes/D.|-5182794243525578284[0] + final fun component1(): kotlin/Int // examples.classes/D.component1|162597135895221648[0] + final fun copy(kotlin/Int =...): examples.classes/D // examples.classes/D.copy|-6971662324481626298[0] + final fun equals(kotlin/Any?): kotlin/Boolean // examples.classes/D.equals|4638265728071529943[0] + final fun hashCode(): kotlin/Int // examples.classes/D.hashCode|3409210261493131192[0] + final fun toString(): kotlin/String // examples.classes/D.toString|-1522858123163872138[0] + final val x // examples.classes/D.x|-8060530855978347579[0] + final fun (): kotlin/Int // examples.classes/D.x.|1482705010654679335[0] +} +final class examples.classes/Outer { // examples.classes/Outer|null[0] + constructor () // examples.classes/Outer.|-5645683436151566731[0] + final class Nested { // examples.classes/Outer.Nested|null[0] + constructor () // examples.classes/Outer.Nested.|-5645683436151566731[0] + final inner class Inner { // examples.classes/Outer.Nested.Inner|null[0] + constructor () // examples.classes/Outer.Nested.Inner.|-5645683436151566731[0] + } + } +} +final const val examples.classes/con // examples.classes/con|-2899158152154217071[0] + final fun (): kotlin/String // examples.classes/con.|-2604863570302238407[0] +final enum class examples.classes/E : kotlin/Enum { // examples.classes/E|null[0] + enum entry A // examples.classes/E.A|null[0] + enum entry B // examples.classes/E.B|null[0] + enum entry C // examples.classes/E.C|null[0] + final fun valueOf(kotlin/String): examples.classes/E // examples.classes/E.valueOf|-4683474617854611729[0] + final fun values(): kotlin/Array // examples.classes/E.values|-8715569000920726747[0] + final val entries // examples.classes/E.entries|-5134227801081826149[0] + final fun (): kotlin.enums/EnumEntries // examples.classes/E.entries.|-6068527377476727729[0] +} +final fun <#A: kotlin/Any?> examples.classes/consume(#A) // examples.classes/consume|8042761629495509481[0] +final fun examples.classes/testFun(): kotlin/Int // examples.classes/testFun|6322333980269160703[0] +final inline fun examples.classes/testInlineFun() // examples.classes/testInlineFun|-9193388292326484960[0] +final object examples.classes/O // examples.classes/O|null[0] +final val examples.classes/l // examples.classes/l|3307215303229595169[0] + final fun (): kotlin/Long // examples.classes/l.|3795442967620585[0] +final var examples.classes/r // examples.classes/r|-8117627916896159533[0] + final fun (): kotlin/Float // examples.classes/r.|-7424184448774736572[0] + final fun (kotlin/Float) // examples.classes/r.|9171637170963327464[0] +open annotation class examples.classes/A : kotlin/Annotation { // examples.classes/A|null[0] + constructor () // examples.classes/A.|-5645683436151566731[0] +} +open class examples.classes/OC { // examples.classes/OC|null[0] + constructor () // examples.classes/OC.|-5645683436151566731[0] + final fun c() // examples.classes/OC.c|-2724918380551733646[0] + open fun o(): kotlin/Int // examples.classes/OC.o|-3264635847192431671[0] +} diff --git a/src/functionalTest/resources/examples/classes/TopLevelDeclarations.klib.with.guessed.linux.dump b/src/functionalTest/resources/examples/classes/TopLevelDeclarations.klib.with.guessed.linux.dump new file mode 100644 index 00000000..5b397f22 --- /dev/null +++ b/src/functionalTest/resources/examples/classes/TopLevelDeclarations.klib.with.guessed.linux.dump @@ -0,0 +1,67 @@ +// Klib ABI Dump +// Targets: [androidNativeArm32, androidNativeArm64, androidNativeX64, androidNativeX86, linuxArm64, linuxX64.linux, mingwX64] +// Rendering settings: +// - Signature version: 2 +// - Show manifest properties: true +// - Show declarations: true + +// Library unique name: +abstract class examples.classes/AC { // examples.classes/AC|null[0] + abstract fun a() // examples.classes/AC.a|a(){}[0] + constructor () // examples.classes/AC.|(){}[0] + final fun b() // examples.classes/AC.b|b(){}[0] +} +abstract interface examples.classes/I // examples.classes/I|null[0] +final class examples.classes/C { // examples.classes/C|null[0] + constructor (kotlin/Any) // examples.classes/C.|(kotlin.Any){}[0] + final fun m() // examples.classes/C.m|m(){}[0] + final val v // examples.classes/C.v|{}v[0] + final fun (): kotlin/Any // examples.classes/C.v.|(){}[0] +} +final class examples.classes/D { // examples.classes/D|null[0] + constructor (kotlin/Int) // examples.classes/D.|(kotlin.Int){}[0] + final fun component1(): kotlin/Int // examples.classes/D.component1|component1(){}[0] + final fun copy(kotlin/Int =...): examples.classes/D // examples.classes/D.copy|copy(kotlin.Int){}[0] + final fun equals(kotlin/Any?): kotlin/Boolean // examples.classes/D.equals|equals(kotlin.Any?){}[0] + final fun hashCode(): kotlin/Int // examples.classes/D.hashCode|hashCode(){}[0] + final fun toString(): kotlin/String // examples.classes/D.toString|toString(){}[0] + final val x // examples.classes/D.x|{}x[0] + final fun (): kotlin/Int // examples.classes/D.x.|(){}[0] +} +final class examples.classes/Outer { // examples.classes/Outer|null[0] + constructor () // examples.classes/Outer.|(){}[0] + final class Nested { // examples.classes/Outer.Nested|null[0] + constructor () // examples.classes/Outer.Nested.|(){}[0] + final inner class Inner { // examples.classes/Outer.Nested.Inner|null[0] + constructor () // examples.classes/Outer.Nested.Inner.|(){}[0] + } + } +} +final const val examples.classes/con // examples.classes/con|{}con[0] + final fun (): kotlin/String // examples.classes/con.|(){}[0] +final enum class examples.classes/E : kotlin/Enum { // examples.classes/E|null[0] + enum entry A // examples.classes/E.A|null[0] + enum entry B // examples.classes/E.B|null[0] + enum entry C // examples.classes/E.C|null[0] + final fun valueOf(kotlin/String): examples.classes/E // examples.classes/E.valueOf|valueOf#static(kotlin.String){}[0] + final fun values(): kotlin/Array // examples.classes/E.values|values#static(){}[0] + final val entries // examples.classes/E.entries|#static{}entries[0] + final fun (): kotlin.enums/EnumEntries // examples.classes/E.entries.|#static(){}[0] +} +final fun <#A: kotlin/Any?> examples.classes/consume(#A) // examples.classes/consume|consume(0:0){0§}[0] +final fun examples.classes/testFun(): kotlin/Int // examples.classes/testFun|testFun(){}[0] +final inline fun examples.classes/testInlineFun() // examples.classes/testInlineFun|testInlineFun(){}[0] +final object examples.classes/O // examples.classes/O|null[0] +final val examples.classes/l // examples.classes/l|{}l[0] + final fun (): kotlin/Long // examples.classes/l.|(){}[0] +final var examples.classes/r // examples.classes/r|{}r[0] + final fun (): kotlin/Float // examples.classes/r.|(){}[0] + final fun (kotlin/Float) // examples.classes/r.|(kotlin.Float){}[0] +open annotation class examples.classes/A : kotlin/Annotation { // examples.classes/A|null[0] + constructor () // examples.classes/A.|(){}[0] +} +open class examples.classes/OC { // examples.classes/OC|null[0] + constructor () // examples.classes/OC.|(){}[0] + final fun c() // examples.classes/OC.c|c(){}[0] + open fun o(): kotlin/Int // examples.classes/OC.o|o(){}[0] +} diff --git a/src/functionalTest/resources/examples/classes/TopLevelDeclarations.klib.with.linux.dump b/src/functionalTest/resources/examples/classes/TopLevelDeclarations.klib.with.linux.dump new file mode 100644 index 00000000..c7bb38f6 --- /dev/null +++ b/src/functionalTest/resources/examples/classes/TopLevelDeclarations.klib.with.linux.dump @@ -0,0 +1,67 @@ +// Klib ABI Dump +// Targets: [androidNativeArm32, androidNativeArm64, androidNativeX64, androidNativeX86, linuxArm64, linuxX64, mingwX64] +// Rendering settings: +// - Signature version: 2 +// - Show manifest properties: true +// - Show declarations: true + +// Library unique name: +abstract class examples.classes/AC { // examples.classes/AC|null[0] + abstract fun a() // examples.classes/AC.a|a(){}[0] + constructor () // examples.classes/AC.|(){}[0] + final fun b() // examples.classes/AC.b|b(){}[0] +} +abstract interface examples.classes/I // examples.classes/I|null[0] +final class examples.classes/C { // examples.classes/C|null[0] + constructor (kotlin/Any) // examples.classes/C.|(kotlin.Any){}[0] + final fun m() // examples.classes/C.m|m(){}[0] + final val v // examples.classes/C.v|{}v[0] + final fun (): kotlin/Any // examples.classes/C.v.|(){}[0] +} +final class examples.classes/D { // examples.classes/D|null[0] + constructor (kotlin/Int) // examples.classes/D.|(kotlin.Int){}[0] + final fun component1(): kotlin/Int // examples.classes/D.component1|component1(){}[0] + final fun copy(kotlin/Int =...): examples.classes/D // examples.classes/D.copy|copy(kotlin.Int){}[0] + final fun equals(kotlin/Any?): kotlin/Boolean // examples.classes/D.equals|equals(kotlin.Any?){}[0] + final fun hashCode(): kotlin/Int // examples.classes/D.hashCode|hashCode(){}[0] + final fun toString(): kotlin/String // examples.classes/D.toString|toString(){}[0] + final val x // examples.classes/D.x|{}x[0] + final fun (): kotlin/Int // examples.classes/D.x.|(){}[0] +} +final class examples.classes/Outer { // examples.classes/Outer|null[0] + constructor () // examples.classes/Outer.|(){}[0] + final class Nested { // examples.classes/Outer.Nested|null[0] + constructor () // examples.classes/Outer.Nested.|(){}[0] + final inner class Inner { // examples.classes/Outer.Nested.Inner|null[0] + constructor () // examples.classes/Outer.Nested.Inner.|(){}[0] + } + } +} +final const val examples.classes/con // examples.classes/con|{}con[0] + final fun (): kotlin/String // examples.classes/con.|(){}[0] +final enum class examples.classes/E : kotlin/Enum { // examples.classes/E|null[0] + enum entry A // examples.classes/E.A|null[0] + enum entry B // examples.classes/E.B|null[0] + enum entry C // examples.classes/E.C|null[0] + final fun valueOf(kotlin/String): examples.classes/E // examples.classes/E.valueOf|valueOf#static(kotlin.String){}[0] + final fun values(): kotlin/Array // examples.classes/E.values|values#static(){}[0] + final val entries // examples.classes/E.entries|#static{}entries[0] + final fun (): kotlin.enums/EnumEntries // examples.classes/E.entries.|#static(){}[0] +} +final fun <#A: kotlin/Any?> examples.classes/consume(#A) // examples.classes/consume|consume(0:0){0§}[0] +final fun examples.classes/testFun(): kotlin/Int // examples.classes/testFun|testFun(){}[0] +final inline fun examples.classes/testInlineFun() // examples.classes/testInlineFun|testInlineFun(){}[0] +final object examples.classes/O // examples.classes/O|null[0] +final val examples.classes/l // examples.classes/l|{}l[0] + final fun (): kotlin/Long // examples.classes/l.|(){}[0] +final var examples.classes/r // examples.classes/r|{}r[0] + final fun (): kotlin/Float // examples.classes/r.|(){}[0] + final fun (kotlin/Float) // examples.classes/r.|(kotlin.Float){}[0] +open annotation class examples.classes/A : kotlin/Annotation { // examples.classes/A|null[0] + constructor () // examples.classes/A.|(){}[0] +} +open class examples.classes/OC { // examples.classes/OC|null[0] + constructor () // examples.classes/OC.|(){}[0] + final fun c() // examples.classes/OC.c|c(){}[0] + open fun o(): kotlin/Int // examples.classes/OC.o|o(){}[0] +} diff --git a/src/functionalTest/resources/examples/classes/TopLevelDeclarations.kt b/src/functionalTest/resources/examples/classes/TopLevelDeclarations.kt new file mode 100644 index 00000000..067bd288 --- /dev/null +++ b/src/functionalTest/resources/examples/classes/TopLevelDeclarations.kt @@ -0,0 +1,38 @@ +/* + * Copyright 2016-2023 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 examples.classes + +public fun testFun(): Int = 42 +public fun consume(arg: T) = Unit +public inline fun testInlineFun() = Unit +public const val con: String = "I'm a constant!" +public val l: Long = 0xc001 +public var r: Float = 3.14f + +public annotation class A +public interface I +public data class D(val x: Int) +public class C(public val v: Any) { + public fun m() = Unit +} + +public object O +public enum class E { A, B, C } +public abstract class AC { + public abstract fun a() + public fun b() = Unit +} +public open class OC { + public open fun o(): Int = 42 + public fun c() = Unit +} +public class Outer { + public class Nested { + public inner class Inner { + + } + } +} diff --git a/src/functionalTest/resources/examples/gradle/base/enableJvmInWithNativePlugin.gradle.kts b/src/functionalTest/resources/examples/gradle/base/enableJvmInWithNativePlugin.gradle.kts new file mode 100644 index 00000000..439d731f --- /dev/null +++ b/src/functionalTest/resources/examples/gradle/base/enableJvmInWithNativePlugin.gradle.kts @@ -0,0 +1,9 @@ +/* + * Copyright 2016-2023 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. + */ + +kotlin { + jvm { + } +} diff --git a/src/functionalTest/resources/examples/gradle/base/multiplatformWithJvmTargets.gradle.kts b/src/functionalTest/resources/examples/gradle/base/multiplatformWithJvmTargets.gradle.kts index 8762a653..41189756 100644 --- a/src/functionalTest/resources/examples/gradle/base/multiplatformWithJvmTargets.gradle.kts +++ b/src/functionalTest/resources/examples/gradle/base/multiplatformWithJvmTargets.gradle.kts @@ -21,6 +21,9 @@ kotlin { testRuns["test"].executionTask.configure { useJUnit() } + attributes { + attribute(Attribute.of("variant", String::class.java), "a") + } } jvm("anotherJvm") { compilations.all { @@ -29,6 +32,9 @@ kotlin { testRuns["test"].executionTask.configure { useJUnit() } + attributes { + attribute(Attribute.of("variant", String::class.java), "b") + } } } sourceSets { diff --git a/src/functionalTest/resources/examples/gradle/base/withNativePlugin.gradle.kts b/src/functionalTest/resources/examples/gradle/base/withNativePlugin.gradle.kts new file mode 100644 index 00000000..ed1e174c --- /dev/null +++ b/src/functionalTest/resources/examples/gradle/base/withNativePlugin.gradle.kts @@ -0,0 +1,38 @@ +/* + * Copyright 2016-2023 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 "1.9.10" + 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/functionalTest/resources/examples/gradle/base/withNativePluginAndNoTargets.gradle.kts b/src/functionalTest/resources/examples/gradle/base/withNativePluginAndNoTargets.gradle.kts new file mode 100644 index 00000000..50668766 --- /dev/null +++ b/src/functionalTest/resources/examples/gradle/base/withNativePluginAndNoTargets.gradle.kts @@ -0,0 +1,30 @@ +/* + * Copyright 2016-2023 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 "1.9.10" + id("org.jetbrains.kotlinx.binary-compatibility-validator") +} + +repositories { + mavenCentral() +} + +kotlin { + 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/functionalTest/resources/examples/gradle/base/withNativePluginAndSingleTarget.gradle.kts b/src/functionalTest/resources/examples/gradle/base/withNativePluginAndSingleTarget.gradle.kts new file mode 100644 index 00000000..db44005d --- /dev/null +++ b/src/functionalTest/resources/examples/gradle/base/withNativePluginAndSingleTarget.gradle.kts @@ -0,0 +1,32 @@ +/* + * Copyright 2016-2023 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 "1.9.10" + id("org.jetbrains.kotlinx.binary-compatibility-validator") +} + +repositories { + mavenCentral() +} + +kotlin { + linuxArm64() + + 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/functionalTest/resources/examples/gradle/configuration/appleTargets/targets.gradle.kts b/src/functionalTest/resources/examples/gradle/configuration/appleTargets/targets.gradle.kts new file mode 100644 index 00000000..c3384eb8 --- /dev/null +++ b/src/functionalTest/resources/examples/gradle/configuration/appleTargets/targets.gradle.kts @@ -0,0 +1,20 @@ +/* + * Copyright 2016-2023 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. + */ + +kotlin { + macosX64() + macosArm64() + iosX64() + iosArm64() + iosSimulatorArm64() + tvosX64() + tvosArm64() + tvosSimulatorArm64() + watchosArm32() + watchosArm64() + watchosX64() + watchosSimulatorArm64() + watchosDeviceArm64() +} diff --git a/src/functionalTest/resources/examples/gradle/configuration/grouping/clashingTargetNames.gradle.kts b/src/functionalTest/resources/examples/gradle/configuration/grouping/clashingTargetNames.gradle.kts new file mode 100644 index 00000000..94581efc --- /dev/null +++ b/src/functionalTest/resources/examples/gradle/configuration/grouping/clashingTargetNames.gradle.kts @@ -0,0 +1,14 @@ +/* + * 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. + */ + +kotlin { + linuxX64("linux") + linuxArm64() + mingwX64() + androidNativeArm32() + androidNativeArm64() + androidNativeX64() + androidNativeX86() +} diff --git a/src/functionalTest/resources/examples/gradle/configuration/grouping/customTargetNames.gradle.kts b/src/functionalTest/resources/examples/gradle/configuration/grouping/customTargetNames.gradle.kts new file mode 100644 index 00000000..73aafef9 --- /dev/null +++ b/src/functionalTest/resources/examples/gradle/configuration/grouping/customTargetNames.gradle.kts @@ -0,0 +1,17 @@ +/* + * 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. + */ + +kotlin { + linuxX64("linuxA") { + attributes { + attribute(Attribute.of("variant", String::class.java), "a") + } + } + linuxX64("linuxB") { + attributes { + attribute(Attribute.of("variant", String::class.java), "b") + } + } +} diff --git a/src/functionalTest/resources/examples/gradle/configuration/ignoreSubclasses/ignore.gradle.kts b/src/functionalTest/resources/examples/gradle/configuration/ignoreSubclasses/ignore.gradle.kts new file mode 100644 index 00000000..776e96fd --- /dev/null +++ b/src/functionalTest/resources/examples/gradle/configuration/ignoreSubclasses/ignore.gradle.kts @@ -0,0 +1,9 @@ +/* + * Copyright 2016-2023 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. + */ + +configure { + // ignoredClasses.add("subclasses.A.B") + ignoredClasses.add("subclasses.A\$B") +} diff --git a/src/functionalTest/resources/examples/gradle/configuration/nonNativeKlibTargets/targets.gradle.kts b/src/functionalTest/resources/examples/gradle/configuration/nonNativeKlibTargets/targets.gradle.kts new file mode 100644 index 00000000..f6030484 --- /dev/null +++ b/src/functionalTest/resources/examples/gradle/configuration/nonNativeKlibTargets/targets.gradle.kts @@ -0,0 +1,10 @@ +/* + * 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. + */ + +kotlin { + wasmWasi() + wasmJs() + js() +} diff --git a/src/functionalTest/resources/examples/gradle/configuration/nonPublicMarkers/klib.gradle.kts b/src/functionalTest/resources/examples/gradle/configuration/nonPublicMarkers/klib.gradle.kts new file mode 100644 index 00000000..e8533816 --- /dev/null +++ b/src/functionalTest/resources/examples/gradle/configuration/nonPublicMarkers/klib.gradle.kts @@ -0,0 +1,13 @@ +/* + * Copyright 2016-2023 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. + */ + +configure { + nonPublicMarkers.add("annotations.HiddenClass") + nonPublicMarkers.add("annotations.HiddenCtor") + nonPublicMarkers.add("annotations.HiddenProperty") + nonPublicMarkers.add("annotations.HiddenGetter") + nonPublicMarkers.add("annotations.HiddenSetter") + nonPublicMarkers.add("annotations.HiddenFunction") +} diff --git a/src/functionalTest/resources/examples/gradle/configuration/signatures/invalid.gradle.kts b/src/functionalTest/resources/examples/gradle/configuration/signatures/invalid.gradle.kts new file mode 100644 index 00000000..ddb2f310 --- /dev/null +++ b/src/functionalTest/resources/examples/gradle/configuration/signatures/invalid.gradle.kts @@ -0,0 +1,10 @@ +/* + * Copyright 2016-2023 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. + */ + +configure { + klib { + signatureVersion = kotlinx.validation.api.klib.KlibSignatureVersion.of(100500) + } +} diff --git a/src/functionalTest/resources/examples/gradle/configuration/signatures/v1.gradle.kts b/src/functionalTest/resources/examples/gradle/configuration/signatures/v1.gradle.kts new file mode 100644 index 00000000..28f0322f --- /dev/null +++ b/src/functionalTest/resources/examples/gradle/configuration/signatures/v1.gradle.kts @@ -0,0 +1,8 @@ +/* + * Copyright 2016-2023 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. + */ + +configure { + klib.signatureVersion = kotlinx.validation.api.klib.KlibSignatureVersion.of(1) +} diff --git a/src/functionalTest/resources/examples/gradle/configuration/unsupported/enforce.gradle.kts b/src/functionalTest/resources/examples/gradle/configuration/unsupported/enforce.gradle.kts new file mode 100644 index 00000000..99270cde --- /dev/null +++ b/src/functionalTest/resources/examples/gradle/configuration/unsupported/enforce.gradle.kts @@ -0,0 +1,8 @@ +/* + * Copyright 2016-2023 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. + */ + +configure { + klib.strictValidation = true +} diff --git a/src/main/kotlin/ApiValidationExtension.kt b/src/main/kotlin/ApiValidationExtension.kt index ed09ba42..7a879775 100644 --- a/src/main/kotlin/ApiValidationExtension.kt +++ b/src/main/kotlin/ApiValidationExtension.kt @@ -5,6 +5,8 @@ package kotlinx.validation +import kotlinx.validation.api.klib.KlibSignatureVersion + public open class ApiValidationExtension { /** @@ -71,4 +73,51 @@ public open class ApiValidationExtension { * By default, it's `api`. */ public var apiDumpDirectory: String = "api" + + /** + * KLib ABI validation settings. + * + * @see KlibValidationSettings + */ + @ExperimentalBCVApi + public val klib: KlibValidationSettings = KlibValidationSettings() + + /** + * Configure KLib ABI validation settings. + */ + @ExperimentalBCVApi + public fun klib(block: KlibValidationSettings.() -> Unit) { + block(this.klib) + } +} + +/** + * Settings affecting KLib ABI validation. + */ +@ExperimentalBCVApi +public open class KlibValidationSettings { + /** + * Enables KLib ABI validation checks. + */ + public var enabled: Boolean = false + /** + * Specifies which version of signature KLib ABI dump should contain. + * By default, or when explicitly set to null, the latest supported version will be used. + * + * This option covers some advanced scenarios and does not require any configuration by default. + * + * A linker uses signatures to look up symbols, thus signature changes brake binary compatibility and + * should be tracked. Signature format itself is not stabilized yet and may change in the future. In that case, + * a new version of a signature will be introduced. Change of a signature version will be reflected in a dump + * causing a validation failure even if declarations itself remained unchanged. + * However, if a klib supports multiple signature versions simultaneously, one my explicitly specify the version + * that will be dumped to prevent changes in a dump file. + */ + public var signatureVersion: KlibSignatureVersion = KlibSignatureVersion.LATEST + /** + * Fail validation if some build targets are not supported by the host compiler. + * By default, ABI dumped only for supported files will be validated. This option makes validation behavior + * stricter and treats having unsupported targets as an error. + */ + public var strictValidation: Boolean = false } diff --git a/src/main/kotlin/BinaryCompatibilityValidatorPlugin.kt b/src/main/kotlin/BinaryCompatibilityValidatorPlugin.kt index c117659f..1548c4db 100644 --- a/src/main/kotlin/BinaryCompatibilityValidatorPlugin.kt +++ b/src/main/kotlin/BinaryCompatibilityValidatorPlugin.kt @@ -1,18 +1,27 @@ /* - * Copyright 2016-2023 JetBrains s.r.o. + * Copyright 2016-2024 JetBrains s.r.o. * Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file. */ 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.* 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 import java.io.* +@OptIn(ExperimentalBCVApi::class, ExperimentalLibraryAbiReader::class) public class BinaryCompatibilityValidatorPlugin : Plugin { override fun apply(target: Project): Unit = with(target) { @@ -30,9 +39,21 @@ public class BinaryCompatibilityValidatorPlugin : Plugin { for (project in ignored) { require(project in all) { "Cannot find excluded project $project in all projects: $all" } } + if (extension.klib.enabled) { + try { + LibraryAbiReader.javaClass + } catch (e: NoClassDefFoundError) { + throw IllegalStateException( + "KLib validation is not available. " + + "Make sure the project uses at least Kotlin 1.9.20 or disable KLib validation " + + "by setting apiValidation.klib.enabled to false", e + ) + } + } } } + @OptIn(ExperimentalBCVApi::class) private fun configureProject(project: Project, extension: ApiValidationExtension) { configureKotlinPlugin(project, extension) configureAndroidPlugin(project, extension) @@ -54,7 +75,7 @@ public class BinaryCompatibilityValidatorPlugin : Plugin { extension: ApiValidationExtension ) = configurePlugin("kotlin-multiplatform", project, extension) { if (project.name in extension.ignoredProjects) return@configurePlugin - val kotlin = project.extensions.getByName("kotlin") as KotlinMultiplatformExtension + val kotlin = project.kotlinMultiplatform // Create common tasks for multiplatform val commonApiDump = project.tasks.register("apiDump") { @@ -76,16 +97,15 @@ public class BinaryCompatibilityValidatorPlugin : Plugin { } } - val dirConfig = jvmTargetCountProvider.map { + val jvmDirConfig = jvmTargetCountProvider.map { if (it == 1) DirConfig.COMMON else DirConfig.TARGET_DIR } + val klibDirConfig = project.provider { DirConfig.COMMON } - kotlin.targets.matching { - it.platformType == KotlinPlatformType.jvm || it.platformType == KotlinPlatformType.androidJvm - }.all { target -> - val targetConfig = TargetConfig(project, extension, target.name, dirConfig) + kotlin.targets.matching { it.jvmBased }.all { target -> + val targetConfig = TargetConfig(project, extension, target.name, jvmDirConfig) if (target.platformType == KotlinPlatformType.jvm) { - target.compilations.matching { it.name == "main" }.all { + target.mainCompilations.all { project.configureKotlinCompilation(it, extension, targetConfig, commonApiDump, commonApiCheck) } } else if (target.platformType == KotlinPlatformType.androidJvm) { @@ -101,6 +121,7 @@ public class BinaryCompatibilityValidatorPlugin : Plugin { } } } + KlibValidationPipelineBuilder(klibDirConfig, extension).configureTasks(project, commonApiDump, commonApiCheck) } private fun configureAndroidPlugin( @@ -180,7 +201,7 @@ private enum class DirConfig { * the resulting paths will be * `/api/jvm/project.api` and `/api/android/project.api` */ - TARGET_DIR, + TARGET_DIR } private fun Project.configureKotlinCompilation( @@ -192,13 +213,17 @@ private fun Project.configureKotlinCompilation( useOutput: Boolean = false, ) { val projectName = project.name + val dumpFileName = project.jvmDumpFileName val apiDirProvider = targetConfig.apiDir val apiBuildDir = apiDirProvider.map { layout.buildDirectory.asFile.get().resolve(it) } val apiBuild = task(targetConfig.apiTaskName("Build")) { // Do not enable task for empty umbrella modules isEnabled = - apiCheckEnabled(projectName, extension) && compilation.allKotlinSourceSets.any { it.kotlin.srcDirs.any { it.exists() } } + apiCheckEnabled( + projectName, + extension + ) && compilation.allKotlinSourceSets.any { it.kotlin.srcDirs.any { it.exists() } } // '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" @@ -214,7 +239,7 @@ private fun Project.configureKotlinCompilation( inputDependencies = files(provider { if (isEnabled) compilation.compileDependencyFiles else emptyList() }) } - outputApiDir = apiBuildDir.get() + outputApiFile = apiBuildDir.get().resolve(dumpFileName) } configureCheckTasks(apiBuildDir, apiBuild, extension, targetConfig, commonApiDump, commonApiCheck) } @@ -231,11 +256,16 @@ internal val Project.apiValidationExtensionOrNull: ApiValidationExtension? private fun apiCheckEnabled(projectName: String, extension: ApiValidationExtension): Boolean = projectName !in extension.ignoredProjects && !extension.validationDisabled +@OptIn(ExperimentalBCVApi::class) +private fun klibAbiCheckEnabled(projectName: String, extension: ApiValidationExtension): Boolean = + projectName !in extension.ignoredProjects && !extension.validationDisabled && extension.klib.enabled + private fun Project.configureApiTasks( extension: ApiValidationExtension, targetConfig: TargetConfig = TargetConfig(this, extension), ) { val projectName = project.name + val dumpFileName = project.jvmDumpFileName val apiBuildDir = targetConfig.apiDir.map { layout.buildDirectory.asFile.get().resolve(it) } val sourceSetsOutputsProvider = project.provider { sourceSets @@ -249,8 +279,9 @@ private fun Project.configureApiTasks( 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() }) - outputApiDir = apiBuildDir.get() + inputDependencies = + files(provider { if (isEnabled) sourceSetsOutputsProvider.get() else emptyList() }) + outputApiFile = apiBuildDir.get().resolve(dumpFileName) } configureCheckTasks(apiBuildDir, apiBuild, extension, targetConfig) @@ -258,7 +289,7 @@ private fun Project.configureApiTasks( private fun Project.configureCheckTasks( apiBuildDir: Provider, - apiBuild: TaskProvider, + apiBuild: TaskProvider<*>, extension: ApiValidationExtension, targetConfig: TargetConfig, commonApiDump: TaskProvider? = null, @@ -274,16 +305,18 @@ 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" - compareApiDumps(apiReferenceDir = apiCheckDir.get(), apiBuildDir = apiBuildDir.get()) + projectApiFile = apiCheckDir.get().resolve(jvmDumpFileName) + generatedApiFile = apiBuildDir.get().resolve(jvmDumpFileName) dependsOn(apiBuild) } - val apiDump = task(targetConfig.apiTaskName("Dump")) { + val dumpFileName = project.jvmDumpFileName + val apiDump = task(targetConfig.apiTaskName("Dump")) { isEnabled = apiCheckEnabled(projectName, extension) && apiBuild.map { it.enabled }.getOrElse(true) group = "other" description = "Syncs API from build dir to ${targetConfig.apiDir} dir for $projectName" - from(apiBuildDir) - into(apiCheckDir) + from = apiBuildDir.get().resolve(dumpFileName) + to = apiCheckDir.get().resolve(dumpFileName) dependsOn(apiBuild) } @@ -299,3 +332,354 @@ private inline fun Project.task( name: String, noinline configuration: T.() -> Unit, ): TaskProvider = tasks.register(name, T::class.java, Action(configuration)) + +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" + +/** + * KLib ABI dump validation and dump extraction consists of multiple steps that extracts and transforms dumps for klibs. + * The main entry point for validation is the `klibApiCheck` task, which is a dependency for `apiCheck` task, and the + * main entry point for dump extraction is the `klibApiDump` task, which is a dependency for `apiDump` task. + * + * Both `klibApiCheck` and `klibApiDump` depends on multiple other tasks that extracts dumps for compiled klibs, + * generate (in case of dumping) dumps for targets that are not supported by the host compiler and don't have compiled + * klibs, and, finally, merges individual dumps into a single merged KLib ABI dump file that is then either stored + * inside a project's api dir (in case of dumping), or compared against a golden value (in case of validation). + * + * 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 + * 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 + * to merge: `klibApiMerge` uses only dump files extracted from compiled klibs, these dumps are extracted using + * 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 + * 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 + * target that were extracted from the golden image. + */ +@ExperimentalBCVApi +private class KlibValidationPipelineBuilder( + val dirConfig: Provider?, + val extension: ApiValidationExtension +) { + lateinit var intermediateFilesConfig: Provider + + fun configureTasks(project: Project, commonApiDump: TaskProvider, commonApiCheck: TaskProvider) { + // In the intermediate phase of KLib dump generation, there are always multiple targets; thus we need + // a target-based directory tree. + intermediateFilesConfig = project.provider { DirConfig.TARGET_DIR } + val klibApiDirConfig = dirConfig?.map { TargetConfig(project, extension, KLIB_DUMPS_DIRECTORY, dirConfig) } + val klibDumpConfig = TargetConfig(project, extension, KLIB_DUMPS_DIRECTORY, intermediateFilesConfig) + val klibInferDumpConfig = + TargetConfig(project, extension, KLIB_INFERRED_DUMPS_DIRECTORY, intermediateFilesConfig) + + val projectDir = project.projectDir + val klibApiDir = klibApiDirConfig?.map { + projectDir.resolve(it.apiDir.get()) + }!! + 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 klibMerge = project.mergeKlibsUmbrellaTask(klibDumpConfig, klibMergeDir) + val klibMergeInferred = project.mergeInferredKlibsUmbrellaTask(klibDumpConfig, klibMergeInferredDir) + val klibDump = project.dumpKlibsTask(klibDumpConfig, klibApiDir, klibMergeInferredDir) + 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) } + klibCheck.configure { + it.dependsOn(klibExtractAbiForSupportedTargets) + it.dependsOn(klibMerge) + } + + 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) + } + + private fun Project.dumpKlibsTask( + klibDumpConfig: TargetConfig, + klibApiDir: Provider, + klibMergeDir: File + ) = project.task(klibDumpConfig.apiTaskName("Dump")) { + isEnabled = klibAbiCheckEnabled(project.name, extension) + description = "Syncs a KLib ABI dump from a build dir to the ${klibDumpConfig.apiDir} dir for ${project.name}" + group = "other" + from = klibMergeDir.resolve(klibDumpFileName) + to = klibApiDir.get().resolve(klibDumpFileName) + } + + private fun Project.extractAbi( + klibDumpConfig: TargetConfig, + klibApiDir: Provider, + klibOutputDir: File + ) = project.task( + klibDumpConfig.apiTaskName("ExtractForValidation") + ) + { + isEnabled = klibAbiCheckEnabled(project.name, extension) + 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) + } + + private fun Project.mergeInferredKlibsUmbrellaTask( + klibDumpConfig: TargetConfig, + klibMergeDir: File, + ) = project.task( + klibDumpConfig.apiTaskName("MergeInferred") + ) + { + isEnabled = klibAbiCheckEnabled(project.name, extension) + 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) + } + + private fun Project.mergeKlibsUmbrellaTask( + klibDumpConfig: TargetConfig, + klibMergeDir: File + ) = 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) + } + + fun Project.bannedTargets(): Set { + val prop = project.properties[BANNED_TARGETS_PROPERTY_NAME] as String? + prop ?: return emptySet() + return prop.split(",").map { it.trim() }.toSet().also { + if (it.isNotEmpty()) { + project.logger.warn( + "WARNING: Following property is not empty: $BANNED_TARGETS_PROPERTY_NAME. " + + "If you're don't know what it means, please make sure that its value is empty." + ) + } + } + } + + fun Project.configureTargets( + klibApiDir: Provider, + mergeTask: TaskProvider, + mergeInferredTask: TaskProvider + ) { + val kotlin = project.kotlinMultiplatform + + val supportedTargetsProvider = supportedTargets() + kotlin.targets.matching { it.emitsKlib }.configureEach { currentTarget -> + val mainCompilations = currentTarget.mainCompilations + if (mainCompilations.none()) { + return@configureEach + } + + 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 targetSupported = targetIsSupported(currentTarget) + // If a target is supported, the workflow is simple: create a dump, then merge it along with other dumps. + if (targetSupported) { + mainCompilations.all { + val buildTargetAbi = configureKlibCompilation( + it, extension, targetConfig, + apiBuildDir + ) + mergeTask.configure { + it.addInput(targetName, apiBuildDir) + it.dependsOn(buildTargetAbi) + } + mergeInferredTask.configure { + it.addInput(targetName, apiBuildDir) + it.dependsOn(buildTargetAbi) + } + } + return@configureEach + } + // If the target is unsupported, the regular merge task will only depend on a task complaining about + // the target being unsupported. + val unsupportedTargetStub = mergeDependencyForUnsupportedTarget(targetConfig) + mergeTask.configure { + it.dependsOn(unsupportedTargetStub) + } + // The actual merge will happen here, where we'll try to infer a dump for the unsupported target and merge + // it with other supported target dumps. + val proxy = unsupportedTargetDumpProxy(klibApiDir, targetConfig, + extractUnderlyingTarget(currentTarget), + apiBuildDir, supportedTargetsProvider) + 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." + ) + } + } + } + } + + 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 + } + } + + 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 { + if (it is KotlinNativeTarget) { + hm.isEnabled(it.konanTarget) && it.targetName !in banned + } else { + true + } + } + .map { KlibTarget(extractUnderlyingTarget(it), it.targetName).toString() } + .toSet() + } + } + + + private fun Project.configureKlibCompilation( + compilation: KotlinCompilation, + extension: ApiValidationExtension, + targetConfig: TargetConfig, + apiBuildDir: File + ): 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 + ) && compilation.allKotlinSourceSets.any { it.kotlin.srcDirs.any { it.exists() } } + // '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) + } + return buildTask + } + + private fun Project.mergeDependencyForUnsupportedTarget(targetConfig: TargetConfig): TaskProvider { + return project.task(targetConfig.apiTaskName("Build")) { + isEnabled = apiCheckEnabled(project.name, extension) + + doLast { + logger.warn( + "Target ${targetConfig.targetName} is not supported by the host compiler and a " + + "KLib ABI dump could not be directly generated for it." + ) + } + } + } + + private fun Project.unsupportedTargetDumpProxy( + klibApiDir: Provider, + targetConfig: TargetConfig, + underlyingTarget: String, + apiBuildDir: File, + supportedTargets: Provider> + ): TaskProvider { + val targetName = targetConfig.targetName!! + return project.task(targetConfig.apiTaskName("Infer")) { + isEnabled = klibAbiCheckEnabled(project.name, extension) + 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)) + } + } +} + +private val KotlinTarget.emitsKlib: Boolean + get() { + val platformType = this.platformType + return platformType == KotlinPlatformType.native || + platformType == KotlinPlatformType.wasm || + platformType == KotlinPlatformType.js + } + +private val KotlinTarget.jvmBased: Boolean + get() { + val platformType = this.platformType + 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 + +private val KotlinTarget.mainCompilations + get() = compilations.matching { it.name == "main" } + +private val Project.jvmDumpFileName: String + get() = "$name.api" +private val Project.klibDumpFileName: String + get() = "$name.klib.api" diff --git a/src/main/kotlin/BuildTaskBase.kt b/src/main/kotlin/BuildTaskBase.kt new file mode 100644 index 00000000..a042f2d8 --- /dev/null +++ b/src/main/kotlin/BuildTaskBase.kt @@ -0,0 +1,58 @@ +/* + * Copyright 2016-2023 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 org.gradle.api.DefaultTask +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 var _ignoredPackages: Set? = null + @get:Input + public var ignoredPackages : Set + get() = _ignoredPackages ?: extension?.ignoredPackages ?: emptySet() + set(value) { _ignoredPackages = value } + + private var _nonPublicMarkes: Set? = null + @get:Input + public var nonPublicMarkers : Set + get() = _nonPublicMarkes ?: extension?.nonPublicMarkers ?: emptySet() + set(value) { _nonPublicMarkes = value } + + private var _ignoredClasses: Set? = null + @get:Input + public var ignoredClasses : Set + get() = _ignoredClasses ?: extension?.ignoredClasses ?: emptySet() + set(value) { _ignoredClasses = value } + + private var _publicPackages: Set? = null + @get:Input + public var publicPackages: Set + get() = _publicPackages ?: extension?.publicPackages ?: emptySet() + set(value) { _publicPackages = value } + + private var _publicMarkers: Set? = null + @get:Input + public var publicMarkers: Set + get() = _publicMarkers ?: extension?.publicMarkers ?: emptySet() + set(value) { _publicMarkers = value} + + private var _publicClasses: Set? = null + @get:Input + public var publicClasses: Set + get() = _publicClasses ?: extension?.publicClasses ?: emptySet() + set(value) { _publicClasses = value } + + @get:Internal + internal val projectName = project.name +} diff --git a/src/main/kotlin/CopyFile.kt b/src/main/kotlin/CopyFile.kt new file mode 100644 index 00000000..df9f6d79 --- /dev/null +++ b/src/main/kotlin/CopyFile.kt @@ -0,0 +1,31 @@ +/* + * 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 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 java.nio.file.Files +import java.nio.file.StandardCopyOption + +// Built-in Gradle's Copy/Sync tasks accepts only a destination directory (not a single file) +// 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 + + @OutputFile + lateinit var to: File + + @TaskAction + fun copy() { + Files.copy(from.toPath(), to.toPath(), StandardCopyOption.REPLACE_EXISTING) + } +} diff --git a/src/main/kotlin/ExperimentalBCVApi.kt b/src/main/kotlin/ExperimentalBCVApi.kt new file mode 100644 index 00000000..7258943a --- /dev/null +++ b/src/main/kotlin/ExperimentalBCVApi.kt @@ -0,0 +1,14 @@ +/* + * 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 + +/** + * Marks an API that is still experimental in Binary compatibility validator and may change + * in the future. There are also no guarantees on preserving the behavior of the API until its + * stabilization. + */ +@RequiresOptIn(level = RequiresOptIn.Level.WARNING) +public annotation class ExperimentalBCVApi diff --git a/src/main/kotlin/KotlinApiBuildTask.kt b/src/main/kotlin/KotlinApiBuildTask.kt index 0df744bb..70881de4 100644 --- a/src/main/kotlin/KotlinApiBuildTask.kt +++ b/src/main/kotlin/KotlinApiBuildTask.kt @@ -1,5 +1,5 @@ /* - * Copyright 2016-2020 JetBrains s.r.o. + * Copyright 2016-2024 JetBrains s.r.o. * Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file. */ @@ -9,14 +9,11 @@ import kotlinx.validation.api.* import org.gradle.api.* import org.gradle.api.file.* import org.gradle.api.tasks.* -import java.io.* import java.util.jar.JarFile import javax.inject.Inject public open class KotlinApiBuildTask @Inject constructor( -) : DefaultTask() { - - private val extension = project.apiValidationExtensionOrNull +) : BuildTaskBase() { @InputFiles @Optional @@ -32,52 +29,10 @@ public open class KotlinApiBuildTask @Inject constructor( @PathSensitive(PathSensitivity.RELATIVE) public lateinit var inputDependencies: FileCollection - @OutputDirectory - public lateinit var outputApiDir: File - - private var _ignoredPackages: Set? = null - @get:Input - internal var ignoredPackages : Set - get() = _ignoredPackages ?: extension?.ignoredPackages ?: emptySet() - set(value) { _ignoredPackages = value } - - private var _nonPublicMarkes: Set? = null - @get:Input - internal var nonPublicMarkers : Set - get() = _nonPublicMarkes ?: extension?.nonPublicMarkers ?: emptySet() - set(value) { _nonPublicMarkes = value } - - private var _ignoredClasses: Set? = null - @get:Input - internal var ignoredClasses : Set - get() = _ignoredClasses ?: extension?.ignoredClasses ?: emptySet() - set(value) { _ignoredClasses = value } - - private var _publicPackages: Set? = null - @get:Input - internal var publicPackages: Set - get() = _publicPackages ?: extension?.publicPackages ?: emptySet() - set(value) { _publicPackages = value } - - private var _publicMarkers: Set? = null - @get:Input - internal var publicMarkers: Set - get() = _publicMarkers ?: extension?.publicMarkers ?: emptySet() - set(value) { _publicMarkers = value} - - private var _publicClasses: Set? = null - @get:Input - internal var publicClasses: Set - get() = _publicClasses ?: extension?.publicClasses ?: emptySet() - set(value) { _publicClasses = value } - - @get:Internal - internal val projectName = project.name - @TaskAction internal fun generate() { - cleanup(outputApiDir) - outputApiDir.mkdirs() + outputApiFile.delete() + outputApiFile.parentFile.mkdirs() val inputClassesDirs = inputClassesDirs val signatures = when { @@ -104,28 +59,8 @@ public open class KotlinApiBuildTask @Inject constructor( .filterOutNonPublic(ignoredPackages + ignoredPackagesNames, ignoredClasses) .filterOutAnnotated(nonPublicMarkers.map(::replaceDots).toSet()) - outputApiDir.resolve("$projectName.api").bufferedWriter().use { writer -> - filteredSignatures - .sortedBy { it.name } - .forEach { api -> - writer.append(api.signature).appendLine(" {") - api.memberSignatures - .sortedWith(MEMBER_SORT_ORDER) - .forEach { writer.append("\t").appendLine(it.signature) } - writer.appendLine("}\n") - } - } - } - - private fun cleanup(file: File) { - if (file.exists()) { - val listing = file.listFiles() - if (listing != null) { - for (sub in listing) { - cleanup(sub) - } - } - file.delete() + outputApiFile.bufferedWriter().use { writer -> + filteredSignatures.dump(writer) } } } diff --git a/src/main/kotlin/KotlinApiCompareTask.kt b/src/main/kotlin/KotlinApiCompareTask.kt index ac4eed55..88e693ef 100644 --- a/src/main/kotlin/KotlinApiCompareTask.kt +++ b/src/main/kotlin/KotlinApiCompareTask.kt @@ -1,5 +1,5 @@ /* - * Copyright 2016-2020 JetBrains s.r.o. + * Copyright 2016-2024 JetBrains s.r.o. * Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file. */ @@ -17,40 +17,12 @@ import org.gradle.api.tasks.* public open class KotlinApiCompareTask @Inject constructor(private val objects: ObjectFactory): DefaultTask() { - /* - * Nullability and optionality is a workaround for - * https://github.com/gradle/gradle/issues/2016 - * - * Unfortunately, there is no way to skip validation apart from setting 'null' - */ - @Optional - @InputDirectory + @InputFiles @PathSensitive(PathSensitivity.RELATIVE) - public var projectApiDir: File? = null - - // Used for diagnostic error message when projectApiDir doesn't exist - @Input - @Optional - public var nonExistingProjectApiDir: String? = null - - internal fun compareApiDumps(apiReferenceDir: File, apiBuildDir: File) { - if (apiReferenceDir.exists()) { - projectApiDir = apiReferenceDir - } else { - projectApiDir = null - nonExistingProjectApiDir = apiReferenceDir.toString() - } - this.apiBuildDir = apiBuildDir - } - - @InputDirectory - @PathSensitive(PathSensitivity.RELATIVE) - public lateinit var apiBuildDir: File + public lateinit var projectApiFile: File - @OutputFile - @Optional - @Suppress("unused") - public val dummyOutputFile: File? = null + @InputFiles + public lateinit var generatedApiFile: File private val projectName = project.name @@ -58,10 +30,15 @@ public open class KotlinApiCompareTask @Inject constructor(private val objects: @TaskAction internal fun verify() { - val projectApiDir = projectApiDir - ?: error("Expected folder with API declarations '$nonExistingProjectApiDir' does not exist.\n" + + 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 /* @@ -72,35 +49,34 @@ public open class KotlinApiCompareTask @Inject constructor(private val objects: * To workaround that, we replace paths we are looking for the same paths that * actually exist on FS. */ - fun caseInsensitiveMap() = TreeMap { rp, rp2 -> - rp.toString().compareTo(rp2.toString(), true) + fun caseInsensitiveMap() = TreeMap { rp, rp2 -> + rp.compareTo(rp2, true) } val apiBuildDirFiles = caseInsensitiveMap() val expectedApiFiles = caseInsensitiveMap() - objects.fileTree().from(apiBuildDir).visit { file -> - apiBuildDirFiles[file.relativePath] = file.relativePath + objects.fileTree().from(buildApiDir).visit { file -> + apiBuildDirFiles[file.name] = file.relativePath } objects.fileTree().from(projectApiDir).visit { file -> - expectedApiFiles[file.relativePath] = file.relativePath - } - - if (apiBuildDirFiles.size != 1) { - error("Expected a single file $subject.api, but found: $expectedApiFiles") + expectedApiFiles[file.name] = file.relativePath } - var expectedApiDeclaration = apiBuildDirFiles.keys.single() - if (expectedApiDeclaration !in expectedApiFiles) { - error("File ${expectedApiDeclaration.lastName} is missing from ${projectApiDir.relativeDirPath()}, please run " + + if (!expectedApiFiles.containsKey(projectApiFile.name)) { + error("File ${projectApiFile.name} is missing from ${projectApiDir.relativeDirPath()}, please run " + ":$subject:apiDump task to generate one") } + if (!apiBuildDirFiles.containsKey(generatedApiFile.name)) { + error("File ${generatedApiFile.name} is missing from dump results.") + } + // Normalize case-sensitivity - expectedApiDeclaration = expectedApiFiles.getValue(expectedApiDeclaration) - val actualApiDeclaration = apiBuildDirFiles.getValue(expectedApiDeclaration) + val expectedApiDeclaration = expectedApiFiles.getValue(projectApiFile.name) + val actualApiDeclaration = apiBuildDirFiles.getValue(generatedApiFile.name) val diffSet = mutableSetOf() val expectedFile = expectedApiDeclaration.getFile(projectApiDir) - val actualFile = actualApiDeclaration.getFile(apiBuildDir) + val actualFile = actualApiDeclaration.getFile(buildApiDir) val diff = compareFiles(expectedFile, actualFile) if (diff != null) diffSet.add(diff) if (diffSet.isNotEmpty()) { diff --git a/src/main/kotlin/KotlinKlibAbiBuildTask.kt b/src/main/kotlin/KotlinKlibAbiBuildTask.kt new file mode 100644 index 00000000..c591c632 --- /dev/null +++ b/src/main/kotlin/KotlinKlibAbiBuildTask.kt @@ -0,0 +1,70 @@ +/* + * 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.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) +} + +/** + * Generates a text file with a KLib ABI dump for a single klib. + */ +internal abstract class KotlinKlibAbiBuildTask : BuildTaskBase() { + + /** + * Path to a klib to dump. + */ + @InputFiles + lateinit var klibFile: FileCollection + + /** + * Bind this task with a klib compilation. + */ + @InputFiles + lateinit var compilationDependencies: FileCollection + + /** + * Refer to [KlibValidationSettings.signatureVersion] for details. + */ + @Optional + @get:Input + var signatureVersion: SerializableSignatureVersion = SerializableSignatureVersion(KlibSignatureVersion.LATEST) + + /** + * Name of a target [klibFile] was compiled for. + */ + @Input + lateinit var target: String + + @OptIn(ExperimentalBCVApi::class) + @TaskAction + internal fun generate() { + outputApiFile.delete() + outputApiFile.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() + }) + + dump.saveTo(outputApiFile) + } +} diff --git a/src/main/kotlin/KotlinKlibExtractSupportedTargetsAbiTask.kt b/src/main/kotlin/KotlinKlibExtractSupportedTargetsAbiTask.kt new file mode 100644 index 00000000..58298a18 --- /dev/null +++ b/src/main/kotlin/KotlinKlibExtractSupportedTargetsAbiTask.kt @@ -0,0 +1,68 @@ +/* + * 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/KotlinKlibInferAbiForUnsupportedTargetTask.kt new file mode 100644 index 00000000..af0228f0 --- /dev/null +++ b/src/main/kotlin/KotlinKlibInferAbiForUnsupportedTargetTask.kt @@ -0,0 +1,133 @@ +/* + * 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.* +import kotlinx.validation.api.klib.TargetHierarchy +import org.gradle.api.DefaultTask +import org.gradle.api.provider.Provider +import org.gradle.api.tasks.* +import java.io.File + +/** + * Task infers a possible KLib ABI dump for an unsupported target. + * To infer a dump, tasks walk up the default targets hierarchy tree starting from the unsupported + * target until it finds a node corresponding to a group of targets having at least one supported target. + * After that, dumps generated for such supported targets are merged and declarations that are common to all + * of them are considered as a common ABI that most likely will be shared by the unsupported target. + * At the next step, if a project contains an old dump, declarations specific to the unsupported target are copied + * 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 + + /** + * The name of a target to infer a dump for. + */ + @Input + lateinit var unsupportedTargetCanonicalName: String + + /** + * A root directory containing dumps successfully generated for each supported target. + * It is assumed that this directory contains subdirectories named after targets. + */ + @InputFiles + lateinit var outputApiDir: String + + /** + * Set of all supported targets. + */ + @Input + lateinit var supportedTargets: Provider> + + /** + * 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 + + /** + * A path to an inferred dump file. + */ + @OutputFile + lateinit var outputFile: File + + @OptIn(ExperimentalBCVApi::class) + @TaskAction + internal fun generate() { + val unsupportedTarget = KlibTarget(unsupportedTargetCanonicalName, unsupportedTargetName) + val supportedTargetNames = supportedTargets.get().map { KlibTarget.parse(it) }.toSet() + // 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) + // Load dumps that are a good fit for inference + val supportedTargetDumps = matchingTargets.map { target -> + val dumpFile = File(outputApiDir).parentFile.resolve(target.configurableName).resolve(dumpFileName) + KlibDump.from(dumpFile, target.configurableName).also { + check(it.targets.single() == target) + } + } + + // Load an old dump, if any + var image: KlibDump? = null + if (inputImageFile.exists()) { + if (inputImageFile.length() > 0L) { + image = KlibDump.from(inputImageFile) + } else { + logger.warn( + "Project's ABI file exists, but empty: $inputImageFile. " + + "The file will be ignored during ABI dump inference for the unsupported target " + + unsupportedTarget + ) + } + } + + inferAbi(unsupportedTarget, supportedTargetDumps, image).saveTo(outputFile) + + logger.warn( + "An ABI dump for target $unsupportedTarget 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. " + + "It is recommended to regenerate the dump on the host supporting all required compilation target." + ) + } + + private fun findMatchingTargets( + supportedTargets: Set, + unsupportedTarget: KlibTarget + ): Collection { + var currentGroup: String? = unsupportedTarget.targetName + while (currentGroup != null) { + // If a current group has some supported targets, use them. + val groupTargets = TargetHierarchy.targets(currentGroup) + val matchingTargets = supportedTargets.filter { groupTargets.contains(it.targetName) } + if (matchingTargets.isNotEmpty()) { + return matchingTargets + } + // Otherwise, walk up the target hierarchy. + currentGroup = TargetHierarchy.parent(currentGroup) + } + throw IllegalStateException( + "The target $unsupportedTarget is not supported by the host compiler " + + "and there are no targets similar to $unsupportedTarget to infer a dump from it." + ) + } +} diff --git a/src/main/kotlin/KotlinKlibMergeAbiTask.kt b/src/main/kotlin/KotlinKlibMergeAbiTask.kt new file mode 100644 index 00000000..ebbf04e2 --- /dev/null +++ b/src/main/kotlin/KotlinKlibMergeAbiTask.kt @@ -0,0 +1,61 @@ +/* + * 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.saveTo +import org.gradle.api.DefaultTask +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 + + /** + * A path to a resulting merged dump. + */ + @OutputFile + lateinit var mergedFile: File + + /** + * The name of a dump file. + */ + @Input + lateinit var dumpFileName: String + + internal fun addInput(target: String, file: File) { + targetToFile[target] = file + } + + @OptIn(ExperimentalBCVApi::class) + @TaskAction + internal fun merge() { + KlibDump().apply { + targetToFile.forEach { (targetName, dumpDir) -> + merge(dumpDir.resolve(dumpFileName), targetName) + } + }.saveTo(mergedFile) + } +} diff --git a/src/main/kotlin/api/KotlinSignaturesLoading.kt b/src/main/kotlin/api/KotlinSignaturesLoading.kt index ebefee00..801926fb 100644 --- a/src/main/kotlin/api/KotlinSignaturesLoading.kt +++ b/src/main/kotlin/api/KotlinSignaturesLoading.kt @@ -313,15 +313,16 @@ public fun List.dump(): PrintStream = dump(to = System.out @ExternalApi public fun List.dump(to: T): T { - forEach { classApi -> - with(to) { - append(classApi.signature).appendLine(" {") - classApi.memberSignatures - .sortedWith(MEMBER_SORT_ORDER) - .forEach { append("\t").appendLine(it.signature) } - appendLine("}\n") + sortedBy { it.name } + .forEach { classApi -> + with(to) { + append(classApi.signature).appendLine(" {") + classApi.memberSignatures + .sortedWith(MEMBER_SORT_ORDER) + .forEach { append("\t").appendLine(it.signature) } + appendLine("}\n") + } } - } return to } diff --git a/src/main/kotlin/api/klib/KlibAbiDumpFileMerger.kt b/src/main/kotlin/api/klib/KlibAbiDumpFileMerger.kt new file mode 100644 index 00000000..a5eddc7e --- /dev/null +++ b/src/main/kotlin/api/klib/KlibAbiDumpFileMerger.kt @@ -0,0 +1,768 @@ +/* + * 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.api.klib + +import java.io.File +import java.nio.file.Files +import java.util.* +import kotlin.Comparator + +private class PeekingLineIterator(private val lines: Iterator) : Iterator { + private var nextLine: String? = null + + public fun peek(): String? { + if (nextLine != null) { + return nextLine + } + if (!lines.hasNext()) { + return null + } + nextLine = lines.next() + return nextLine + } + + override fun hasNext(): Boolean { + return nextLine != null || lines.hasNext() + } + + override fun next(): String { + if (nextLine != null) { + val res = nextLine!! + nextLine = null + return res + } + return lines.next() + } +} + +private const val MERGED_DUMP_FILE_HEADER = "// Klib ABI Dump" +private const val REGULAR_DUMP_FILE_HEADER = "// Rendering settings:" +private const val COMMENT_PREFIX = "//" +private const val TARGETS_LIST_PREFIX = "// Targets: [" +private const val TARGETS_LIST_SUFFIX = "]" +private const val TARGETS_DELIMITER = ", " +private const val CLASS_DECLARATION_TERMINATOR = "}" +private const val INDENT_WIDTH = 4 +private const val ALIAS_PREFIX = "// Alias: " +private const val PLATFORM_PREFIX = "// Platform: " +private const val NATIVE_TARGETS_PREFIX = "// Native targets: " +private const val LIBRARY_NAME_PREFIX = "// Library unique name:" + +private fun String.depth(): Int { + val indentation = this.takeWhile { it == ' ' }.count() + require(indentation % INDENT_WIDTH == 0) { + "Unexpected indentation, should be a multiple of $INDENT_WIDTH: $this" + } + return indentation / INDENT_WIDTH +} + +private fun parseBcvTargetsLine(line: String): Set { + val trimmedLine = line.trimStart(' ') + check(trimmedLine.startsWith(TARGETS_LIST_PREFIX) && trimmedLine.endsWith(TARGETS_LIST_SUFFIX)) { + "Not a targets list line: \"$line\"" + } + return trimmedLine.substring(TARGETS_LIST_PREFIX.length, trimmedLine.length - 1) + .split(TARGETS_DELIMITER) + .map { KlibTarget.parse(it) } + .toSet() +} + +private class KlibAbiDumpHeader( + val content: List, + val underlyingTargets: Set +) { + constructor(content: List, underlyingTarget: KlibTarget) : this(content, setOf(underlyingTarget)) +} + +/** + * A class representing a textual KLib ABI dump, either a regular one, or a merged. + */ +internal class KlibAbiDumpMerger { + private val _targets: MutableSet = mutableSetOf() + private val headerContent: MutableList = mutableListOf() + private val topLevelDeclaration: DeclarationContainer = DeclarationContainer("") + + /** + * All targets for which this dump contains declarations. + */ + internal val targets: Set = _targets + + internal fun merge(file: File, configurableTargetName: String? = null) { + require(file.exists()) { "File does not exist: $file" } + // TODO: replace with file.toPath().useLines once language version get upgraded + Files.lines(file.toPath()).use { + merge(it.iterator(), configurableTargetName) + } + } + + internal fun merge(lines: Iterator, configurableTargetName: String? = null) { + merge(PeekingLineIterator(lines), configurableTargetName) + } + + private fun merge(lines: PeekingLineIterator, configurableTargetName: String?) { + require(lines.peek() != null) { "File is empty" } + val isMergedFile = lines.determineFileType() + + val aliases = mutableMapOf>() + val bcvTargets = mutableSetOf() + if (isMergedFile) { + lines.next() // skip the heading line + bcvTargets.addAll(lines.parseTargets(configurableTargetName)) + check(bcvTargets.size == 1 || configurableTargetName == null) { + "Can't use an explicit target name with a multi-target dump. " + + "targetName: $configurableTargetName, dump targets: $bcvTargets" + } + aliases.putAll(lines.parseAliases()) + } + val header = lines.parseFileHeader(isMergedFile, configurableTargetName) + bcvTargets.addAll(header.underlyingTargets) + bcvTargets.intersect(targets).also { + check(it.isEmpty()) { "This dump and a file to merge share some targets: $it" } + } + + if (this._targets.isEmpty()) { + headerContent.addAll(header.content) + } else if (headerContent != header.content) { + throw IllegalStateException( + "File header doesn't match the header of other files\n" + + headerContent.toString() + "\n\n\n" + header.content.toString() + ) + } + this._targets.addAll(bcvTargets) + topLevelDeclaration.targets.addAll(bcvTargets) + + // All declarations belonging to the same scope have equal indentation. + // Nested declarations have higher indentation. + // By tracking the indentation, we can decide if the line should be added into the current container, + // to its parent container (i.e., the line represents sibling declaration) or the current declaration ended, + // and we must pop one or several declarations out of the parsing stack. + var currentContainer = topLevelDeclaration + var depth = -1 + val targetsStack = mutableListOf>().apply { add(bcvTargets) } + + while (lines.hasNext()) { + val line = lines.peek()!! + if (line.isEmpty()) { lines.next(); continue } + // TODO: wrap the line and cache the depth inside that wrapper? + val lineDepth = line.depth() + when { + // The depth is the same as before; we encountered a sibling + depth == lineDepth -> { + // pop it off to swap previous value from the same depth, + // parseDeclaration will update it + targetsStack.removeLast() + currentContainer = + lines.parseDeclaration(lineDepth, currentContainer.parent!!, targetsStack, aliases) + } + // The depth is increasing; that means we encountered child declaration + depth < lineDepth -> { + check(lineDepth - depth == 1) { + "The line has too big indentation relative to a previous line\nline: $line\n" + + "previous: ${currentContainer.text}" + } + currentContainer = + lines.parseDeclaration(lineDepth, currentContainer, targetsStack, aliases) + depth = lineDepth + } + // Otherwise, we're finishing all the declaration with greater depth compared to the depth of + // the next line. + // We won't process a line if it contains a new declaration here, just update the depth and current + // declaration reference to process the new declaration on the next iteration. + else -> { + while (currentContainer.text.depth() > lineDepth) { + currentContainer = currentContainer.parent!! + targetsStack.removeLast() + } + // If the line is '}' - add it as a terminator to the corresponding declaration, it'll simplify + // dumping the merged file back to text format. + if (line.trim() == CLASS_DECLARATION_TERMINATOR) { + currentContainer.delimiter = line + // We processed the terminator char, so let's skip this line. + lines.next() + } + // For the top level declaration depth is -1 + depth = if (currentContainer.parent == null) -1 else currentContainer.text.depth() + } + } + } + } + + private fun PeekingLineIterator.parseTargets(configurableTargetName: String?): Set { + val line = peek() + require(line != null) { + "List of targets expected, but there are no more lines left." + } + require(line.startsWith(TARGETS_LIST_PREFIX)) { + "The line should starts with $TARGETS_LIST_PREFIX, but was: $line" + } + next() + val targets = parseBcvTargetsLine(line) + require(configurableTargetName == null || targets.size == 1) { + "Can't use configurableTargetName ($configurableTargetName) for a multi-target dump: $targets" + } + if (configurableTargetName != null) { + return setOf(KlibTarget(targets.first().targetName, configurableTargetName)) + } + return targets + } + + private fun PeekingLineIterator.parseAliases(): Map> { + val aliases = mutableMapOf>() + while (peek()?.startsWith(ALIAS_PREFIX) == true) { + val line = next() + val trimmedLine = line.substring(ALIAS_PREFIX.length) + val separatorIdx = trimmedLine.indexOf(" => [") + if (separatorIdx == -1 || !trimmedLine.endsWith(']')) { + throw IllegalStateException("Invalid alias line: $line") + } + val name = trimmedLine.substring(0, separatorIdx) + val targets = trimmedLine.substring( + separatorIdx + " => [".length, + trimmedLine.length - 1 + ) + .split(",") + .map { KlibTarget.parse(it.trim()) } + .toSet() + aliases[name] = targets + } + return aliases + } + + private fun PeekingLineIterator.parseFileHeader( + isMergedFile: Boolean, + configurableTargetName: String? + ): KlibAbiDumpHeader { + val header = mutableListOf() + var targets: String? = null + var platform: String? = null + + // read the common head first + while (hasNext()) { + val next = peek()!! + if (next.isNotBlank() && !next.startsWith(COMMENT_PREFIX)) { + throw IllegalStateException("Library header has invalid format at line \"$next\"") + } + header.add(next) + next() + if (next.startsWith(LIBRARY_NAME_PREFIX)) { + break + } + } + // then try to parse a manifest + while (hasNext()) { + val next = peek()!! + if (!next.startsWith(COMMENT_PREFIX)) break + next() + // There's no manifest in merged files + check(!isMergedFile) { "Unexpected header line: $next" } + when { + next.startsWith(PLATFORM_PREFIX) -> { + platform = next.split(": ")[1].trim() + } + + next.startsWith(NATIVE_TARGETS_PREFIX) -> { + targets = next.split(": ")[1].trim() + } + } + } + if (isMergedFile) { + return KlibAbiDumpHeader(header, emptySet()) + } + + // transform a combination of platform name and targets list to a set of KlibTargets + return KlibAbiDumpHeader(header, extractTargets(platform, targets, configurableTargetName)) + } + + private fun extractTargets( + platformString: String?, + targetsString: String?, + configurableTargetName: String? + ): Set { + check(platformString != null) { + "The dump does not contain platform name. Please make sure that the manifest was included in the dump" + } + + if (platformString == "WASM") { + // Currently, there's no way to distinguish Wasm targets without explicitly specifying a target name + check(configurableTargetName != null) { "targetName has to be specified for a Wasm target" } + return setOf(KlibTarget(configurableTargetName)) + } + if (platformString != "NATIVE") { + val platformStringLc = platformString.toLowerCase(Locale.ROOT) + return if (configurableTargetName == null) { + setOf(KlibTarget(platformStringLc)) + } else { + setOf(KlibTarget(platformStringLc, configurableTargetName)) + } + } + + check(targetsString != null) { "Dump for a native platform missing targets list." } + + val targetsList = targetsString.split(TARGETS_DELIMITER).map { + konanTargetNameMapping[it.trim()] ?: throw IllegalStateException("Unknown native target: $it") + } + require(targetsList.size == 1 || configurableTargetName == null) { + "Can't use configurableTargetName ($configurableTargetName) for a multi-target dump: $targetsList" + } + if (targetsList.size == 1 && configurableTargetName != null) { + return setOf(KlibTarget(targetsList.first(), configurableTargetName)) + } + return targetsList.asSequence().map { KlibTarget(it) }.toSet() + } + + private fun PeekingLineIterator.determineFileType(): Boolean { + val headerLine = peek() ?: throw IllegalStateException("File is empty") + if (headerLine.trimEnd() == MERGED_DUMP_FILE_HEADER) { + return true + } + if (headerLine.trimEnd() == REGULAR_DUMP_FILE_HEADER) { + return false + } + val headerStart = if (headerLine.length > 32) { + headerLine.substring(0, 32) + "..." + } else { + headerLine + } + throw IllegalStateException( + "Expected a file staring with \"$REGULAR_DUMP_FILE_HEADER\" " + + "or \"$MERGED_DUMP_FILE_HEADER\", but the file stats with \"$headerStart\"" + ) + } + + private fun PeekingLineIterator.parseDeclaration( + depth: Int, + parent: DeclarationContainer, + parentTargetsStack: MutableList>, + aliases: Map> + ): DeclarationContainer { + val line = peek()!! + return if (line.startsWith(" ".repeat(depth * INDENT_WIDTH) + TARGETS_LIST_PREFIX)) { + next() // skip prefix + // Target list means that the declaration following it has a narrower set of targets than its parent, + // so we must use it. + val targets = parseBcvTargetsLine(line) + val expandedTargets = targets.flatMap { + aliases[it.configurableName] ?: listOf(it) + }.toSet() + parentTargetsStack.add(expandedTargets) + parent.createOrUpdateChildren(next(), expandedTargets) + } else { + // Inherit all targets from a parent + parentTargetsStack.add(parentTargetsStack.last()) + parent.createOrUpdateChildren(next(), parentTargetsStack.last()) + } + } + + fun dump(appendable: Appendable) { + if (targets.isEmpty()) { + check(topLevelDeclaration.children.isEmpty()) { + "Dump containing some declaration should have at least a single target" + } + return + } + val formatter = createFormatter() + appendable.append(MERGED_DUMP_FILE_HEADER).append('\n') + appendable.append(formatter.formatHeader(targets)).append('\n') + headerContent.forEach { + appendable.append(it).append('\n') + } + topLevelDeclaration.children.values.sortedWith(DeclarationsComparator).forEach { + it.dump(appendable, _targets, formatter) + } + } + + private fun createFormatter(): KlibsTargetsFormatter { + for (target in targets) { + val node = TargetHierarchy.hierarchyIndex[target.targetName] + if (node != null && node.allLeafs.size == 1 && node.allLeafs.first() != node.node.name) { + throw IllegalStateException( + "Can't use target aliases as one of the this dump's targets" + + " has the same name as a group in the default targets hierarchy: $target" + ) + } + } + return KlibsTargetsFormatter(this) + } + + /** + * Remove the [target] from this dump. + * If some declaration was declared only for [target], it will be removed from the dump. + */ + fun remove(target: KlibTarget) { + if (!_targets.remove(target)) { + return + } + topLevelDeclaration.remove(target) + } + + /** + * Leave only declarations specific to a [target]. + * A declaration is considered target-specific if: + * 1) it defined for some [targets] subset including [target], but not for all [targets]; + * 2) it defined for all [targets], but contains target-specific child declaration. + */ + fun retainTargetSpecificAbi(target: KlibTarget) { + if (!_targets.contains(target)) { + _targets.clear() + topLevelDeclaration.children.clear() + topLevelDeclaration.targets.clear() + return + } + + topLevelDeclaration.retainSpecific(target, _targets) + _targets.retainAll(setOf(target)) + } + + /** + * Remove all declarations that are not defined for all [KlibAbiDumpMerger.targets]. + */ + fun retainCommonAbi() { + topLevelDeclaration.retainCommon(_targets) + if (topLevelDeclaration.children.isEmpty()) { + _targets.clear() + } + } + + /** + * Merge the [other] dump containing declarations for a single target into this dump. + * The dump [other] should contain exactly one target and this dump should not contain that target. + */ + fun mergeTargetSpecific(other: KlibAbiDumpMerger) { + require(other._targets.size == 1) { + "The dump to merge in should have a single target, but its targets are: ${other.targets}" + } + require(other._targets.first() !in _targets) { + "Targets of this dump and the dump to merge into it should not intersect. " + + "Common target: ${other.targets.first()}}" + } + + _targets.addAll(other._targets) + topLevelDeclaration.mergeTargetSpecific(other.topLevelDeclaration) + } + + /** + * Merges other [KlibAbiDumpMerger] into this one. + */ + fun merge(other: KlibAbiDumpMerger) { + if (other.targets.isEmpty()) return + + targets.intersect(other.targets).also { + require(it.isEmpty()) { + "Targets of this dump and the dump to merge into it should not intersect. Common targets: $it" + } + } + if (headerContent != other.headerContent) { + // the dump was empty + if (headerContent.isEmpty() && targets.isEmpty()) { + headerContent.addAll(other.headerContent) + } else { + throw IllegalArgumentException("Dumps headers does not match") + } + } + + _targets.addAll(other._targets) + topLevelDeclaration.merge(other.topLevelDeclaration) + } + + /** + * For each declaration change targets to a specified [targets] set. + */ + fun overrideTargets(targets: Set) { + _targets.clear() + _targets.addAll(targets) + + topLevelDeclaration.overrideTargets(targets) + } + + internal fun visit(action: (DeclarationContainer) -> Unit) { + topLevelDeclaration.children.values.forEach(action) + } +} + +/** + * A class representing a single declaration from a KLib API dump along with all its children + * declarations. + */ +internal class DeclarationContainer(val text: String, val parent: DeclarationContainer? = null) { + val targets: MutableSet = mutableSetOf() + val children: MutableMap = mutableMapOf() + var delimiter: String? = null + + fun createOrUpdateChildren(text: String, targets: Set): DeclarationContainer { + val child = children.computeIfAbsent(text) { + val newChild = DeclarationContainer(it, this) + newChild + } + child.targets.addAll(targets) + return child + } + + fun dump(appendable: Appendable, allTargets: Set, formatter: KlibsTargetsFormatter) { + if (targets != allTargets/* && !dumpFormat.singleTargetDump*/) { + // Use the same indentation for target list as for the declaration itself + appendable.append(" ".repeat(text.depth() * INDENT_WIDTH)) + .append(formatter.formatDeclarationTargets(targets)) + .append('\n') + } + appendable.append(text).append('\n') + children.values.sortedWith(DeclarationsComparator).forEach { + it.dump(appendable, this.targets, formatter) + } + if (delimiter != null) { + appendable.append(delimiter).append('\n') + } + } + + fun remove(target: KlibTarget) { + if (parent != null && !targets.contains(target)) { + return + } + targets.remove(target) + mutateChildrenAndRemoveTargetless { it.remove(target) } + } + + fun retainSpecific(target: KlibTarget, allTargets: Set) { + if (parent != null && !targets.contains(target)) { + children.clear() + targets.clear() + return + } + + mutateChildrenAndRemoveTargetless { it.retainSpecific(target, allTargets) } + + if (targets == allTargets) { + if (children.isEmpty()) { + targets.clear() + } else { + targets.retainAll(setOf(target)) + } + } else { + targets.retainAll(setOf(target)) + } + } + + fun retainCommon(commonTargets: Set) { + if (parent != null && targets != commonTargets) { + children.clear() + targets.clear() + return + } + mutateChildrenAndRemoveTargetless { it.retainCommon(commonTargets) } + } + + fun mergeTargetSpecific(other: DeclarationContainer) { + targets.addAll(other.targets) + other.children.forEach { otherChild -> + when (val child = children[otherChild.key]) { + null -> children[otherChild.key] = otherChild.value + else -> child.mergeTargetSpecific(otherChild.value) + } + } + children.forEach { + if (other.targets.first() !in it.value.targets) { + it.value.addTargetRecursively(other.targets.first()) + } + } + } + + fun merge(other: DeclarationContainer) { + targets.addAll(other.targets) + val parent = this + other.children.forEach { (line, decl) -> + children.compute(line) { _, thisDecl -> + if (thisDecl == null) { + decl.deepCopy(parent) + } else { + thisDecl.apply { merge(decl) } + } + } + } + } + + fun deepCopy(parent: DeclarationContainer): DeclarationContainer { + val copy = DeclarationContainer(this.text, parent) + copy.delimiter = delimiter + copy.targets.addAll(targets) + children.forEach { key, value -> + copy.children[key] = value.deepCopy(copy) + } + return copy + } + + private fun addTargetRecursively(first: KlibTarget) { + targets.add(first) + children.forEach { it.value.addTargetRecursively(first) } + } + + fun overrideTargets(targets: Set) { + this.targets.clear() + this.targets.addAll(targets) + children.forEach { it.value.overrideTargets(targets) } + } + + private inline fun mutateChildrenAndRemoveTargetless(blockAction: (DeclarationContainer) -> Unit) { + val iterator = children.iterator() + while (iterator.hasNext()) { + val (_, child) = iterator.next() + blockAction(child) + if (child.targets.isEmpty()) { + iterator.remove() + } + } + } + + internal fun visit(action: (DeclarationContainer) -> Unit) { + children.forEach { + action(it.value) + } + } +} + +// TODO: optimize +private object DeclarationsComparator : Comparator { + private fun Set.serializeAndSort(): MutableList { + return this.mapTo(mutableListOf()) { it.toString() }.apply { sort() } + } + + override fun compare(c0: DeclarationContainer, c1: DeclarationContainer): Int { + return if (c0.targets == c1.targets) { + c0.text.compareTo(c1.text) + } else { + if (c0.targets.size == c1.targets.size) { + val c0targets = c0.targets.serializeAndSort().iterator() + val c1targets = c1.targets.serializeAndSort().iterator() + var result = 0 + while (c1targets.hasNext() && c0targets.hasNext() && result == 0) { + result = c0targets.next().compareTo(c1targets.next()) + } + result + } else { + // the longer the target list, the earlier the declaration would appear + c1.targets.size.compareTo(c0.targets.size) + } + } + } +} + +internal class KlibsTargetsFormatter(klibDump: KlibAbiDumpMerger) { + private data class Alias(val name: String, val targets: Set) + + private val aliases: List + + init { + // place more specific groups (with a higher depth value) closer to the beginning of the list + val nodesDescendingComparator = + compareByDescending> { it.value.depth } + .thenByDescending { it.key } + val allTargets = klibDump.targets + val aliasesBuilder = mutableListOf() + TargetHierarchy.hierarchyIndex.entries + .sortedWith(nodesDescendingComparator) + .forEach { + // intersect with all targets to use only enabled targets in aliases + // intersection is based on underlying target name as a set of such names is fixed + val leafs = it.value.allLeafs + val availableTargets = allTargets.asSequence().filter { leafs.contains(it.targetName) }.toSet() + if (availableTargets.isNotEmpty()) { + aliasesBuilder.add(Alias(it.key, availableTargets)) + } + } + + // filter out all groups consisting of less than one member + aliasesBuilder.removeIf { it.targets.size < 2 } + aliasesBuilder.removeIf { it.targets == allTargets } + filterOutDumplicateGroups(aliasesBuilder) + filterOutUnusedGroups(klibDump, aliasesBuilder) + + // reverse the order to place a common group first + aliases = aliasesBuilder.reversed() + } + + private fun filterOutDumplicateGroups(allGroups: MutableList) { + // Remove all duplicating groups. At this point, aliases are sorted so + // that more specific groups are before more general groups, so we'll remove + // more common groups here. + for (idx in allGroups.size - 1 downTo 1) { + if (allGroups[idx].targets == allGroups[idx - 1].targets) { + // TODO: can we avoid shifting the whole trailing part of the list? + allGroups.removeAt(idx) + } + } + } + + // TODO: optimize the algorithm + private fun filterOutUnusedGroups(klibDump: KlibAbiDumpMerger, allGroups: MutableList) { + // Collect all target sets that are actually in use + val targetSetsInUse = mutableSetOf>() + val allTargets = klibDump.targets + fun visitor(decl: DeclarationContainer) { + if (decl.targets != allTargets) { + targetSetsInUse.add(decl.targets) + } + decl.visit(::visitor) + } + klibDump.visit(::visitor) + // Scan groups from general to specific and check it there are some + // declarations having a target set such that it includes a group. + // If there are no such declarations - a group has to be removed. + for (idx in allGroups.size - 1 downTo 0) { + val alias = allGroups[idx] + val updatedTargetSets = mutableSetOf>() + // scan actually used target sets + val targetSetIterator = targetSetsInUse.iterator() + while (targetSetIterator.hasNext()) { + val s = targetSetIterator.next() + // If a target set includes this group, take that set, remove all targets + // corresponding to the current group from it and then, later, add the set back. + if (s.containsAll(alias.targets)) { + targetSetIterator.remove() + updatedTargetSets.add(s.subtract(alias.targets)) + } + } + // If updatedTargetSets is empty, there are no target sets including the current group, + // so we have to remove the group. + if (updatedTargetSets.isEmpty()) { + allGroups.removeAt(idx) + } else { + targetSetsInUse.addAll(updatedTargetSets) + } + } + } + + fun formatHeader(targets: Set): String { + return buildString { + append( + targets.asSequence().map { it.toString() }.sorted().joinToString( + prefix = TARGETS_LIST_PREFIX, + postfix = TARGETS_LIST_SUFFIX, + separator = TARGETS_DELIMITER + ) + ) + aliases.forEach { + append("\n$ALIAS_PREFIX${it.name} => [") + append(it.targets.map { it.toString() }.sorted().joinToString(TARGETS_DELIMITER)) + append(TARGETS_LIST_SUFFIX) + } + } + } + + fun formatDeclarationTargets(targets: Set): String { + val mutableTargets = targets.toMutableSet() + val resultingTargets = mutableListOf() + for (alias in aliases) { + if (mutableTargets.containsAll(alias.targets)) { + mutableTargets.removeAll(alias.targets) + resultingTargets.add(alias.name) + } + } + resultingTargets.addAll(mutableTargets.map { it.toString() }) + return resultingTargets.sorted().joinToString( + prefix = TARGETS_LIST_PREFIX, + postfix = TARGETS_LIST_SUFFIX, + separator = TARGETS_DELIMITER + ) + } +} diff --git a/src/main/kotlin/api/klib/KlibDump.kt b/src/main/kotlin/api/klib/KlibDump.kt new file mode 100644 index 00000000..2b8f35c4 --- /dev/null +++ b/src/main/kotlin/api/klib/KlibDump.kt @@ -0,0 +1,283 @@ +/* + * 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.api.klib + +import kotlinx.validation.ExperimentalBCVApi +import org.jetbrains.kotlin.ir.backend.js.MainModule +import java.io.File +import java.io.FileNotFoundException + +/** + * Represents KLib ABI dump and allows manipulating it. + * + * Usual [KlibDump] workflows consists of loading, updating and writing a dump back. + * + * **Creating a textual dump from a klib** + * ```kotlin + * val dump = KlibDump.fromKlib(File("/path/to/library.klib")) + * dump.saveTo(File("/path/to/dump.klib.api")) + * ``` + * + * **Loading a dump** + * ```kotlin + * val dump = KlibDump.from(File("/path/to/dump.klib.api")) + * ``` + * + * **Merging multiple dumps into a new merged dump** + * ```kotlin + * val klibs = listOf(File("/path/to/library-linuxX64.klib"), File("/path/to/library-linuxArm64.klib"), ...) + * val mergedDump = KlibDump() + * klibs.forEach { mergedDump.mergeFromKlib(it) } + * mergedDump.saveTo(File("/path/to/merged.klib.api")) + * ``` + * + * **Updating an existing merged dump** + * ```kotlin + * val mergedDump = KlibDump.from(File("/path/to/merged.klib.api")) + * val newTargetDump = KlibDump.fromKlib(File("/path/to/library-linuxX64.klib")) + * mergedDump.remove(newTargetDump.targets) + * mergedDump.merge(newTargetDump) + * mergedDump.saveTo(File("/path/to/merged.klib.api")) + * ``` + */ +@ExperimentalBCVApi +public class KlibDump { + internal val merger: KlibAbiDumpMerger = KlibAbiDumpMerger() + + /** + * Set of all targets for which this dump contains declarations. + * + * @sample samples.KlibDumpSamples.extractTargets + */ + public val targets: Set + get() = merger.targets + + + /** + * Loads a textual KLib dump and merges it into this dump. + * + * If a dump contains only a single target, it's possible to specify a custom configurable target name. + * Please refer to [KlibTarget.configurableName] for more details on the meaning of that name. + * + * By default, [configurableTargetName] is null and information about a target will be taken directly from + * the loaded dump. + * + * It's an error to specify non-null [configurableTargetName] for a dump containing multiple targets. + * It's also an error to merge dumps having some targets in common. + * + * @throws IllegalArgumentException if this dump and [dumpFile] shares same targets. + * @throws IllegalArgumentException if [dumpFile] contains multiple targets + * and [configurableTargetName] is not null. + * @throws IllegalArgumentException if [dumpFile] is not a file. + * @throws FileNotFoundException if [dumpFile] does not exist. + * + * @sample samples.KlibDumpSamples.mergeDumps + */ + public fun merge(dumpFile: File, configurableTargetName: String? = null) { + if(!dumpFile.exists()) { throw FileNotFoundException("File does not exist: ${dumpFile.absolutePath}") } + require(dumpFile.isFile) { "Not a file: ${dumpFile.absolutePath}" } + merger.merge(dumpFile, configurableTargetName) + } + + /** + * Merges [other] dump with this one. + * + * It's also an error to merge dumps having some targets in common. + * + * The operation does not modify [other]. + * + * @throws IllegalArgumentException if this dump and [other] shares same targets. + * + * @sample samples.KlibDumpSamples.mergeDumpObjects + */ + public fun merge(other: KlibDump) { + val intersection = targets.intersect(other.targets) + require(intersection.isEmpty()) { + "Cannot merge dump as this and other dumps share some targets: $intersection" + } + merger.merge(other.merger) + } + + /** + * Removes all declarations that do not belong to specified targets and removes these targets from the dump. + * + * All targets in the [targets] collection not contained within this dump will be ignored. + * + * @sample samples.KlibDumpSamples.extractTargets + */ + public fun retain(targets: Iterable) { + val toRemove = merger.targets.subtract(targets.toSet()) + remove(toRemove) + } + + /** + * Remove all declarations that do belong to specified targets and remove these targets from the dump. + * + * All targets in the [targets] collection not contained within this dump will be ignored. + * + * @sample samples.KlibDumpSamples.mergeDumpObjects + */ + public fun remove(targets: Iterable) { + targets.forEach { + merger.remove(it) + } + } + + /** + * Creates a copy of this dump. + */ + public fun copy(): KlibDump = KlibDump().also { it.merge(this) } + + /** + * Serializes the dump and writes it to [to]. + * + * @sample samples.KlibDumpSamples.mergeDumps + */ + public fun saveTo(to: Appendable) { + merger.dump(to) + } + + public companion object { + /** + * Loads a dump from a textual form. + * + * If a dump contains only a single target, it's possible to specify a custom configurable target name. + * Please refer to [KlibTarget.configurableName] for more details on the meaning of that name. + * + * By default, [configurableTargetName] is null and information about a target will be taken directly from + * the loaded dump. + * + * It's an error to specify non-null [configurableTargetName] for a dump containing multiple targets. + * + * @throws IllegalArgumentException if [dumpFile] contains multiple targets + * and [configurableTargetName] is not null. + * @throws IllegalArgumentException if [dumpFile] is empty. + * @throws IllegalArgumentException if [dumpFile] is not a file. + * @throws FileNotFoundException if [dumpFile] does not exist. + * + * @sample samples.KlibDumpSamples.mergeDumpObjects + */ + public fun from(dumpFile: File, configurableTargetName: String? = null): KlibDump { + if(!dumpFile.exists()) { throw FileNotFoundException("File does not exist: ${dumpFile.absolutePath}") } + require(dumpFile.isFile) { "Not a file: ${dumpFile.absolutePath}" } + return KlibDump().apply { merge(dumpFile, configurableTargetName) } + } + + /** + * Dumps a public ABI of a klib represented by [klibFile] using [filters] + * and returns a [KlibDump] representing it. + * + * To control which declarations are dumped, [filters] could be used. By default, no filters will be applied. + * + * If a klib contains only a single target, it's possible to specify a custom configurable target name. + * Please refer to [KlibTarget.configurableName] for more details on the meaning of that name. + * + * By default, [configurableTargetName] is null and information about a target will be taken directly from + * the klib. + * + * It's an error to specify non-null [configurableTargetName] for a klib containing multiple targets. + * + * @throws IllegalArgumentException if [klibFile] contains multiple targets + * and [configurableTargetName] is not null. + * @throws IllegalStateException if a klib could not be loaded from [klibFile]. + * @throws FileNotFoundException if [klibFile] does not exist. + */ + public fun fromKlib( + klibFile: File, + configurableTargetName: String? = null, + filters: KlibDumpFilters = KlibDumpFilters.DEFAULT + ): KlibDump { + val dump = buildString { + dumpTo(this, klibFile, filters) + } + return KlibDump().apply { + merger.merge(dump.splitToSequence('\n').iterator(), configurableTargetName) + } + } + } +} + +/** + * Infer a possible public ABI for [unsupportedTarget] as an ABI common across all [supportedTargetDumps]. + * If there's an [oldMergedDump] consisting of declarations of multiple targets, including [unsupportedTarget], + * a portion of that dump specific to the [unsupportedTarget] will be extracted and merged to the common ABI + * build from [supportedTargetDumps]. + * + * Returned dump contains only declarations for [unsupportedTarget]. + * + * The function aimed to facilitate ABI dumps generation for targets that are not supported by a host compiler. + * In practice, it means generating dumps for Apple targets on non-Apple hosts. + * + * @throws IllegalArgumentException when one of [supportedTargetDumps] contains [unsupportedTarget] + * @throws IllegalArgumentException when [supportedTargetDumps] are empty and [oldMergedDump] is null + * + * @sample samples.KlibDumpSamples.inferDump + */ +@ExperimentalBCVApi +public fun inferAbi( + unsupportedTarget: KlibTarget, + supportedTargetDumps: Iterable, + oldMergedDump: KlibDump? = null +): KlibDump { + require(supportedTargetDumps.iterator().hasNext() || oldMergedDump != null) { + "Can't infer a dump without any dumps provided (supportedTargetDumps is empty, oldMergedDump is null)" + } + supportedTargetDumps.asSequence().flatMap { it.targets }.toSet().also { + require(!it.contains(unsupportedTarget)) { + "Supported target dumps already contains unsupportedTarget=$unsupportedTarget" + } + } + + val retainedDump = KlibDump().apply { + if (oldMergedDump != null) { + merge(oldMergedDump) + merger.retainTargetSpecificAbi(unsupportedTarget) + } + } + val commonDump = KlibDump().apply { + supportedTargetDumps.forEach { + merge(it) + } + merger.retainCommonAbi() + } + commonDump.merge(retainedDump) + commonDump.merger.overrideTargets(setOf(unsupportedTarget)) + return commonDump +} + +/** + * Dumps a public ABI of a klib represented by [klibFile] using [filters] and merges it into this dump. + * + * To control which declarations are dumped, [filters] could be used. By default, no filters will be applied. + * + * If a klib contains only a single target, it's possible to specify a custom configurable target name. + * Please refer to [KlibTarget.configurableName] for more details on the meaning of that name. + * + * By default, [configurableTargetName] is null and information about a target will be taken directly from + * the klib. + * + * It's an error to specify non-null [configurableTargetName] for a klib containing multiple targets. + * It's also an error to merge dumps having some targets in common. + * + * @throws IllegalArgumentException if this dump and [klibFile] shares same targets. + * @throws IllegalArgumentException if [klibFile] contains multiple targets + * and [configurableTargetName] is not null. + * @throws IllegalStateException if a klib could not be loaded from [klibFile]. + * @throws FileNotFoundException if [klibFile] does not exist. + */ +@ExperimentalBCVApi +public fun KlibDump.mergeFromKlib( + klibFile: File, configurableTargetName: String? = null, + filters: KlibDumpFilters = KlibDumpFilters.DEFAULT +) { + this.merge(KlibDump.fromKlib(klibFile, configurableTargetName, filters)) +} + +/** + * Serializes the dump and writes it to [file]. + */ +@ExperimentalBCVApi +public fun KlibDump.saveTo(file: File): Unit = file.bufferedWriter().use { saveTo(it) } diff --git a/src/main/kotlin/api/klib/KlibDumpFilters.kt b/src/main/kotlin/api/klib/KlibDumpFilters.kt new file mode 100644 index 00000000..ce90824b --- /dev/null +++ b/src/main/kotlin/api/klib/KlibDumpFilters.kt @@ -0,0 +1,198 @@ +/* + * 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.api.klib + +import kotlinx.validation.ExperimentalBCVApi +import org.jetbrains.kotlin.library.abi.* +import java.io.File +import java.io.FileNotFoundException + +/** + * Filters affecting how the klib ABI will be represented in a dump. + */ +@ExperimentalBCVApi +public class KlibDumpFilters internal constructor( + /** + * Names of packages that should be excluded from a dump. + * If a package is listed here, none of its declarations will be included in a dump. + */ + public val ignoredPackages: Set, + /** + * Names of classes that should be excluded from a dump. + */ + public val ignoredClasses: Set, + /** + * Names of annotations marking non-public declarations. + * Such declarations will be excluded from a dump. + */ + public val nonPublicMarkers: Set, + /** + * KLib ABI signature version to include in a dump. + */ + public val signatureVersion: KlibSignatureVersion +) { + + public class Builder @PublishedApi internal constructor() { + /** + * Names of packages that should be excluded from a dump. + * If a package is listed here, none of its declarations will be included in a dump. + * + * By default, there are no ignored packages. + */ + public val ignoredPackages: MutableSet = mutableSetOf() + + /** + * Names of classes that should be excluded from a dump. + * + * By default, there are no ignored classes. + */ + public val ignoredClasses: MutableSet = mutableSetOf() + + /** + * Names of annotations marking non-public declarations. + * Such declarations will be excluded from a dump. + * + * By default, a set of non-public markers is empty. + */ + public val nonPublicMarkers: MutableSet = mutableSetOf() + + /** + * KLib ABI signature version to include in a dump. + * + * By default, the latest ABI signature version provided by a klib + * and supported by a reader will be used. + */ + public var signatureVersion: KlibSignatureVersion = KlibSignatureVersion.LATEST + + @PublishedApi + internal fun build(): KlibDumpFilters { + return KlibDumpFilters(ignoredPackages, ignoredClasses, nonPublicMarkers, signatureVersion) + } + } + + public companion object { + /** + * Default KLib ABI dump filters which declares no filters + * and uses the latest KLib ABI signature version available. + */ + public val DEFAULT: KlibDumpFilters = KLibDumpFilters {} + } +} + +/** + * Builds a new [KlibDumpFilters] instance by invoking a [builderAction] on a temporary + * [KlibDumpFilters.Builder] instance and then converting it into filters. + * + * Supplied [KlibDumpFilters.Builder] is valid only during the scope of [builderAction] execution. + */ +@ExperimentalBCVApi +public fun KLibDumpFilters(builderAction: KlibDumpFilters.Builder.() -> Unit): KlibDumpFilters { + val builder = KlibDumpFilters.Builder() + builderAction(builder) + return builder.build() +} + +@ExperimentalBCVApi +@OptIn(ExperimentalLibraryAbiReader::class) +internal fun dumpTo(to: Appendable, klibFile: File, filters: KlibDumpFilters) { + if(!klibFile.exists()) { throw FileNotFoundException("File does not exist: ${klibFile.absolutePath}") } + val abiFilters = mutableListOf() + filters.ignoredClasses.toKlibNames().also { + if (it.isNotEmpty()) { + abiFilters.add(AbiReadingFilter.ExcludedClasses(it)) + } + } + filters.nonPublicMarkers.toKlibNames().also { + if (it.isNotEmpty()) { + abiFilters.add(AbiReadingFilter.NonPublicMarkerAnnotations(it)) + } + } + if (filters.ignoredPackages.isNotEmpty()) { + abiFilters.add(AbiReadingFilter.ExcludedPackages(filters.ignoredPackages.map { AbiCompoundName(it) })) + } + + val library = try { + LibraryAbiReader.readAbiInfo(klibFile, abiFilters) + } catch (t: Throwable) { + throw IllegalStateException("Unable to read klib from ${klibFile.absolutePath}", t) + } + + val supportedSignatureVersions = library.signatureVersions.asSequence().filter { it.isSupportedByAbiReader } + + val signatureVersion = if (filters.signatureVersion == KlibSignatureVersion.LATEST) { + supportedSignatureVersions.maxByOrNull { it.versionNumber } + ?: throw IllegalStateException("Can't choose signatureVersion") + } else { + supportedSignatureVersions.find { it.versionNumber == filters.signatureVersion.version } + ?: throw IllegalArgumentException( + "Unsupported KLib signature version '${filters.signatureVersion.version}'. " + + "Supported versions are: ${ + supportedSignatureVersions.map { it.versionNumber }.sorted().toList() + }" + ) + } + + LibraryAbiRenderer.render( + library, to, AbiRenderingSettings( + renderedSignatureVersion = signatureVersion, + renderManifest = true, + renderDeclarations = true + ) + ) +} + +// We're assuming that all names are in valid binary form as it's described in JVMLS §13.1: +// https://docs.oracle.com/javase/specs/jls/se21/html/jls-13.html#jls-13.1 +@OptIn(ExperimentalLibraryAbiReader::class) +private fun Collection.toKlibNames(): List = + this.map(String::toAbiQualifiedName).filterNotNull() + +@OptIn(ExperimentalLibraryAbiReader::class) +internal fun String.toAbiQualifiedName(): AbiQualifiedName? { + if (this.isBlank() || this.contains('/')) return null + // Easiest part: dissect package name from the class name + val idx = this.lastIndexOf('.') + if (idx == -1) { + return AbiQualifiedName(AbiCompoundName(""), this.classNameToCompoundName()) + } else { + val packageName = this.substring(0, idx) + val className = this.substring(idx + 1) + return AbiQualifiedName(AbiCompoundName(packageName), className.classNameToCompoundName()) + } +} + +@OptIn(ExperimentalLibraryAbiReader::class) +private fun String.classNameToCompoundName(): AbiCompoundName { + if (this.isEmpty()) return AbiCompoundName(this) + + val segments = mutableListOf() + val builder = StringBuilder() + + for (idx in this.indices) { + val c = this[idx] + // Don't treat a character as a separator if: + // - it's not a '$' + // - it's at the beginning of the segment + // - it's the last character of the string + if (c != '$' || builder.isEmpty() || idx == this.length - 1) { + builder.append(c) + continue + } + check(c == '$') + // class$$$susbclass -> class.$$subclass, were at second $ here. + if (builder.last() == '$') { + builder.append(c) + continue + } + + segments.add(builder.toString()) + builder.clear() + } + if (builder.isNotEmpty()) { + segments.add(builder.toString()) + } + return AbiCompoundName(segments.joinToString(separator = ".")) +} diff --git a/src/main/kotlin/api/klib/KlibSignatureVersion.kt b/src/main/kotlin/api/klib/KlibSignatureVersion.kt new file mode 100644 index 00000000..eee9ba20 --- /dev/null +++ b/src/main/kotlin/api/klib/KlibSignatureVersion.kt @@ -0,0 +1,35 @@ +/* + * 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.api.klib + +public class KlibSignatureVersion internal constructor(internal val version: Int) { + + public companion object { + public fun of(value: Int): KlibSignatureVersion { + require(value >= 1) { + "Invalid version value, expected positive value: $value" + } + return KlibSignatureVersion(value) + } + + public val LATEST: KlibSignatureVersion = KlibSignatureVersion(Int.MIN_VALUE) + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is KlibSignatureVersion) return false + + return version == other.version + } + + override fun hashCode(): Int { + return version.hashCode() + } + + override fun toString(): String { + return "KlibSignatureVersion($version)" + } +} diff --git a/src/main/kotlin/api/klib/KlibTarget.kt b/src/main/kotlin/api/klib/KlibTarget.kt new file mode 100644 index 00000000..bfcc78da --- /dev/null +++ b/src/main/kotlin/api/klib/KlibTarget.kt @@ -0,0 +1,80 @@ +/* + * 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.api.klib + + +/** + * Target name consisting of two parts: a [configurableName] that could be configured by a user, and an [targetName] + * that names a target platform and could not be configured by a user. + * + * When serialized, the target represented as a tuple `.`, like `ios.iosArm64`. + * If both names are the same (they are by default, unless a user decides to use a custom name), the serialized + * from is shortened to a single term. For example, `macosArm64.macosArm64` and `macosArm64` are a long and a short + * serialized forms of the same target. + */ +public class KlibTarget internal constructor( + /** + * An actual name of a target that remains unaffected by a custom name settings in a build script. + */ + public val targetName: String, + /** + * A name of a target that could be configured by a user in a build script. + * Usually, it's the same name as [targetName]. + */ + public val configurableName: String +) { + init { + require(!configurableName.contains(".")) { + "Configurable name can't contain the '.' character: $configurableName" + } + require(!targetName.contains(".")) { + "Target name can't contain the '.' character: $targetName" + } + } + public companion object { + /** + * Parses a [KlibTarget] from a [value] string in a long (`.`) + * or a short (``) format. + * + * @throws IllegalArgumentException if [value] does not conform the format. + */ + public fun parse(value: String): KlibTarget { + require(value.isNotBlank()) { "Target name could not be blank." } + if (!value.contains('.')) { + return KlibTarget(value) + } + val parts = value.split('.') + if (parts.size != 2 || parts.any { it.isBlank() }) { + throw IllegalArgumentException( + "Target has illegal name format: \"$value\", expected: ." + ) + } + return KlibTarget(parts[0], parts[1]) + } + } + + + override fun toString(): String = + if (configurableName == targetName) configurableName else "$targetName.$configurableName" + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is KlibTarget) return false + + if (configurableName != other.configurableName) return false + if (targetName != other.targetName) return false + + return true + } + + override fun hashCode(): Int { + var result = configurableName.hashCode() + result = 31 * result + targetName.hashCode() + return result + } +} + +internal fun KlibTarget(name: String) = KlibTarget(name, name) diff --git a/src/main/kotlin/api/klib/TargetHierarchy.kt b/src/main/kotlin/api/klib/TargetHierarchy.kt new file mode 100644 index 00000000..fe5aac63 --- /dev/null +++ b/src/main/kotlin/api/klib/TargetHierarchy.kt @@ -0,0 +1,138 @@ +/* + * 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.api.klib + +/** + * A hierarchy of KMP targets that should resemble the default hierarchy template. + */ +internal object TargetHierarchy { + class Node(val name: String, vararg childrenNodes: Node) { + var parent: Node? = null + val children = childrenNodes.toList().toTypedArray() + + init { + childrenNodes.forEach { + it.parent = this + } + } + } + + data class NodeClosure(val node: Node, val depth: Int, val allLeafs: Set) + + internal val hierarchyIndex: Map + + private val hierarchy = Node( + "all", + Node("js"), + Node("wasmJs"), + Node("wasmWasi"), + Node( + "native", + Node( + "mingw", + Node("mingwX64"), + Node("mingwX86") + ), + Node( + "linux", + Node("linuxArm64"), + Node("linuxArm32Hfp"), + Node("linuxX64"), + ), + Node( + "androidNative", + Node("androidNativeArm64"), + Node("androidNativeArm32"), + Node("androidNativeX86"), + Node("androidNativeX64") + ), + Node( + "apple", + Node( + "macos", + Node("macosArm64"), + Node("macosX64") + ), + Node( + "ios", + Node("iosArm64"), + Node("iosArm32"), + Node("iosX64"), + Node("iosSimulatorArm64") + ), + Node( + "tvos", + Node("tvosArm64"), + Node("tvosX64"), + Node("tvosSimulatorArm64") + ), + Node( + "watchos", + Node("watchosArm32"), + Node("watchosArm64"), + Node("watchosX64"), + Node("watchosSimulatorArm64"), + Node("watchosDeviceArm64"), + Node("watchosX86") + ) + ) + ) + ) + + private fun Node.collectLeafs(to: MutableMap, depth: Int): Set { + val leafs = mutableSetOf() + if (children.isEmpty()) { + leafs.add(name) + } else { + children.forEach { leafs.addAll(it.collectLeafs(to, depth + 1)) } + } + to[name] = NodeClosure(this, depth, leafs) + return leafs + } + + init { + val index = mutableMapOf() + val rootDepth = 0 + val leafs = hierarchy.collectLeafs(index, rootDepth + 1) + index[hierarchy.name] = NodeClosure(hierarchy, rootDepth, leafs) + hierarchyIndex = index + } + + fun parent(targetOrGroup: String): String? { + return hierarchyIndex[targetOrGroup]?.node?.parent?.name + } + + fun targets(targetOrGroup: String): Set { + return hierarchyIndex[targetOrGroup]?.allLeafs ?: emptySet() + } +} + +internal val konanTargetNameMapping = mapOf( + "android_x64" to "androidNativeX64", + "android_x86" to "androidNativeX86", + "android_arm32" to "androidNativeArm32", + "android_arm64" to "androidNativeArm64", + "ios_arm64" to "iosArm64", + "ios_x64" to "iosX64", + "ios_simulator_arm64" to "iosSimulatorArm64", + "watchos_arm32" to "watchosArm32", + "watchos_arm64" to "watchosArm64", + "watchos_x64" to "watchosX64", + "watchos_simulator_arm64" to "watchosSimulatorArm64", + "watchos_device_arm64" to "watchosDeviceArm64", + "tvos_arm64" to "tvosArm64", + "tvos_x64" to "tvosX64", + "tvos_simulator_arm64" to "tvosSimulatorArm64", + "linux_x64" to "linuxX64", + "mingw_x64" to "mingwX64", + "macos_x64" to "macosX64", + "macos_arm64" to "macosArm64", + "linux_arm64" to "linuxArm64", + "ios_arm32" to "iosArm32", + "watchos_x86" to "watchosX86", + "linux_arm32_hfp" to "linuxArm32Hfp", + "mingw_x86" to "mingwX86" +) diff --git a/src/test/kotlin/samples/KlibDumpSamples.kt b/src/test/kotlin/samples/KlibDumpSamples.kt new file mode 100644 index 00000000..f3cf5e01 --- /dev/null +++ b/src/test/kotlin/samples/KlibDumpSamples.kt @@ -0,0 +1,290 @@ +/* + * 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 samples + +import kotlinx.validation.ExperimentalBCVApi +import kotlinx.validation.api.klib.KlibDump +import kotlinx.validation.api.klib.KlibTarget +import kotlinx.validation.api.klib.inferAbi +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TemporaryFolder +import java.io.File +import kotlin.test.assertEquals + +class KlibDumpSamples { + @JvmField + @Rule + var tempFolder = TemporaryFolder() + + fun createDumpWithContent(content: String): File { + val file = tempFolder.newFile() + file.writer().use { + it.write(content) + } + return file + } + + @OptIn(ExperimentalBCVApi::class) + @Test + fun mergeDumps() { + val linuxX64Dump = createDumpWithContent(""" + // Rendering settings: + // - Signature version: 2 + // - Show manifest properties: false + // - Show declarations: true + + // Library unique name: + // Platform: NATIVE + // Native targets: linux_x64 + // Compiler version: 1.9.22 + // ABI version: 1.8.0 + final class org.example/ShardedClass { // org.example/ShardedClass|null[0] + final val value // org.example/ShardedClass.value|{}value[0] + final fun (): kotlin/Int // org.example/ShardedClass.value.|(){}[0] + constructor (kotlin/Int) // org.example/ShardedClass.|(kotlin.Int){}[0] + final fun add(kotlin/Int): kotlin/Int // org.example/ShardedClass.add|add(kotlin.Int){}[0] + } + final fun org.example/ShardedClass(kotlin/Int, kotlin/Float, kotlin/Long): org.example/ShardedClass // org.example/ShardedClass|ShardedClass(kotlin.Int;kotlin.Float;kotlin.Long){}[0] + """.trimIndent()) + + val linuxArm64Dump = createDumpWithContent(""" + // Rendering settings: + // - Signature version: 2 + // - Show manifest properties: false + // - Show declarations: true + + // Library unique name: + // Platform: NATIVE + // Native targets: linux_arm64 + // Compiler version: 1.9.22 + // ABI version: 1.8.0 + final class org.example/ShardedClass { // org.example/ShardedClass|null[0] + final val value // org.example/ShardedClass.value|{}value[0] + final fun (): kotlin/Int // org.example/ShardedClass.value.|(){}[0] + constructor (kotlin/Int) // org.example/ShardedClass.|(kotlin.Int){}[0] + final fun add(kotlin/Int): kotlin/Int // org.example/ShardedClass.add|add(kotlin.Int){}[0] + } + """.trimIndent()) + + val mergedDump = KlibDump().apply { + merge(linuxX64Dump) + merge(linuxArm64Dump) + } + val mergedDumpContent = buildString { mergedDump.saveTo(this) } + + assertEquals(""" + // Klib ABI Dump + // Targets: [linuxArm64, linuxX64] + // Rendering settings: + // - Signature version: 2 + // - Show manifest properties: false + // - Show declarations: true + + // Library unique name: + final class org.example/ShardedClass { // org.example/ShardedClass|null[0] + constructor (kotlin/Int) // org.example/ShardedClass.|(kotlin.Int){}[0] + final fun add(kotlin/Int): kotlin/Int // org.example/ShardedClass.add|add(kotlin.Int){}[0] + final val value // org.example/ShardedClass.value|{}value[0] + final fun (): kotlin/Int // org.example/ShardedClass.value.|(){}[0] + } + // Targets: [linuxX64] + final fun org.example/ShardedClass(kotlin/Int, kotlin/Float, kotlin/Long): org.example/ShardedClass // org.example/ShardedClass|ShardedClass(kotlin.Int;kotlin.Float;kotlin.Long){}[0] + + """.trimIndent(), mergedDumpContent) + } + + @OptIn(ExperimentalBCVApi::class) + @Test + fun mergeDumpObjects() { + val linuxX64Dump = createDumpWithContent(""" + // Rendering settings: + // - Signature version: 2 + // - Show manifest properties: false + // - Show declarations: true + + // Library unique name: + // Platform: NATIVE + // Native targets: linux_x64 + // Compiler version: 1.9.22 + // ABI version: 1.8.0 + final class org.example/ShardedClass { // org.example/ShardedClass|null[0] + final val value // org.example/ShardedClass.value|{}value[0] + final fun (): kotlin/Int // org.example/ShardedClass.value.|(){}[0] + constructor (kotlin/Int) // org.example/ShardedClass.|(kotlin.Int){}[0] + final fun add(kotlin/Int): kotlin/Int // org.example/ShardedClass.add|add(kotlin.Int){}[0] + } + final fun org.example/ShardedClass(kotlin/Int, kotlin/Float, kotlin/Long): org.example/ShardedClass // org.example/ShardedClass|ShardedClass(kotlin.Int;kotlin.Float;kotlin.Long){}[0] + """.trimIndent()) + + val linuxArm64Dump = createDumpWithContent(""" + // Rendering settings: + // - Signature version: 2 + // - Show manifest properties: false + // - Show declarations: true + + // Library unique name: + // Platform: NATIVE + // Native targets: linux_arm64 + // Compiler version: 1.9.22 + // ABI version: 1.8.0 + final class org.example/ShardedClass { // org.example/ShardedClass|null[0] + final val value // org.example/ShardedClass.value|{}value[0] + final fun (): kotlin/Int // org.example/ShardedClass.value.|(){}[0] + constructor (kotlin/Int) // org.example/ShardedClass.|(kotlin.Int){}[0] + final fun add(kotlin/Int): kotlin/Int // org.example/ShardedClass.add|add(kotlin.Int){}[0] + } + """.trimIndent()) + + val mergedDump = KlibDump() + mergedDump.merge(KlibDump.from(linuxArm64Dump)) + mergedDump.merge(KlibDump.from(linuxX64Dump, configurableTargetName = "linuxX86_64")) + val mergedDumpContent = buildString { mergedDump.saveTo(this) } + + assertEquals(""" + // Klib ABI Dump + // Targets: [linuxArm64, linuxX64.linuxX86_64] + // Rendering settings: + // - Signature version: 2 + // - Show manifest properties: false + // - Show declarations: true + + // Library unique name: + final class org.example/ShardedClass { // org.example/ShardedClass|null[0] + constructor (kotlin/Int) // org.example/ShardedClass.|(kotlin.Int){}[0] + final fun add(kotlin/Int): kotlin/Int // org.example/ShardedClass.add|add(kotlin.Int){}[0] + final val value // org.example/ShardedClass.value|{}value[0] + final fun (): kotlin/Int // org.example/ShardedClass.value.|(){}[0] + } + // Targets: [linuxX64.linuxX86_64] + final fun org.example/ShardedClass(kotlin/Int, kotlin/Float, kotlin/Long): org.example/ShardedClass // org.example/ShardedClass|ShardedClass(kotlin.Int;kotlin.Float;kotlin.Long){}[0] + + """.trimIndent(), mergedDumpContent) + + mergedDump.remove(listOf(KlibTarget.parse("linuxX64.linuxX86_64"))) + val filteredDumpContent = buildString { mergedDump.saveTo(this) } + assertEquals(""" + // Klib ABI Dump + // Targets: [linuxArm64] + // Rendering settings: + // - Signature version: 2 + // - Show manifest properties: false + // - Show declarations: true + + // Library unique name: + final class org.example/ShardedClass { // org.example/ShardedClass|null[0] + constructor (kotlin/Int) // org.example/ShardedClass.|(kotlin.Int){}[0] + final fun add(kotlin/Int): kotlin/Int // org.example/ShardedClass.add|add(kotlin.Int){}[0] + final val value // org.example/ShardedClass.value|{}value[0] + final fun (): kotlin/Int // org.example/ShardedClass.value.|(){}[0] + } + + """.trimIndent(), filteredDumpContent) + } + + @OptIn(ExperimentalBCVApi::class) + @Test + fun extractTargets() { + // Oh no, we're running on Windows and Apple targets are unsupported, let's filter it out! + val mergedDump = createDumpWithContent(""" + // Klib ABI Dump + // Targets: [iosArm64, iosSimulatorArm64, iosX64, linuxArm64, linuxX64] + // Rendering settings: + // - Signature version: 2 + // - Show manifest properties: true + // - Show declarations: true + + // Library unique name: + abstract class examples.classes/Klass // examples.classes/Klass|null[0] + // Targets: [iosArm64, iosX64] + abstract interface examples.classes/Iface // examples.classes/Iface|null[0] + + """.trimIndent()) + + val dump = KlibDump.from(mergedDump) + assertEquals( + listOf("iosArm64", "iosSimulatorArm64", "iosX64", "linuxArm64", "linuxX64").map(KlibTarget::parse).toSet(), + dump.targets + ) + // remove everything except linux* + dump.retain(dump.targets.filter { it.targetName.startsWith("linux") }) + + val filteredDumpContent = buildString { dump.saveTo(this) } + assertEquals(""" + // Klib ABI Dump + // Targets: [linuxArm64, linuxX64] + // Rendering settings: + // - Signature version: 2 + // - Show manifest properties: true + // - Show declarations: true + + // Library unique name: + abstract class examples.classes/Klass // examples.classes/Klass|null[0] + + """.trimIndent(), + filteredDumpContent) + } + + @OptIn(ExperimentalBCVApi::class) + @Test + fun inferDump() { + // We want to get a dump for iosArm64, but our host compiler doesn't support it. + val unsupportedTarget = KlibTarget.parse("iosArm64") + // Thankfully, we have an old merged dump ... + val oldMergedDump = createDumpWithContent(""" + // Klib ABI Dump + // Targets: [iosArm64, linuxArm64] + // Rendering settings: + // - Signature version: 2 + // - Show manifest properties: true + // - Show declarations: true + + // Library unique name: + abstract class examples.classes/Klass // examples.classes/Klass|null[0] + // Targets: [iosArm64] + abstract interface examples.classes/Iface // examples.classes/Iface|null[0] + + """.trimIndent()) + + // ... and a new dump for linuxArm64 + val linuxDump = createDumpWithContent(""" + // Klib ABI Dump + // Targets: [linuxArm64] + // Rendering settings: + // - Signature version: 2 + // - Show manifest properties: true + // - Show declarations: true + + // Library unique name: + abstract class examples.classes/NewKlass // examples.classes/NewKlass|null[0] + + """.trimIndent()) + + // Let's use these dumps to infer a public ABI on iosArm64 + val inferredIosArm64Dump = inferAbi( + unsupportedTarget = unsupportedTarget, + supportedTargetDumps = listOf(KlibDump.from(linuxDump)), + oldMergedDump = KlibDump.from(oldMergedDump)) + + assertEquals(unsupportedTarget, inferredIosArm64Dump.targets.single()) + + val inferredDumpContent = buildString { inferredIosArm64Dump.saveTo(this) } + assertEquals(""" + // Klib ABI Dump + // Targets: [iosArm64] + // Rendering settings: + // - Signature version: 2 + // - Show manifest properties: true + // - Show declarations: true + + // Library unique name: + abstract class examples.classes/NewKlass // examples.classes/NewKlass|null[0] + abstract interface examples.classes/Iface // examples.classes/Iface|null[0] + + """.trimIndent(), + inferredDumpContent) + } +} diff --git a/src/test/kotlin/tests/ClassNameConvertionTest.kt b/src/test/kotlin/tests/ClassNameConvertionTest.kt new file mode 100644 index 00000000..9777d4fb --- /dev/null +++ b/src/test/kotlin/tests/ClassNameConvertionTest.kt @@ -0,0 +1,44 @@ +/* + * Copyright 2016-2023 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. + */ + +@file:OptIn(ExperimentalLibraryAbiReader::class) + +package kotlinx.validation.api.tests + +import kotlinx.validation.api.klib.toAbiQualifiedName +import org.jetbrains.kotlin.library.abi.* +import org.junit.Test +import kotlin.test.assertEquals +import kotlin.test.assertNull + +class ClassNameConvertionTest { + @Test + fun testConvertBinaryName() { + assertNull("".toAbiQualifiedName()) + assertNull(" ".toAbiQualifiedName()) + assertNull("a/b/c/d".toAbiQualifiedName()) + assertNull("a.b.c/d.e".toAbiQualifiedName()) + + checkNames("Hello", AbiQualifiedName("", "Hello")) + checkNames("a.b.c", AbiQualifiedName("a.b", "c")) + checkNames("a\$b\$c", AbiQualifiedName("", "a.b.c")) + checkNames("p.a\$b\$c", AbiQualifiedName("p", "a.b.c")) + checkNames("org.example.Outer\$Inner\$\$serializer", + AbiQualifiedName("org.example", "Outer.Inner.\$serializer")) + checkNames("org.example.Outer\$Inner\$\$\$serializer", + AbiQualifiedName("org.example", "Outer.Inner.\$\$serializer")) + checkNames("a.b.e.s.c.MapStream\$Stream\$", + AbiQualifiedName("a.b.e.s.c", "MapStream.Stream\$")) + } + + private fun checkNames(binaryClassName: String, qualifiedName: AbiQualifiedName) { + val converted = binaryClassName.toAbiQualifiedName()!! + assertEquals(qualifiedName.packageName, converted.packageName) + assertEquals(qualifiedName.relativeName, converted.relativeName) + } +} + +private fun AbiQualifiedName(packageName: String, className: String) = + AbiQualifiedName(AbiCompoundName(packageName), AbiCompoundName(className)) diff --git a/src/test/kotlin/tests/KlibAbiMergingTest.kt b/src/test/kotlin/tests/KlibAbiMergingTest.kt new file mode 100644 index 00000000..b4fdf42c --- /dev/null +++ b/src/test/kotlin/tests/KlibAbiMergingTest.kt @@ -0,0 +1,339 @@ +/* + * 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 tests + +import kotlinx.validation.api.klib.KlibAbiDumpMerger +import kotlinx.validation.api.klib.KlibTarget +import org.junit.Rule +import org.junit.rules.TemporaryFolder +import java.io.File +import java.io.FileWriter +import java.nio.file.Files +import java.util.UUID +import kotlin.random.Random +import kotlin.test.* +import kotlin.test.Test + +class KlibAbiMergingTest { + @JvmField + @Rule + val tempDir = TemporaryFolder() + + private fun file(name: String): File { + val res = KlibAbiMergingTest::class.java.getResourceAsStream(name) + ?: throw IllegalStateException("Resource not found: $name") + val tempFile = File(tempDir.root, UUID.randomUUID().toString()) + Files.copy(res, tempFile.toPath()) + return tempFile + } + + private fun lines(name: String): Sequence { + val res = KlibAbiMergingTest::class.java.getResourceAsStream(name) + ?: throw IllegalStateException("Resource not found: $name") + return res.bufferedReader().lineSequence() + } + + private fun dumpToFile(klib: KlibAbiDumpMerger): File { + val file = tempDir.newFile() + FileWriter(file).use { + klib.dump(it) + } + return file + } + + @Test + fun identicalDumpFiles() { + val klib = KlibAbiDumpMerger() + klib.merge(file("/merge/identical/dump_macos_arm64.abi")) + klib.merge(file("/merge/identical/dump_linux_x64.abi")) + val merged = dumpToFile(klib) + + assertContentEquals( + lines("/merge/identical/merged.abi"), + Files.readAllLines(merged.toPath()).asSequence() + ) + } + + @Test + fun identicalDumpFilesWithAliases() { + val klib = KlibAbiDumpMerger() + klib.merge(file("/merge/identical/dump_macos_arm64.abi")) + klib.merge(file("/merge/identical/dump_linux_x64.abi")) + val merged = dumpToFile(klib) + + // there are no groups other than "all", so no aliases will be added + assertContentEquals( + lines("/merge/identical/merged.abi"), + Files.readAllLines(merged.toPath()).asSequence() + ) + } + + @Test + fun divergingDumpFiles() { + val targets = mutableListOf("androidNativeArm64", "linuxArm64", "linuxX64", "tvosX64") + val random = Random(42) + for (i in 0 until 10) { + val klib = KlibAbiDumpMerger() + targets.shuffle(random) + targets.forEach { + klib.merge(file("/merge/diverging/$it.api")) + } + val merged = dumpToFile(klib) + assertContentEquals( + lines("/merge/diverging/merged.abi"), + Files.readAllLines(merged.toPath()).asSequence(), + merged.readText() + ) + } + } + + @Test + fun divergingDumpFilesWithAliases() { + val random = Random(42) + for (i in 0 until 10) { + val klib = KlibAbiDumpMerger() + val targets = mutableListOf("androidNativeArm64", "linuxArm64", "linuxX64", "tvosX64") + targets.shuffle(random) + targets.forEach { + klib.merge(file("/merge/diverging/$it.api")) + } + val merged = dumpToFile(klib) + assertContentEquals( + lines("/merge/diverging/merged_with_aliases.abi"), + Files.readAllLines(merged.toPath()).asSequence() + ) + } + } + + @Test + fun mergeDumpsWithDivergedHeaders() { + val klib = KlibAbiDumpMerger() + klib.merge(file("/merge/header-mismatch/v1.abi"), "linuxArm64") + + assertFailsWith { + klib.merge(file("/merge/header-mismatch/v2.abi"), "linuxX64") + } + } + + @Test + fun overwriteAll() { + val klib = KlibAbiDumpMerger() + klib.merge(file("/merge/diverging/merged.abi")) + + val targets = listOf("androidNativeArm64", "linuxArm64", "linuxX64", "tvosX64") + targets.forEach { target -> + klib.remove(KlibTarget(target)) + klib.merge(file("/merge/diverging/$target.api")) + } + + val merged = dumpToFile(klib) + + assertContentEquals( + lines("/merge/diverging/merged.abi"), + Files.readAllLines(merged.toPath()).asSequence() + ) + } + + @Test + fun read() { + val klib = KlibAbiDumpMerger() + klib.merge(file("/merge/idempotent/bcv-klib-test.abi")) + + val written = dumpToFile(klib) + assertContentEquals( + lines("/merge/idempotent/bcv-klib-test.abi"), + Files.readAllLines(written.toPath()).asSequence() + ) + } + + @Test + fun readDeclarationWithNarrowerChildrenDeclarations() { + val klib = KlibAbiDumpMerger() + klib.merge(file("/merge/parseNarrowChildrenDecls/merged.abi")) + + klib.remove(KlibTarget("linuxArm64")) + val written1 = dumpToFile(klib) + assertContentEquals( + lines("/merge/parseNarrowChildrenDecls/withoutLinuxArm64.abi"), + Files.readAllLines(written1.toPath()).asSequence() + ) + + klib.remove(KlibTarget("linuxX64")) + val written2 = dumpToFile(klib) + assertContentEquals( + lines("/merge/parseNarrowChildrenDecls/withoutLinuxAll.abi"), + Files.readAllLines(written2.toPath()).asSequence() + ) + } + + @Test + fun guessAbi() { + val klib = KlibAbiDumpMerger() + klib.merge(file("/merge/guess/merged.api")) + klib.retainTargetSpecificAbi(KlibTarget("linuxArm64")) + + val retainedLinuxAbiDump = dumpToFile(klib) + assertContentEquals( + lines("/merge/guess/linuxArm64Specific.api"), + Files.readAllLines(retainedLinuxAbiDump.toPath()).asSequence() + ) + + val commonAbi = KlibAbiDumpMerger() + commonAbi.merge(file("/merge/guess/merged.api")) + commonAbi.remove(KlibTarget("linuxArm64")) + commonAbi.retainCommonAbi() + + val commonAbiDump = dumpToFile(commonAbi) + assertContentEquals( + lines("/merge/guess/common.api"), + Files.readAllLines(commonAbiDump.toPath()).asSequence() + ) + + commonAbi.mergeTargetSpecific(klib) + commonAbi.overrideTargets(setOf(KlibTarget("linuxArm64"))) + + val guessedAbiDump = dumpToFile(commonAbi) + assertContentEquals( + lines("/merge/guess/guessed.api"), + Files.readAllLines(guessedAbiDump.toPath()).asSequence() + ) + } + + @Test + fun loadInvalidFile() { + assertFails { + KlibAbiDumpMerger().merge(file("/merge/illegalFiles/emptyFile.txt")) + } + + assertFails { + KlibAbiDumpMerger().merge(file("/merge/illegalFiles/nonDumpFile.txt")) + } + + assertFails { + KlibAbiDumpMerger().merge(file("/merge/illegalFiles/emptyFile.txt"), "linuxX64") + } + + assertFails { + KlibAbiDumpMerger().merge(file("/merge/illegalFiles/nonDumpFile.txt"), "linuxX64") + } + + assertFails { + // Not a single-target dump + KlibAbiDumpMerger().merge(file("/merge/diverging/merged.api"), "linuxX64") + } + + assertFails { + KlibAbiDumpMerger().apply { + merge(file("/merge/stdlib_native_common.abi"), "linuxArm64") + } + } + } + + @Test + fun webTargets() { + val klib = KlibAbiDumpMerger() + klib.merge(file("/merge/webTargets/js.abi")) + klib.merge(file("/merge/webTargets/wasmWasi.abi"), "wasmWasi") + klib.merge(file("/merge/webTargets/wasmJs.abi"), "wasmJs") + + val merged = dumpToFile(klib) + + assertContentEquals( + lines("/merge/webTargets/merged.abi"), + Files.readAllLines(merged.toPath()).asSequence() + ) + } + + @Test + fun unqualifiedWasmTarget() { + // currently, there's no way to distinguish wasmWasi from wasmJs + assertFailsWith { + KlibAbiDumpMerger().merge(file("/merge/webTargets/wasmWasi.abi")) + } + } + + @Test + fun intersectingTargets() { + val dump = KlibAbiDumpMerger().apply { + merge(file("/merge/diverging/merged.abi")) + } + assertFailsWith { + dump.merge(file("/merge/diverging/linuxArm64.api")) + } + // but here, we're loading a dump for different target (configuredName changed) + dump.merge(file("/merge/diverging/linuxArm64.api"), "custom") + } + + @Test + fun customTargetNames() { + val lib = KlibAbiDumpMerger().apply { + merge(file("/merge/diverging/androidNativeArm64.api"), "android") + merge(file("/merge/diverging/linuxArm64.api"), "linux") + merge(file("/merge/diverging/linuxX64.api")) + merge(file("/merge/diverging/tvosX64.api")) + } + + val dump = dumpToFile(lib) + assertContentEquals( + lines("/merge/diverging/merged_with_aliases_and_custom_names.abi"), + Files.readAllLines(dump.toPath()).asSequence() + ) + } + + @Test + fun customTargetExtraction() { + val lib = KlibAbiDumpMerger().apply { + merge(file("/merge/diverging/merged_with_aliases_and_custom_names.abi")) + } + val targets = lib.targets.filter { it.targetName != "linuxArm64" } + targets.forEach { lib.remove(it) } + val extracted = dumpToFile(lib) + assertContentEquals( + lines("/merge/diverging/linuxArm64.extracted.api"), + Files.readAllLines(extracted.toPath()).asSequence() + ) + } + + @Test + fun webTargetsExtraction() { + val mergedPath = "/merge/webTargets/merged.abi" + + fun checkExtracted(targetName: String, expectedFile: String) { + val lib = KlibAbiDumpMerger().apply { merge(file(mergedPath)) } + val targets = lib.targets + targets.filter { it.configurableName != targetName }.forEach { lib.remove(it) } + val dump = dumpToFile(lib) + assertContentEquals( + lines(expectedFile), + Files.readAllLines(dump.toPath()).asSequence(), + "Dumps mismatched for target $targetName" + ) + } + + checkExtracted("js", "/merge/webTargets/js.ext.abi") + checkExtracted("wasmWasi", "/merge/webTargets/wasmWasi.ext.abi") + checkExtracted("wasmJs", "/merge/webTargets/wasmJs.ext.abi") + } + + @Test + fun loadMultiTargetDump() { + val lib = KlibAbiDumpMerger().apply { + merge(file("/merge/stdlib_native_common.abi")) + } + val expectedTargetNames = listOf( + "androidNativeArm32", "androidNativeArm64", "androidNativeX64", "androidNativeX86", + "iosArm64", "iosSimulatorArm64", "iosX64", "linuxArm32Hfp", "linuxArm64", "linuxX64", + "macosArm64", "macosX64", "mingwX64", "tvosArm64", "tvosSimulatorArm64", "tvosX64", + "watchosArm32", "watchosArm64", "watchosDeviceArm64", "watchosSimulatorArm64", "watchosX64" + ) + val expectedTargets = expectedTargetNames.asSequence().map(KlibTarget::parse).toSet() + assertEquals(expectedTargets, lib.targets) + + assertFailsWith { + KlibAbiDumpMerger().merge(file("/merge/stdlib_native_common.abi"), "target") + } + } +} diff --git a/src/test/kotlin/tests/KlibDumpTest.kt b/src/test/kotlin/tests/KlibDumpTest.kt new file mode 100644 index 00000000..7915e4df --- /dev/null +++ b/src/test/kotlin/tests/KlibDumpTest.kt @@ -0,0 +1,659 @@ +/* + * 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 tests + +import kotlinx.validation.ExperimentalBCVApi +import kotlinx.validation.api.klib.* +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TemporaryFolder +import java.io.File +import java.io.FileNotFoundException +import java.util.* +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith +import kotlin.test.assertTrue + +private val rawLinuxDump = """ + // Rendering settings: + // - Signature version: 2 + // - Show manifest properties: false + // - Show declarations: true + + // Library unique name: + // Platform: NATIVE + // Native targets: linux_x64 + // Compiler version: 1.9.22 + // ABI version: 1.8.0 + final class org.example/ShardedClass { // org.example/ShardedClass|null[0] + final val value // org.example/ShardedClass.value|{}value[0] + final fun (): kotlin/Int // org.example/ShardedClass.value.|(){}[0] + constructor (kotlin/Int) // org.example/ShardedClass.|(kotlin.Int){}[0] + final fun add(kotlin/Int): kotlin/Int // org.example/ShardedClass.add|add(kotlin.Int){}[0] + } + final fun org.example/ShardedClass(kotlin/Int, kotlin/Float, kotlin/Long): org.example/ShardedClass // org.example/ShardedClass|ShardedClass(kotlin.Int;kotlin.Float;kotlin.Long){}[0] +""".trimIndent() + +private val mergedLinuxDump = """ + // Klib ABI Dump + // Targets: [linuxX64] + // Rendering settings: + // - Signature version: 2 + // - Show manifest properties: false + // - Show declarations: true + + // Library unique name: + final class org.example/ShardedClass { // org.example/ShardedClass|null[0] + constructor (kotlin/Int) // org.example/ShardedClass.|(kotlin.Int){}[0] + final fun add(kotlin/Int): kotlin/Int // org.example/ShardedClass.add|add(kotlin.Int){}[0] + final val value // org.example/ShardedClass.value|{}value[0] + final fun (): kotlin/Int // org.example/ShardedClass.value.|(){}[0] + } + final fun org.example/ShardedClass(kotlin/Int, kotlin/Float, kotlin/Long): org.example/ShardedClass // org.example/ShardedClass|ShardedClass(kotlin.Int;kotlin.Float;kotlin.Long){}[0] + +""".trimIndent() + + +private val mergedLinuxDumpWithTargetSpecificDeclaration = """ + // Klib ABI Dump + // Targets: [linuxArm64, linuxX64] + // Rendering settings: + // - Signature version: 2 + // - Show manifest properties: false + // - Show declarations: true + + // Library unique name: + final class org.example/ShardedClass { // org.example/ShardedClass|null[0] + constructor (kotlin/Int) // org.example/ShardedClass.|(kotlin.Int){}[0] + final val value // org.example/ShardedClass.value|{}value[0] + final fun (): kotlin/Int // org.example/ShardedClass.value.|(){}[0] + // Targets: [linuxArm64] + final fun add2(kotlin/Int): kotlin/Int // org.example/ShardedClass.add|add(kotlin.Int){}[0] + // Targets: [linuxX64] + final fun add(kotlin/Int): kotlin/Int // org.example/ShardedClass.add|add(kotlin.Int){}[0] + } + // Targets: [linuxArm64] + final fun org.example/ShardedClass2(kotlin/Int, kotlin/Float, kotlin/Long): org.example/ShardedClass // org.example/ShardedClass|ShardedClass(kotlin.Int;kotlin.Float;kotlin.Long){}[0] + // Targets: [linuxX64] + final fun org.example/ShardedClass(kotlin/Int, kotlin/Float, kotlin/Long): org.example/ShardedClass // org.example/ShardedClass|ShardedClass(kotlin.Int;kotlin.Float;kotlin.Long){}[0] + +""".trimIndent() + +private val mergedLinuxArm64Dump = """ + // Klib ABI Dump + // Targets: [linuxArm64] + // Rendering settings: + // - Signature version: 2 + // - Show manifest properties: false + // - Show declarations: true + + // Library unique name: + final class org.example/ShardedClass { // org.example/ShardedClass|null[0] + constructor (kotlin/Int) // org.example/ShardedClass.|(kotlin.Int){}[0] + final val value // org.example/ShardedClass.value|{}value[0] + final fun (): kotlin/Int // org.example/ShardedClass.value.|(){}[0] + final fun add2(kotlin/Int): kotlin/Int // org.example/ShardedClass.add|add(kotlin.Int){}[0] + } + final fun org.example/ShardedClass2(kotlin/Int, kotlin/Float, kotlin/Long): org.example/ShardedClass // org.example/ShardedClass|ShardedClass(kotlin.Int;kotlin.Float;kotlin.Long){}[0] + +""".trimIndent() + +private val mergedLinuxDumpWithCustomName = """ + // Klib ABI Dump + // Targets: [linuxX64.testTarget] + // Rendering settings: + // - Signature version: 2 + // - Show manifest properties: false + // - Show declarations: true + + // Library unique name: + final class org.example/ShardedClass { // org.example/ShardedClass|null[0] + constructor (kotlin/Int) // org.example/ShardedClass.|(kotlin.Int){}[0] + final fun add(kotlin/Int): kotlin/Int // org.example/ShardedClass.add|add(kotlin.Int){}[0] + final val value // org.example/ShardedClass.value|{}value[0] + final fun (): kotlin/Int // org.example/ShardedClass.value.|(){}[0] + } + final fun org.example/ShardedClass(kotlin/Int, kotlin/Float, kotlin/Long): org.example/ShardedClass // org.example/ShardedClass|ShardedClass(kotlin.Int;kotlin.Float;kotlin.Long){}[0] + +""".trimIndent() + +private val rawMultitargetDump = """ + // Rendering settings: + // - Signature version: 2 + // - Show manifest properties: true + // - Show declarations: true + + // Library unique name: + // Platform: NATIVE + // Native targets: android_arm32, android_arm64, android_x64, android_x86, ios_arm64, ios_simulator_arm64, ios_x64, linux_arm32_hfp, linux_arm64, linux_x64, macos_arm64, macos_x64, mingw_x64, tvos_arm64, tvos_simulator_arm64, tvos_x64, watchos_arm32, watchos_arm64, watchos_device_arm64, watchos_simulator_arm64, watchos_x64 + // Compiler version: 2.0.255-SNAPSHOT + // ABI version: 1.8.0 + abstract interface kotlin/Annotation // kotlin/Annotation|null[0] +""".trimIndent() + +private val mergedMultitargetDump = """ + // Klib ABI Dump + // Targets: [androidNativeArm32, androidNativeArm64, androidNativeX64, androidNativeX86, iosArm64, iosSimulatorArm64, iosX64, linuxArm32Hfp, linuxArm64, linuxX64, macosArm64, macosX64, mingwX64, tvosArm64, tvosSimulatorArm64, tvosX64, watchosArm32, watchosArm64, watchosDeviceArm64, watchosSimulatorArm64, watchosX64] + // Rendering settings: + // - Signature version: 2 + // - Show manifest properties: true + // - Show declarations: true + + // Library unique name: + abstract interface kotlin/Annotation // kotlin/Annotation|null[0] + +""".trimIndent() + + +private val mergedMultitargetDumpFiltered = """ + // Klib ABI Dump + // Targets: [androidNativeArm32] + // Rendering settings: + // - Signature version: 2 + // - Show manifest properties: true + // - Show declarations: true + + // Library unique name: + abstract interface kotlin/Annotation // kotlin/Annotation|null[0] + +""".trimIndent() + +// Note that some cases are already covered in KlibDumpSamples.kt and not duplicated here +@OptIn(ExperimentalBCVApi::class) +class KlibDumpTest { + @JvmField + @Rule + var tmpFolder = TemporaryFolder() + + private fun asFile(dump: String): File { + val file = tmpFolder.newFile() + file.bufferedWriter().use { it.write(dump) } + return file + } + + @Test + fun emptyDump() { + val dump = buildString { + KlibDump().saveTo(this) + } + assertEquals("", dump) + + assertFailsWith { + KlibDump.from(tmpFolder.newFile()) + } + } + + @Test + fun loadFromNonExistingFile() { + assertFailsWith { + KlibDump.from(tmpFolder.root.resolve(UUID.randomUUID().toString())) + } + assertFailsWith { + KlibDump().merge(tmpFolder.root.resolve(UUID.randomUUID().toString())) + } + assertFailsWith { + KlibDump.fromKlib(tmpFolder.root.resolve(UUID.randomUUID().toString())) + } + assertFailsWith { + KlibDump().mergeFromKlib(tmpFolder.root.resolve(UUID.randomUUID().toString())) + } + } + + @Test + fun loadKlibFromNonKlib() { + assertFailsWith { KlibDump.fromKlib(tmpFolder.root) } + assertFailsWith { KlibDump.fromKlib(tmpFolder.newFile()) } + + assertFailsWith { KlibDump().also { it.mergeFromKlib(tmpFolder.root) } } + assertFailsWith { KlibDump().also { it.mergeFromKlib(tmpFolder.newFile()) } } + } + + @Test + fun loadFromDirectory() { + assertFailsWith { + KlibDump.from(tmpFolder.root) + } + assertFailsWith { + KlibDump().merge(tmpFolder.root) + } + } + + @Test + fun loadDumpWithSingleTarget() { + val klibDump = KlibDump.from(asFile(rawLinuxDump)) + assertEquals(setOf(KlibTarget.parse("linuxX64")), klibDump.targets) + assertEquals(mergedLinuxDump, buildString { klibDump.saveTo(this) }) + + val mergedKlibDump = KlibDump.from(asFile(mergedLinuxDump)) + assertEquals(setOf(KlibTarget.parse("linuxX64")), mergedKlibDump.targets) + assertEquals(mergedLinuxDump, buildString { mergedKlibDump.saveTo(this) }) + } + + @Test + fun mergeDumpWithSingleTarget() { + val klibDump = KlibDump().also { it.merge(asFile(rawLinuxDump)) } + assertEquals(setOf(KlibTarget.parse("linuxX64")), klibDump.targets) + assertEquals(mergedLinuxDump, buildString { klibDump.saveTo(this) }) + + val mergedKlibDump = KlibDump().also { it.merge(asFile(mergedLinuxDump)) } + assertEquals(setOf(KlibTarget.parse("linuxX64")), mergedKlibDump.targets) + assertEquals(mergedLinuxDump, buildString { mergedKlibDump.saveTo(this) }) + } + + @Test + fun loadDumpWithSingleTargetWithCustomName() { + val klibDump = KlibDump.from(asFile(rawLinuxDump), configurableTargetName = "testTarget") + assertEquals(setOf(KlibTarget.parse("linuxX64.testTarget")), klibDump.targets) + assertEquals(mergedLinuxDumpWithCustomName, buildString { klibDump.saveTo(this) }) + + val mergedKlibDump = KlibDump.from(asFile(mergedLinuxDump), configurableTargetName = "testTarget") + assertEquals(setOf(KlibTarget.parse("linuxX64.testTarget")), mergedKlibDump.targets) + assertEquals(mergedLinuxDumpWithCustomName, buildString { mergedKlibDump.saveTo(this) }) + + val customTargetDump = KlibDump.from(asFile(mergedLinuxDumpWithCustomName)) + assertEquals(setOf(KlibTarget.parse("linuxX64.testTarget")), customTargetDump.targets) + assertEquals(mergedLinuxDumpWithCustomName, buildString { customTargetDump.saveTo(this) }) + } + + @Test + fun mergeDumpWithSingleTargetWithCustomName() { + val klibDump = KlibDump().also { it.merge(asFile(rawLinuxDump), configurableTargetName = "testTarget") } + assertEquals(setOf(KlibTarget.parse("linuxX64.testTarget")), klibDump.targets) + assertEquals(mergedLinuxDumpWithCustomName, buildString { klibDump.saveTo(this) }) + + val mergedKlibDump = + KlibDump().also { it.merge(asFile(mergedLinuxDump), configurableTargetName = "testTarget") } + assertEquals(setOf(KlibTarget.parse("linuxX64.testTarget")), mergedKlibDump.targets) + assertEquals(mergedLinuxDumpWithCustomName, buildString { mergedKlibDump.saveTo(this) }) + + val customTargetDump = KlibDump().also { it.merge(asFile(mergedLinuxDumpWithCustomName)) } + assertEquals(setOf(KlibTarget.parse("linuxX64.testTarget")), customTargetDump.targets) + assertEquals(mergedLinuxDumpWithCustomName, buildString { customTargetDump.saveTo(this) }) + } + + @Test + fun loadMultitargetDump() { + val dump = KlibDump.from(asFile(rawMultitargetDump)) + assertEquals(21, dump.targets.size) + assertEquals(mergedMultitargetDump, buildString { dump.saveTo(this) }) + + val mergedDump = KlibDump.from(asFile(mergedMultitargetDump)) + assertEquals(21, mergedDump.targets.size) + assertEquals(mergedMultitargetDump, buildString { mergedDump.saveTo(this) }) + } + + @Test + fun mergeMultitargetDump() { + val dump = KlibDump().also { it.merge(asFile(rawMultitargetDump)) } + assertEquals(21, dump.targets.size) + assertEquals(mergedMultitargetDump, buildString { dump.saveTo(this) }) + + val mergedDump = KlibDump().also { it.merge(asFile(mergedMultitargetDump)) } + assertEquals(21, mergedDump.targets.size) + assertEquals(mergedMultitargetDump, buildString { mergedDump.saveTo(this) }) + } + + @Test + fun loadMultitargetDumpUsingCustomName() { + assertFailsWith { + KlibDump.from(asFile(rawMultitargetDump), "abc") + } + assertFailsWith { + KlibDump().also { it.merge(asFile(rawMultitargetDump), "abc") } + } + + assertFailsWith { + KlibDump.from(asFile(mergedMultitargetDump), "abc") + } + assertFailsWith { + KlibDump().also { it.merge(asFile(mergedMultitargetDump), "abc") } + } + } + + @Test + fun retainAll() { + val dump = KlibDump.from(asFile(mergedMultitargetDump)) + val oldTargets = setOf(*dump.targets.toTypedArray()) + + dump.retain(oldTargets) + assertEquals(oldTargets, dump.targets) + assertEquals(mergedMultitargetDump, buildString { dump.saveTo(this) }) + } + + @Test + fun retainSingle() { + val dump = KlibDump.from(asFile(mergedMultitargetDump)) + val singleTarget = KlibTarget.parse("androidNativeArm32") + + dump.retain(setOf(singleTarget)) + assertEquals(setOf(singleTarget), dump.targets) + assertEquals(mergedMultitargetDumpFiltered, buildString { dump.saveTo(this) }) + } + + @Test + fun retainNone() { + val dump = KlibDump.from(asFile(mergedMultitargetDump)) + dump.retain(emptySet()) + + assertTrue(dump.targets.isEmpty()) + assertEquals("", buildString { dump.saveTo(this) }) + } + + @Test + fun removeAll() { + val dump = KlibDump.from(asFile(mergedMultitargetDump)) + dump.remove(listOf(*dump.targets.toTypedArray())) + + assertTrue(dump.targets.isEmpty()) + assertEquals("", buildString { dump.saveTo(this) }) + } + + @Test + fun removeNone() { + val dump = KlibDump.from(asFile(mergedMultitargetDump)) + val oldTargets = setOf(*dump.targets.toTypedArray()) + + dump.remove(emptySet()) + assertEquals(oldTargets, dump.targets) + assertEquals(mergedMultitargetDump, buildString { dump.saveTo(this) }) + } + + @Test + fun removeSome() { + val dump = KlibDump.from(asFile(mergedMultitargetDump)) + val singleTarget = KlibTarget.parse("androidNativeArm32") + + dump.remove(dump.targets.subtract(setOf(singleTarget))) + assertEquals(setOf(singleTarget), dump.targets) + assertEquals(mergedMultitargetDumpFiltered, buildString { dump.saveTo(this) }) + } + + @Test + fun removeOrRetainTargetsNotPresentedInDump() { + val dump = KlibDump.from(asFile(mergedMultitargetDump)) + val targets = setOf(*dump.targets.toTypedArray()) + dump.remove(listOf(KlibTarget.parse("linuxX64.blablabla"))) + assertEquals(targets, dump.targets) + + dump.retain(listOf(KlibTarget.parse("macosArm64.macos"))) + assertTrue(dump.targets.isEmpty()) + } + + @Test + fun removeDeclarationsAlongWithTargets() { + val dump = KlibDump.from(asFile(mergedLinuxDumpWithTargetSpecificDeclaration)) + val toRemove = KlibTarget.parse("linuxArm64") + + dump.remove(listOf(toRemove)) + assertEquals(mergedLinuxDump, buildString { dump.saveTo(this) }) + } + + @Test + fun testCopy() { + val dump = KlibDump.from(asFile(mergedLinuxDumpWithTargetSpecificDeclaration)) + val copy = dump.copy() + + dump.remove(listOf(KlibTarget.parse("linuxArm64"))) + assertEquals(mergedLinuxDumpWithTargetSpecificDeclaration, buildString { copy.saveTo(this) }) + } + + @Test + fun testMergeDumps() { + val dump = KlibDump().also { + it.merge(asFile(mergedLinuxDump)) + it.merge(asFile(mergedLinuxArm64Dump)) + } + assertEquals(mergedLinuxDumpWithTargetSpecificDeclaration, buildString { dump.saveTo(this) }) + } + + @Test + fun mergeDumpsWithIntersectingTargets() { + val mergedDump = KlibDump.from(asFile(rawMultitargetDump)) + + assertFailsWith { + mergedDump.merge(asFile(rawMultitargetDump)) + } + + assertFailsWith { + mergedDump.merge(asFile(mergedLinuxDump)) + } + } + + @Test + fun inferWithoutAnOldDump() { + val unsupportedTarget = KlibTarget.parse("iosArm64") + + val linuxDump = KlibDump.from( + asFile( + """ + // Klib ABI Dump + // Targets: [linuxArm64] + // Rendering settings: + // - Signature version: 2 + // - Show manifest properties: true + // - Show declarations: true + + // Library unique name: + abstract class examples.classes/NewKlass // examples.classes/NewKlass|null[0] + """.trimIndent() + ) + ) + + // Let's use these dumps to infer a public ABI on iosArm64 + val inferredIosArm64Dump = inferAbi( + unsupportedTarget = unsupportedTarget, + supportedTargetDumps = listOf(linuxDump), + oldMergedDump = null + ) + + assertEquals(unsupportedTarget, inferredIosArm64Dump.targets.single()) + + val inferredDumpContent = buildString { inferredIosArm64Dump.saveTo(this) } + assertEquals( + """ + // Klib ABI Dump + // Targets: [iosArm64] + // Rendering settings: + // - Signature version: 2 + // - Show manifest properties: true + // - Show declarations: true + + // Library unique name: + abstract class examples.classes/NewKlass // examples.classes/NewKlass|null[0] + + """.trimIndent(), inferredDumpContent + ) + } + + @Test + fun inferFromAnOldDumpOnly() { + val unsupportedTarget = KlibTarget.parse("iosArm64") + + val oldDump = KlibDump.from( + asFile( + """ + // Klib ABI Dump + // Targets: [iosArm64, linuxArm64] + // Rendering settings: + // - Signature version: 2 + // - Show manifest properties: true + // - Show declarations: true + + // Library unique name: + abstract class examples.classes/Klass // examples.classes/Klass|null[0] + // Targets: [iosArm64] + abstract interface examples.classes/Iface // examples.classes/Iface|null[0] + + """.trimIndent() + ) + ) + + // Let's use these dumps to infer a public ABI on iosArm64 + val inferredIosArm64Dump = inferAbi( + unsupportedTarget = unsupportedTarget, + supportedTargetDumps = emptySet(), + oldMergedDump = oldDump + ) + + assertEquals(unsupportedTarget, inferredIosArm64Dump.targets.single()) + + val inferredDumpContent = buildString { inferredIosArm64Dump.saveTo(this) } + assertEquals( + """ + // Klib ABI Dump + // Targets: [iosArm64] + // Rendering settings: + // - Signature version: 2 + // - Show manifest properties: true + // - Show declarations: true + + // Library unique name: + abstract interface examples.classes/Iface // examples.classes/Iface|null[0] + + """.trimIndent(), inferredDumpContent + ) + } + + @Test + fun inferOutOfThinAir() { + val unsupportedTarget = KlibTarget.parse("iosArm64") + + assertFailsWith { + inferAbi(unsupportedTarget, emptySet(), null) + } + } + + @Test + fun inferFromSelf() { + val dump = KlibDump.from(asFile(mergedLinuxDump)) + assertFailsWith { + inferAbi(dump.targets.first(), listOf(dump)) + } + } + + @Test + fun inferFromIntersectingDumps() { + assertFailsWith { + inferAbi( + KlibTarget.parse("iosArm64.unsupported"), + listOf( + KlibDump.from(asFile(mergedLinuxDump)), + KlibDump.from(asFile(mergedMultitargetDump)) + ) + ) + } + } + + @Test + fun iterativeGrouping() { + val dump = KlibDump.from( + asFile( + """ + // Klib ABI Dump + // Targets: [androidNativeArm32, androidNativeArm64, androidNativeX64, androidNativeX86, linuxArm64, linuxX64, mingwX64] + // Rendering settings: + // - Signature version: 2 + // - Show manifest properties: true + // - Show declarations: true + + // Library unique name: + final class org.different.pack/BuildConfig { // org.different.pack/BuildConfig|null[0] + constructor () // org.different.pack/BuildConfig.|(){}[0] + final fun f1(): kotlin/Int // org.different.pack/BuildConfig.f1|f1(){}[0] + final val p1 // org.different.pack/BuildConfig.p1|{}p1[0] + final fun (): kotlin/Int // org.different.pack/BuildConfig.p1.|(){}[0] + } + // Targets: [androidNativeArm64] + final fun (org.different.pack/BuildConfig).org.different.pack/linuxArm64Specific(): kotlin/Int // org.different.pack/linuxArm64Specific|linuxArm64Specific@org.different.pack.BuildConfig(){}[0] + // Targets: [linuxArm64, linuxX64] + final fun (org.different.pack/BuildConfig).org.different.pack/linuxArm64Specific2(): kotlin/Int // org.different.pack/linuxArm64Specific2|linuxArm64Specific@org.different.pack.BuildConfig(){}[0] + // Targets: [androidNativeArm32, androidNativeArm64, androidNativeX64, androidNativeX86, linuxArm64, linuxX64] + final fun (org.different.pack/BuildConfig).org.different.pack/linuxArm64Specific3(): kotlin/Int // org.different.pack/linuxArm64Specific3|linuxArm64Specific@org.different.pack.BuildConfig(){}[0] + + """.trimIndent() + ) + ) + + val expectedDump = """ + // Klib ABI Dump + // Targets: [androidNativeArm32, androidNativeArm64, androidNativeX64, androidNativeX86, linuxArm64, linuxX64, mingwX64] + // Alias: androidNative => [androidNativeArm32, androidNativeArm64, androidNativeX64, androidNativeX86] + // Alias: linux => [linuxArm64, linuxX64] + // Rendering settings: + // - Signature version: 2 + // - Show manifest properties: true + // - Show declarations: true + + // Library unique name: + final class org.different.pack/BuildConfig { // org.different.pack/BuildConfig|null[0] + constructor () // org.different.pack/BuildConfig.|(){}[0] + final fun f1(): kotlin/Int // org.different.pack/BuildConfig.f1|f1(){}[0] + final val p1 // org.different.pack/BuildConfig.p1|{}p1[0] + final fun (): kotlin/Int // org.different.pack/BuildConfig.p1.|(){}[0] + } + // Targets: [androidNative, linux] + final fun (org.different.pack/BuildConfig).org.different.pack/linuxArm64Specific3(): kotlin/Int // org.different.pack/linuxArm64Specific3|linuxArm64Specific@org.different.pack.BuildConfig(){}[0] + // Targets: [linux] + final fun (org.different.pack/BuildConfig).org.different.pack/linuxArm64Specific2(): kotlin/Int // org.different.pack/linuxArm64Specific2|linuxArm64Specific@org.different.pack.BuildConfig(){}[0] + // Targets: [androidNativeArm64] + final fun (org.different.pack/BuildConfig).org.different.pack/linuxArm64Specific(): kotlin/Int // org.different.pack/linuxArm64Specific|linuxArm64Specific@org.different.pack.BuildConfig(){}[0] + + """.trimIndent() + assertEquals(expectedDump, buildString { dump.saveTo(this) }) + } + + @Test + fun similarGroupRemoval() { + // native function should use a group alias "ios", not "apple", or "native" + val dump = KlibDump.from( + asFile( + """ + // Klib ABI Dump + // Targets: [iosArm64, iosX64, js] + // Rendering settings: + // - Signature version: 2 + // - Show manifest properties: true + // - Show declarations: true + + // Library unique name: + final fun org.example/common(): kotlin/Int // com.example/common|common(){}[0] + // Targets: [iosArm64, iosX64] + final fun org.example/native(): kotlin/Int // com.example/native|native(){}[0] + + """.trimIndent() + ) + ) + + val expectedDump = """ + // Klib ABI Dump + // Targets: [iosArm64, iosX64, js] + // Alias: ios => [iosArm64, iosX64] + // Rendering settings: + // - Signature version: 2 + // - Show manifest properties: true + // - Show declarations: true + + // Library unique name: + final fun org.example/common(): kotlin/Int // com.example/common|common(){}[0] + // Targets: [ios] + final fun org.example/native(): kotlin/Int // com.example/native|native(){}[0] + + """.trimIndent() + assertEquals(expectedDump, buildString { dump.saveTo(this) }) + } + + @Test + fun saveToFile() { + val dump = KlibDump.from(asFile(mergedMultitargetDump)) + val tempFile = tmpFolder.newFile() + dump.saveTo(tempFile) + + assertEquals( + buildString { dump.saveTo(this) }, + tempFile.readText(Charsets.US_ASCII) + ) + } +} diff --git a/src/test/kotlin/tests/KlibSignatureVersionTest.kt b/src/test/kotlin/tests/KlibSignatureVersionTest.kt new file mode 100644 index 00000000..d08abb75 --- /dev/null +++ b/src/test/kotlin/tests/KlibSignatureVersionTest.kt @@ -0,0 +1,39 @@ +/* + * 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 tests + +import kotlinx.validation.api.klib.KlibSignatureVersion +import org.junit.Test +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith +import kotlin.test.assertNotEquals + +class KlibSignatureVersionTest { + @Test + fun signatureConstruction() { + assertFailsWith { KlibSignatureVersion.of(-1) } + assertFailsWith { KlibSignatureVersion.of(0) } + + val correctVersion = KlibSignatureVersion.of(42) + assertEquals(42, correctVersion.version) + } + + @Test + fun signaturesEqual() { + assertEquals(KlibSignatureVersion.of(1), KlibSignatureVersion.of(1)) + KlibSignatureVersion.of(2).also { + assertEquals(it, it) + } + + assertNotEquals(KlibSignatureVersion.of(2), KlibSignatureVersion.of(3)) + } + + @Test + fun signatureHashCode() { + assertEquals(KlibSignatureVersion.of(1).hashCode(), KlibSignatureVersion.of(1).hashCode()) + assertNotEquals(KlibSignatureVersion.of(1).hashCode(), KlibSignatureVersion.of(2).hashCode()) + } +} diff --git a/src/test/kotlin/tests/KlibTargetHierarchyTest.kt b/src/test/kotlin/tests/KlibTargetHierarchyTest.kt new file mode 100644 index 00000000..52a298be --- /dev/null +++ b/src/test/kotlin/tests/KlibTargetHierarchyTest.kt @@ -0,0 +1,69 @@ +/* + * 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.api.klib + +import org.jetbrains.kotlin.konan.target.KonanTarget +import org.junit.Test +import kotlin.test.assertContentEquals +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertTrue + +class KlibTargetHierarchyTest { + @Test + fun testHierarchy() { + assertContentEquals(listOf("linuxArm64", "linux", "native", "all"), + hierarchyFrom("linuxArm64")) + + assertContentEquals(listOf("js", "all"), + hierarchyFrom("js")) + + assertContentEquals(listOf("iosArm64", "ios", "apple", "native", "all"), + hierarchyFrom("iosArm64")) + + assertContentEquals(listOf("androidNative", "native", "all"), + hierarchyFrom("androidNative")) + + assertContentEquals(listOf("unknown"), hierarchyFrom("unknown")) + } + + @Test + fun testTargetsList() { + assertEquals(setOf("linuxX64"), TargetHierarchy.targets("linuxX64")) + assertEquals(setOf("macosX64", "macosArm64"), TargetHierarchy.targets("macos")) + assertEquals(emptySet(), TargetHierarchy.targets("unknown")) + } + + @Test + fun testEveryMappedTargetIsWithinTheHierarchy() { + konanTargetNameMapping.forEach { (underlyingTarget, name) -> + assertNotNull(TargetHierarchy.parent(name), + "Target $name.$underlyingTarget is missing from the hierarchy.") + } + } + + @Test + fun testAllTargetsAreMapped() { + val notMappedTargets = KonanTarget.predefinedTargets.keys.subtract(konanTargetNameMapping.keys) + assertEquals(setOf("wasm32", "linux_mips32", "linux_mipsel32"), notMappedTargets, + "Following targets are not mapped: $notMappedTargets") + } + + private fun hierarchyFrom(groupOrTarget: String): List { + return buildList { + var i = 0 + var group: String? = groupOrTarget + while (group != null) { + if (i > TargetHierarchy.hierarchyIndex.size) { + throw AssertionError("Cycle detected: $this") + } + add(group) + group = TargetHierarchy.parent(group) + i++ + } + } + } +} diff --git a/src/test/kotlin/tests/KlibTargetNameTest.kt b/src/test/kotlin/tests/KlibTargetNameTest.kt new file mode 100644 index 00000000..997cd220 --- /dev/null +++ b/src/test/kotlin/tests/KlibTargetNameTest.kt @@ -0,0 +1,97 @@ +/* + * 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 tests + +import kotlinx.validation.api.klib.KlibTarget +import org.junit.Test +import kotlin.test.assertEquals +import kotlin.test.assertFails +import kotlin.test.assertFailsWith +import kotlin.test.assertNotEquals + +class KlibTargetNameTest { + @Test + fun parse() { + assertEquals("a.b", KlibTarget("a", "b").toString()) + assertEquals("a", KlibTarget("a").toString()) + assertEquals("a", KlibTarget("a", "a").toString()) + + assertFailsWith { KlibTarget.parse("") } + assertFailsWith { KlibTarget.parse(" ") } + assertFailsWith { KlibTarget.parse("a.b.c") } + assertFailsWith { KlibTarget.parse("a.") } + assertFailsWith { KlibTarget.parse(".a") } + + KlibTarget.parse("a.b").also { + assertEquals("b", it.configurableName) + assertEquals("a", it.targetName) + } + + KlibTarget.parse("a.a").also { + assertEquals("a", it.configurableName) + assertEquals("a", it.targetName) + } + + KlibTarget.parse("a").also { + assertEquals("a", it.configurableName) + assertEquals("a", it.targetName) + } + } + + @Test + fun validate() { + assertFailsWith { + KlibTarget("a.b", "c") + } + assertFailsWith { + KlibTarget("a", "b.c") + } + } + + @Test + fun targetsEqual() { + assertEquals(KlibTarget.parse("androidNativeArm64"), KlibTarget.parse("androidNativeArm64")) + assertNotEquals(KlibTarget.parse("androidNativeArm64"), KlibTarget.parse("androidNativeArm32")) + + assertEquals( + KlibTarget.parse("androidNativeArm64.android"), KlibTarget.parse("androidNativeArm64.android") + ) + assertNotEquals( + KlibTarget.parse("androidNativeArm64.android"), KlibTarget.parse("androidNativeArm64") + ) + + assertEquals( + KlibTarget.parse("androidNativeArm64.androidNativeArm64"), + KlibTarget.parse("androidNativeArm64") + ) + } + + @Test + fun targetHashCode() { + assertEquals( + KlibTarget.parse("androidNativeArm64").hashCode(), + KlibTarget.parse("androidNativeArm64").hashCode() + ) + assertNotEquals( + KlibTarget.parse("androidNativeArm64").hashCode(), + KlibTarget.parse("androidNativeArm32").hashCode() + ) + + assertEquals( + KlibTarget.parse("androidNativeArm64.android").hashCode(), + KlibTarget.parse("androidNativeArm64.android").hashCode() + ) + assertNotEquals( + KlibTarget.parse("androidNativeArm64.android").hashCode(), + KlibTarget.parse("androidNativeArm64").hashCode() + ) + + assertEquals( + KlibTarget.parse("androidNativeArm64.androidNativeArm64").hashCode(), + KlibTarget.parse("androidNativeArm64").hashCode() + ) + } +} diff --git a/src/test/resources/merge/diverging/androidNativeArm64.api b/src/test/resources/merge/diverging/androidNativeArm64.api new file mode 100644 index 00000000..970b8860 --- /dev/null +++ b/src/test/resources/merge/diverging/androidNativeArm64.api @@ -0,0 +1,22 @@ +// Rendering settings: +// - Signature version: 2 +// - Show manifest properties: false +// - Show declarations: true + +// Library unique name: +// Platform: NATIVE +// Native targets: android_arm64 +// Compiler version: 1.9.22 +// ABI version: 1.8.0 +final class org.example/ShardedClass { // org.example/ShardedClass|null[0] + final val value // org.example/ShardedClass.value|{}value[0] + final fun (): kotlin/Int // org.example/ShardedClass.value.|(){}[0] + constructor (kotlin/Int) // org.example/ShardedClass.|(kotlin.Int){}[0] + final fun add(kotlin/Int): kotlin/Int // org.example/ShardedClass.add|add(kotlin.Int){}[0] +} +final class org.example/X { // org.example/X|null[0] + constructor (kotlin/Int) // org.example/X.|(kotlin.Int){}[0] +} +final fun org.example/ShardedClass(kotlin/Int, kotlin/Float, kotlin/Long): org.example/ShardedClass // org.example/ShardedClass|ShardedClass(kotlin.Int;kotlin.Float;kotlin.Long){}[0] +final fun org.example/add(org.example/X, org.example/X): org.example/X // org.example/add|add(org.example.X;org.example.X){}[0] +final fun (org.example/X).org.example/add(org.example/X): org.example/X // org.example/add|add@org.example.X(org.example.X){}[0] diff --git a/src/test/resources/merge/diverging/linuxArm64.api b/src/test/resources/merge/diverging/linuxArm64.api new file mode 100644 index 00000000..ee8b81c8 --- /dev/null +++ b/src/test/resources/merge/diverging/linuxArm64.api @@ -0,0 +1,19 @@ +// Rendering settings: +// - Signature version: 2 +// - Show manifest properties: false +// - Show declarations: true + +// Library unique name: +// Platform: NATIVE +// Native targets: linux_arm64 +// Compiler version: 1.9.22 +// ABI version: 1.8.0 +final class org.example/ShardedClass { // org.example/ShardedClass|null[0] + final val value // org.example/ShardedClass.value|{}value[0] + final fun (): kotlin/Int // org.example/ShardedClass.value.|(){}[0] + constructor (kotlin/Int) // org.example/ShardedClass.|(kotlin.Int){}[0] + final fun add(kotlin/Int): kotlin/Int // org.example/ShardedClass.add|add(kotlin.Int){}[0] +} +final fun org.example/ShardedClass(kotlin/Int, kotlin/Float, kotlin/Long): org.example/ShardedClass // org.example/ShardedClass|ShardedClass(kotlin.Int;kotlin.Float;kotlin.Long){}[0] +final fun org.example/add(kotlin/Long, kotlin/Long): kotlin/Long // org.example/add|add(kotlin.Long;kotlin.Long){}[0] +final fun (kotlin/Long).org.example/add(kotlin/Long): kotlin/Long // org.example/add|add@kotlin.Long(kotlin.Long){}[0] diff --git a/src/test/resources/merge/diverging/linuxArm64.extracted.api b/src/test/resources/merge/diverging/linuxArm64.extracted.api new file mode 100644 index 00000000..f0b0753b --- /dev/null +++ b/src/test/resources/merge/diverging/linuxArm64.extracted.api @@ -0,0 +1,17 @@ +// Klib ABI Dump +// Targets: [linuxArm64.linux] +// Rendering settings: +// - Signature version: 2 +// - Show manifest properties: false +// - Show declarations: true + +// Library unique name: +final class org.example/ShardedClass { // org.example/ShardedClass|null[0] + constructor (kotlin/Int) // org.example/ShardedClass.|(kotlin.Int){}[0] + final fun add(kotlin/Int): kotlin/Int // org.example/ShardedClass.add|add(kotlin.Int){}[0] + final val value // org.example/ShardedClass.value|{}value[0] + final fun (): kotlin/Int // org.example/ShardedClass.value.|(){}[0] +} +final fun (kotlin/Long).org.example/add(kotlin/Long): kotlin/Long // org.example/add|add@kotlin.Long(kotlin.Long){}[0] +final fun org.example/ShardedClass(kotlin/Int, kotlin/Float, kotlin/Long): org.example/ShardedClass // org.example/ShardedClass|ShardedClass(kotlin.Int;kotlin.Float;kotlin.Long){}[0] +final fun org.example/add(kotlin/Long, kotlin/Long): kotlin/Long // org.example/add|add(kotlin.Long;kotlin.Long){}[0] diff --git a/src/test/resources/merge/diverging/linuxX64.api b/src/test/resources/merge/diverging/linuxX64.api new file mode 100644 index 00000000..e827b6c6 --- /dev/null +++ b/src/test/resources/merge/diverging/linuxX64.api @@ -0,0 +1,19 @@ +// Rendering settings: +// - Signature version: 2 +// - Show manifest properties: false +// - Show declarations: true + +// Library unique name: +// Platform: NATIVE +// Native targets: linux_x64 +// Compiler version: 1.9.22 +// ABI version: 1.8.0 +final class org.example/ShardedClass { // org.example/ShardedClass|null[0] + final val value // org.example/ShardedClass.value|{}value[0] + final fun (): kotlin/Int // org.example/ShardedClass.value.|(){}[0] + constructor (kotlin/Int) // org.example/ShardedClass.|(kotlin.Int){}[0] + final fun add(kotlin/Int): kotlin/Int // org.example/ShardedClass.add|add(kotlin.Int){}[0] +} +final fun org.example/ShardedClass(kotlin/Int, kotlin/Float, kotlin/Long): org.example/ShardedClass // org.example/ShardedClass|ShardedClass(kotlin.Int;kotlin.Float;kotlin.Long){}[0] +final fun org.example/add(kotlin/Long, kotlin/Long): kotlin/Long // org.example/add|add(kotlin.Long;kotlin.Long){}[0] +final fun (kotlin/Long).org.example/add(kotlin/Long): kotlin/Long // org.example/add|add@kotlin.Long(kotlin.Long){}[0] diff --git a/src/test/resources/merge/diverging/merged.abi b/src/test/resources/merge/diverging/merged.abi new file mode 100644 index 00000000..de31ec4a --- /dev/null +++ b/src/test/resources/merge/diverging/merged.abi @@ -0,0 +1,32 @@ +// Klib ABI Dump +// Targets: [androidNativeArm64, linuxArm64, linuxX64, tvosX64] +// Alias: linux => [linuxArm64, linuxX64] +// Rendering settings: +// - Signature version: 2 +// - Show manifest properties: false +// - Show declarations: true + +// Library unique name: +final class org.example/ShardedClass { // org.example/ShardedClass|null[0] + constructor (kotlin/Int) // org.example/ShardedClass.|(kotlin.Int){}[0] + final fun add(kotlin/Int): kotlin/Int // org.example/ShardedClass.add|add(kotlin.Int){}[0] + final val value // org.example/ShardedClass.value|{}value[0] + final fun (): kotlin/Int // org.example/ShardedClass.value.|(){}[0] +} +final fun org.example/ShardedClass(kotlin/Int, kotlin/Float, kotlin/Long): org.example/ShardedClass // org.example/ShardedClass|ShardedClass(kotlin.Int;kotlin.Float;kotlin.Long){}[0] +// Targets: [linux] +final fun (kotlin/Long).org.example/add(kotlin/Long): kotlin/Long // org.example/add|add@kotlin.Long(kotlin.Long){}[0] +// Targets: [linux] +final fun org.example/add(kotlin/Long, kotlin/Long): kotlin/Long // org.example/add|add(kotlin.Long;kotlin.Long){}[0] +// Targets: [androidNativeArm64] +final class org.example/X { // org.example/X|null[0] + constructor (kotlin/Int) // org.example/X.|(kotlin.Int){}[0] +} +// Targets: [androidNativeArm64] +final fun (org.example/X).org.example/add(org.example/X): org.example/X // org.example/add|add@org.example.X(org.example.X){}[0] +// Targets: [androidNativeArm64] +final fun org.example/add(org.example/X, org.example/X): org.example/X // org.example/add|add(org.example.X;org.example.X){}[0] +// Targets: [tvosX64] +final fun (kotlin/Int).org.example/add(kotlin/Int): kotlin/Int // org.example/add|add@kotlin.Int(kotlin.Int){}[0] +// Targets: [tvosX64] +final fun org.example/add(kotlin/Int, kotlin/Int): kotlin/Int // org.example/add|add(kotlin.Int;kotlin.Int){}[0] diff --git a/src/test/resources/merge/diverging/merged_with_aliases.abi b/src/test/resources/merge/diverging/merged_with_aliases.abi new file mode 100644 index 00000000..de31ec4a --- /dev/null +++ b/src/test/resources/merge/diverging/merged_with_aliases.abi @@ -0,0 +1,32 @@ +// Klib ABI Dump +// Targets: [androidNativeArm64, linuxArm64, linuxX64, tvosX64] +// Alias: linux => [linuxArm64, linuxX64] +// Rendering settings: +// - Signature version: 2 +// - Show manifest properties: false +// - Show declarations: true + +// Library unique name: +final class org.example/ShardedClass { // org.example/ShardedClass|null[0] + constructor (kotlin/Int) // org.example/ShardedClass.|(kotlin.Int){}[0] + final fun add(kotlin/Int): kotlin/Int // org.example/ShardedClass.add|add(kotlin.Int){}[0] + final val value // org.example/ShardedClass.value|{}value[0] + final fun (): kotlin/Int // org.example/ShardedClass.value.|(){}[0] +} +final fun org.example/ShardedClass(kotlin/Int, kotlin/Float, kotlin/Long): org.example/ShardedClass // org.example/ShardedClass|ShardedClass(kotlin.Int;kotlin.Float;kotlin.Long){}[0] +// Targets: [linux] +final fun (kotlin/Long).org.example/add(kotlin/Long): kotlin/Long // org.example/add|add@kotlin.Long(kotlin.Long){}[0] +// Targets: [linux] +final fun org.example/add(kotlin/Long, kotlin/Long): kotlin/Long // org.example/add|add(kotlin.Long;kotlin.Long){}[0] +// Targets: [androidNativeArm64] +final class org.example/X { // org.example/X|null[0] + constructor (kotlin/Int) // org.example/X.|(kotlin.Int){}[0] +} +// Targets: [androidNativeArm64] +final fun (org.example/X).org.example/add(org.example/X): org.example/X // org.example/add|add@org.example.X(org.example.X){}[0] +// Targets: [androidNativeArm64] +final fun org.example/add(org.example/X, org.example/X): org.example/X // org.example/add|add(org.example.X;org.example.X){}[0] +// Targets: [tvosX64] +final fun (kotlin/Int).org.example/add(kotlin/Int): kotlin/Int // org.example/add|add@kotlin.Int(kotlin.Int){}[0] +// Targets: [tvosX64] +final fun org.example/add(kotlin/Int, kotlin/Int): kotlin/Int // org.example/add|add(kotlin.Int;kotlin.Int){}[0] diff --git a/src/test/resources/merge/diverging/merged_with_aliases_and_custom_names.abi b/src/test/resources/merge/diverging/merged_with_aliases_and_custom_names.abi new file mode 100644 index 00000000..1013b501 --- /dev/null +++ b/src/test/resources/merge/diverging/merged_with_aliases_and_custom_names.abi @@ -0,0 +1,32 @@ +// Klib ABI Dump +// Targets: [androidNativeArm64.android, linuxArm64.linux, linuxX64, tvosX64] +// Alias: linux => [linuxArm64.linux, linuxX64] +// Rendering settings: +// - Signature version: 2 +// - Show manifest properties: false +// - Show declarations: true + +// Library unique name: +final class org.example/ShardedClass { // org.example/ShardedClass|null[0] + constructor (kotlin/Int) // org.example/ShardedClass.|(kotlin.Int){}[0] + final fun add(kotlin/Int): kotlin/Int // org.example/ShardedClass.add|add(kotlin.Int){}[0] + final val value // org.example/ShardedClass.value|{}value[0] + final fun (): kotlin/Int // org.example/ShardedClass.value.|(){}[0] +} +final fun org.example/ShardedClass(kotlin/Int, kotlin/Float, kotlin/Long): org.example/ShardedClass // org.example/ShardedClass|ShardedClass(kotlin.Int;kotlin.Float;kotlin.Long){}[0] +// Targets: [linux] +final fun (kotlin/Long).org.example/add(kotlin/Long): kotlin/Long // org.example/add|add@kotlin.Long(kotlin.Long){}[0] +// Targets: [linux] +final fun org.example/add(kotlin/Long, kotlin/Long): kotlin/Long // org.example/add|add(kotlin.Long;kotlin.Long){}[0] +// Targets: [androidNativeArm64.android] +final class org.example/X { // org.example/X|null[0] + constructor (kotlin/Int) // org.example/X.|(kotlin.Int){}[0] +} +// Targets: [androidNativeArm64.android] +final fun (org.example/X).org.example/add(org.example/X): org.example/X // org.example/add|add@org.example.X(org.example.X){}[0] +// Targets: [androidNativeArm64.android] +final fun org.example/add(org.example/X, org.example/X): org.example/X // org.example/add|add(org.example.X;org.example.X){}[0] +// Targets: [tvosX64] +final fun (kotlin/Int).org.example/add(kotlin/Int): kotlin/Int // org.example/add|add@kotlin.Int(kotlin.Int){}[0] +// Targets: [tvosX64] +final fun org.example/add(kotlin/Int, kotlin/Int): kotlin/Int // org.example/add|add(kotlin.Int;kotlin.Int){}[0] diff --git a/src/test/resources/merge/diverging/tvosX64.api b/src/test/resources/merge/diverging/tvosX64.api new file mode 100644 index 00000000..72bf3f99 --- /dev/null +++ b/src/test/resources/merge/diverging/tvosX64.api @@ -0,0 +1,19 @@ +// Rendering settings: +// - Signature version: 2 +// - Show manifest properties: false +// - Show declarations: true + +// Library unique name: +// Platform: NATIVE +// Native targets: tvos_x64 +// Compiler version: 1.9.22 +// ABI version: 1.8.0 +final class org.example/ShardedClass { // org.example/ShardedClass|null[0] + final val value // org.example/ShardedClass.value|{}value[0] + final fun (): kotlin/Int // org.example/ShardedClass.value.|(){}[0] + constructor (kotlin/Int) // org.example/ShardedClass.|(kotlin.Int){}[0] + final fun add(kotlin/Int): kotlin/Int // org.example/ShardedClass.add|add(kotlin.Int){}[0] +} +final fun org.example/ShardedClass(kotlin/Int, kotlin/Float, kotlin/Long): org.example/ShardedClass // org.example/ShardedClass|ShardedClass(kotlin.Int;kotlin.Float;kotlin.Long){}[0] +final fun org.example/add(kotlin/Int, kotlin/Int): kotlin/Int // org.example/add|add(kotlin.Int;kotlin.Int){}[0] +final fun (kotlin/Int).org.example/add(kotlin/Int): kotlin/Int // org.example/add|add@kotlin.Int(kotlin.Int){}[0] diff --git a/src/test/resources/merge/guess/common.api b/src/test/resources/merge/guess/common.api new file mode 100644 index 00000000..b2823f75 --- /dev/null +++ b/src/test/resources/merge/guess/common.api @@ -0,0 +1,14 @@ +// Klib ABI Dump +// Targets: [androidNativeArm64, linuxX64, tvOsX64] +// Rendering settings: +// - Signature version: 2 +// - Show manifest properties: false +// - Show declarations: true + +// Library unique name: +final class org.example/ShardedClass { // org.example/ShardedClass|null[0] + constructor (kotlin/Int) // org.example/ShardedClass.|(kotlin.Int){}[0] + final fun add(kotlin/Int): kotlin/Int // org.example/ShardedClass.add|add(kotlin.Int){}[0] + final val value // org.example/ShardedClass.value|{}value[0] + final fun (): kotlin/Int // org.example/ShardedClass.value.|(){}[0] +} diff --git a/src/test/resources/merge/guess/guessed.api b/src/test/resources/merge/guess/guessed.api new file mode 100644 index 00000000..ebed8a76 --- /dev/null +++ b/src/test/resources/merge/guess/guessed.api @@ -0,0 +1,17 @@ +// Klib ABI Dump +// Targets: [linuxArm64] +// Rendering settings: +// - Signature version: 2 +// - Show manifest properties: false +// - Show declarations: true + +// Library unique name: +final class org.example/ShardedClass { // org.example/ShardedClass|null[0] + constructor (kotlin/Int) // org.example/ShardedClass.|(kotlin.Int){}[0] + final fun add(kotlin/Int): kotlin/Int // org.example/ShardedClass.add|add(kotlin.Int){}[0] + final fun addNarrow(kotlin/Int): kotlin/Int // org.example/ShardedClass.addNarrow|addNarrow(kotlin.Int){}[0] + final val value // org.example/ShardedClass.value|{}value[0] + final fun (): kotlin/Int // org.example/ShardedClass.value.|(){}[0] +} +final fun (org.example/X).org.example/add(org.example/X): org.example/X // org.example/add|add@org.example.X(org.example.X){}[0] +final fun org.example/add(org.example/X, org.example/X): org.example/X // org.example/add|add(org.example.X;org.example.X){}[0] diff --git a/src/test/resources/merge/guess/linuxArm64Specific.api b/src/test/resources/merge/guess/linuxArm64Specific.api new file mode 100644 index 00000000..f52246ee --- /dev/null +++ b/src/test/resources/merge/guess/linuxArm64Specific.api @@ -0,0 +1,13 @@ +// Klib ABI Dump +// Targets: [linuxArm64] +// Rendering settings: +// - Signature version: 2 +// - Show manifest properties: false +// - Show declarations: true + +// Library unique name: +final class org.example/ShardedClass { // org.example/ShardedClass|null[0] + final fun addNarrow(kotlin/Int): kotlin/Int // org.example/ShardedClass.addNarrow|addNarrow(kotlin.Int){}[0] +} +final fun (org.example/X).org.example/add(org.example/X): org.example/X // org.example/add|add@org.example.X(org.example.X){}[0] +final fun org.example/add(org.example/X, org.example/X): org.example/X // org.example/add|add(org.example.X;org.example.X){}[0] diff --git a/src/test/resources/merge/guess/merged.api b/src/test/resources/merge/guess/merged.api new file mode 100644 index 00000000..adf561ec --- /dev/null +++ b/src/test/resources/merge/guess/merged.api @@ -0,0 +1,24 @@ +// Klib ABI Dump +// Targets: [androidNativeArm64, linuxArm64, linuxX64, tvOsX64] +// Rendering settings: +// - Signature version: 2 +// - Show manifest properties: false +// - Show declarations: true + +// Library unique name: +final class org.example/ShardedClass { // org.example/ShardedClass|null[0] + constructor (kotlin/Int) // org.example/ShardedClass.|(kotlin.Int){}[0] + final fun add(kotlin/Int): kotlin/Int // org.example/ShardedClass.add|add(kotlin.Int){}[0] + final val value // org.example/ShardedClass.value|{}value[0] + final fun (): kotlin/Int // org.example/ShardedClass.value.|(){}[0] + // Targets: [linuxArm64, linuxX64] + final fun addNarrow(kotlin/Int): kotlin/Int // org.example/ShardedClass.addNarrow|addNarrow(kotlin.Int){}[0] +} +// Targets: [linuxArm64] +final fun (org.example/X).org.example/add(org.example/X): org.example/X // org.example/add|add@org.example.X(org.example.X){}[0] +// Targets: [linuxArm64, tvOsX64] +final fun org.example/add(org.example/X, org.example/X): org.example/X // org.example/add|add(org.example.X;org.example.X){}[0] +// Targets: [tvOsX64] +final fun (kotlin/Int).org.example/add(kotlin/Int): kotlin/Int // org.example/add|add@kotlin.Int(kotlin.Int){}[0] +// Targets: [tvOsX64] +final fun org.example/add(kotlin/Int, kotlin/Int): kotlin/Int // org.example/add|add(kotlin.Int;kotlin.Int){}[0] diff --git a/src/test/resources/merge/header-mismatch/v1.abi b/src/test/resources/merge/header-mismatch/v1.abi new file mode 100644 index 00000000..34fdd006 --- /dev/null +++ b/src/test/resources/merge/header-mismatch/v1.abi @@ -0,0 +1,17 @@ +// Rendering settings: +// - Signature version: 1 +// - Show manifest properties: true +// - Show declarations: true + +// Library unique name: +// Platform: NATIVE +// Native targets: linux_arm64 +// Compiler version: 1.9.22 +// ABI version: 1.8.0 +final class org.example/ShardedClass { // org.example/ShardedClass|null[0] + final val value // org.example/ShardedClass.value|1987073854177347439[0] + final fun (): kotlin/Int // org.example/ShardedClass.value.|3260093555963109437[0] + constructor (kotlin/Int) // org.example/ShardedClass.|-5182794243525578284[0] + final fun add(kotlin/Int): kotlin/Int // org.example/ShardedClass.add|4888650976871417104[0] +} +final fun org.example/ShardedClass(kotlin/Int, kotlin/Float, kotlin/Long): org.example/ShardedClass // org.example/ShardedClass|-4796080257537853433[0] diff --git a/src/test/resources/merge/header-mismatch/v2.abi b/src/test/resources/merge/header-mismatch/v2.abi new file mode 100644 index 00000000..ca9ca7cf --- /dev/null +++ b/src/test/resources/merge/header-mismatch/v2.abi @@ -0,0 +1,17 @@ +// Rendering settings: +// - Signature version: 2 +// - Show manifest properties: false +// - Show declarations: true + +// Library unique name: +// Platform: NATIVE +// Native targets: linux_x64 +// Compiler version: 1.9.22 +// ABI version: 1.8.0 +final class org.example/ShardedClass { // org.example/ShardedClass|null[0] + final val value // org.example/ShardedClass.value|{}value[0] + final fun (): kotlin/Int // org.example/ShardedClass.value.|(){}[0] + constructor (kotlin/Int) // org.example/ShardedClass.|(kotlin.Int){}[0] + final fun add(kotlin/Int): kotlin/Int // org.example/ShardedClass.add|add(kotlin.Int){}[0] +} +final fun org.example/ShardedClass(kotlin/Int, kotlin/Float, kotlin/Long): org.example/ShardedClass // org.example/ShardedClass|ShardedClass(kotlin.Int;kotlin.Float;kotlin.Long){}[0] diff --git a/src/test/resources/merge/idempotent/bcv-klib-test.abi b/src/test/resources/merge/idempotent/bcv-klib-test.abi new file mode 100644 index 00000000..7a81715c --- /dev/null +++ b/src/test/resources/merge/idempotent/bcv-klib-test.abi @@ -0,0 +1,34 @@ +// Klib ABI Dump +// Targets: [androidNativeArm32, androidNativeArm64, iosSimulatorArm64, linuxArm64, linuxX64, macosArm64, tvosArm64, tvosSimulatorArm64, tvosX64] +// Alias: androidNative => [androidNativeArm32, androidNativeArm64] +// Alias: apple => [iosSimulatorArm64, macosArm64, tvosArm64, tvosSimulatorArm64, tvosX64] +// Alias: linux => [linuxArm64, linuxX64] +// Rendering settings: +// - Signature version: 2 +// - Show manifest properties: false +// - Show declarations: true + +// Library unique name: +final class org.example/ShardedClass { // org.example/ShardedClass|null[0] + constructor (kotlin/Int) // org.example/ShardedClass.|(kotlin.Int){}[0] + final fun add(kotlin/Int): kotlin/Int // org.example/ShardedClass.add|add(kotlin.Int){}[0] + final val value // org.example/ShardedClass.value|{}value[0] + final fun (): kotlin/Int // org.example/ShardedClass.value.|(){}[0] +} +final fun org.example/ShardedClass(kotlin/Int, kotlin/Float, kotlin/Long): org.example/ShardedClass // org.example/ShardedClass|ShardedClass(kotlin.Int;kotlin.Float;kotlin.Long){}[0] +// Targets: [apple] +final fun (kotlin/Int).org.example/add(kotlin/Int): kotlin/Int // org.example/add|add@kotlin.Int(kotlin.Int){}[0] +// Targets: [apple] +final fun org.example/add(kotlin/Int, kotlin/Int): kotlin/Int // org.example/add|add(kotlin.Int;kotlin.Int){}[0] +// Targets: [androidNative] +final class org.example/X { // org.example/X|null[0] + constructor (kotlin/Int) // org.example/X.|(kotlin.Int){}[0] +} +// Targets: [androidNative] +final fun (org.example/X).org.example/add(org.example/X): org.example/X // org.example/add|add@org.example.X(org.example.X){}[0] +// Targets: [androidNative] +final fun org.example/add(org.example/X, org.example/X): org.example/X // org.example/add|add(org.example.X;org.example.X){}[0] +// Targets: [linux] +final fun (kotlin/Long).org.example/add(kotlin/Long): kotlin/Long // org.example/add|add@kotlin.Long(kotlin.Long){}[0] +// Targets: [linux] +final fun org.example/add(kotlin/Long, kotlin/Long): kotlin/Long // org.example/add|add(kotlin.Long;kotlin.Long){}[0] diff --git a/src/test/resources/merge/identical/dump_linux_x64.abi b/src/test/resources/merge/identical/dump_linux_x64.abi new file mode 100644 index 00000000..ca9ca7cf --- /dev/null +++ b/src/test/resources/merge/identical/dump_linux_x64.abi @@ -0,0 +1,17 @@ +// Rendering settings: +// - Signature version: 2 +// - Show manifest properties: false +// - Show declarations: true + +// Library unique name: +// Platform: NATIVE +// Native targets: linux_x64 +// Compiler version: 1.9.22 +// ABI version: 1.8.0 +final class org.example/ShardedClass { // org.example/ShardedClass|null[0] + final val value // org.example/ShardedClass.value|{}value[0] + final fun (): kotlin/Int // org.example/ShardedClass.value.|(){}[0] + constructor (kotlin/Int) // org.example/ShardedClass.|(kotlin.Int){}[0] + final fun add(kotlin/Int): kotlin/Int // org.example/ShardedClass.add|add(kotlin.Int){}[0] +} +final fun org.example/ShardedClass(kotlin/Int, kotlin/Float, kotlin/Long): org.example/ShardedClass // org.example/ShardedClass|ShardedClass(kotlin.Int;kotlin.Float;kotlin.Long){}[0] diff --git a/src/test/resources/merge/identical/dump_macos_arm64.abi b/src/test/resources/merge/identical/dump_macos_arm64.abi new file mode 100644 index 00000000..a5105085 --- /dev/null +++ b/src/test/resources/merge/identical/dump_macos_arm64.abi @@ -0,0 +1,17 @@ +// Rendering settings: +// - Signature version: 2 +// - Show manifest properties: false +// - Show declarations: true + +// Library unique name: +// Platform: NATIVE +// Native targets: macos_arm64 +// Compiler version: 1.9.22 +// ABI version: 1.8.0 +final class org.example/ShardedClass { // org.example/ShardedClass|null[0] + final val value // org.example/ShardedClass.value|{}value[0] + final fun (): kotlin/Int // org.example/ShardedClass.value.|(){}[0] + constructor (kotlin/Int) // org.example/ShardedClass.|(kotlin.Int){}[0] + final fun add(kotlin/Int): kotlin/Int // org.example/ShardedClass.add|add(kotlin.Int){}[0] +} +final fun org.example/ShardedClass(kotlin/Int, kotlin/Float, kotlin/Long): org.example/ShardedClass // org.example/ShardedClass|ShardedClass(kotlin.Int;kotlin.Float;kotlin.Long){}[0] diff --git a/src/test/resources/merge/identical/merged.abi b/src/test/resources/merge/identical/merged.abi new file mode 100644 index 00000000..27a1c6d6 --- /dev/null +++ b/src/test/resources/merge/identical/merged.abi @@ -0,0 +1,15 @@ +// Klib ABI Dump +// Targets: [linuxX64, macosArm64] +// Rendering settings: +// - Signature version: 2 +// - Show manifest properties: false +// - Show declarations: true + +// Library unique name: +final class org.example/ShardedClass { // org.example/ShardedClass|null[0] + constructor (kotlin/Int) // org.example/ShardedClass.|(kotlin.Int){}[0] + final fun add(kotlin/Int): kotlin/Int // org.example/ShardedClass.add|add(kotlin.Int){}[0] + final val value // org.example/ShardedClass.value|{}value[0] + final fun (): kotlin/Int // org.example/ShardedClass.value.|(){}[0] +} +final fun org.example/ShardedClass(kotlin/Int, kotlin/Float, kotlin/Long): org.example/ShardedClass // org.example/ShardedClass|ShardedClass(kotlin.Int;kotlin.Float;kotlin.Long){}[0] diff --git a/src/test/resources/merge/illegalFiles/emptyFile.txt b/src/test/resources/merge/illegalFiles/emptyFile.txt new file mode 100644 index 00000000..e69de29b diff --git a/src/test/resources/merge/illegalFiles/nonDumpFile.txt b/src/test/resources/merge/illegalFiles/nonDumpFile.txt new file mode 100644 index 00000000..1a4cac6c --- /dev/null +++ b/src/test/resources/merge/illegalFiles/nonDumpFile.txt @@ -0,0 +1 @@ +I'm not a dump diff --git a/src/test/resources/merge/parseNarrowChildrenDecls/merged.abi b/src/test/resources/merge/parseNarrowChildrenDecls/merged.abi new file mode 100644 index 00000000..e9a1408a --- /dev/null +++ b/src/test/resources/merge/parseNarrowChildrenDecls/merged.abi @@ -0,0 +1,16 @@ +// Klib ABI Dump +// Targets: [androidNativeArm64, linuxArm64, linuxX64, tvOsX64] +// Rendering settings: +// - Signature version: 2 +// - Show manifest properties: false +// - Show declarations: true + +// Library unique name: +final class org.example/ShardedClass { // org.example/ShardedClass|null[0] + constructor (kotlin/Int) // org.example/ShardedClass.|(kotlin.Int){}[0] + final fun add(kotlin/Int): kotlin/Int // org.example/ShardedClass.add|add(kotlin.Int){}[0] + final val value // org.example/ShardedClass.value|{}value[0] + final fun (): kotlin/Int // org.example/ShardedClass.value.|(){}[0] + // Targets: [linuxArm64, linuxX64] + final fun addNarrow(kotlin/Int): kotlin/Int // org.example/ShardedClass.addNarrow|addNarrow(kotlin.Int){}[0] +} diff --git a/src/test/resources/merge/parseNarrowChildrenDecls/withoutLinuxAll.abi b/src/test/resources/merge/parseNarrowChildrenDecls/withoutLinuxAll.abi new file mode 100644 index 00000000..07552376 --- /dev/null +++ b/src/test/resources/merge/parseNarrowChildrenDecls/withoutLinuxAll.abi @@ -0,0 +1,14 @@ +// Klib ABI Dump +// Targets: [androidNativeArm64, tvOsX64] +// Rendering settings: +// - Signature version: 2 +// - Show manifest properties: false +// - Show declarations: true + +// Library unique name: +final class org.example/ShardedClass { // org.example/ShardedClass|null[0] + constructor (kotlin/Int) // org.example/ShardedClass.|(kotlin.Int){}[0] + final fun add(kotlin/Int): kotlin/Int // org.example/ShardedClass.add|add(kotlin.Int){}[0] + final val value // org.example/ShardedClass.value|{}value[0] + final fun (): kotlin/Int // org.example/ShardedClass.value.|(){}[0] +} diff --git a/src/test/resources/merge/parseNarrowChildrenDecls/withoutLinuxArm64.abi b/src/test/resources/merge/parseNarrowChildrenDecls/withoutLinuxArm64.abi new file mode 100644 index 00000000..f8ae14ba --- /dev/null +++ b/src/test/resources/merge/parseNarrowChildrenDecls/withoutLinuxArm64.abi @@ -0,0 +1,16 @@ +// Klib ABI Dump +// Targets: [androidNativeArm64, linuxX64, tvOsX64] +// Rendering settings: +// - Signature version: 2 +// - Show manifest properties: false +// - Show declarations: true + +// Library unique name: +final class org.example/ShardedClass { // org.example/ShardedClass|null[0] + constructor (kotlin/Int) // org.example/ShardedClass.|(kotlin.Int){}[0] + final fun add(kotlin/Int): kotlin/Int // org.example/ShardedClass.add|add(kotlin.Int){}[0] + final val value // org.example/ShardedClass.value|{}value[0] + final fun (): kotlin/Int // org.example/ShardedClass.value.|(){}[0] + // Targets: [linuxX64] + final fun addNarrow(kotlin/Int): kotlin/Int // org.example/ShardedClass.addNarrow|addNarrow(kotlin.Int){}[0] +} diff --git a/src/test/resources/merge/stdlib_native_common.abi b/src/test/resources/merge/stdlib_native_common.abi new file mode 100644 index 00000000..a475212a --- /dev/null +++ b/src/test/resources/merge/stdlib_native_common.abi @@ -0,0 +1,17 @@ +// Rendering settings: +// - Signature version: 2 +// - Show manifest properties: true +// - Show declarations: true + +// Library unique name: +// Platform: NATIVE +// Native targets: android_arm32, android_arm64, android_x64, android_x86, ios_arm64, ios_simulator_arm64, ios_x64, linux_arm32_hfp, linux_arm64, linux_x64, macos_arm64, macos_x64, mingw_x64, tvos_arm64, tvos_simulator_arm64, tvos_x64, watchos_arm32, watchos_arm64, watchos_device_arm64, watchos_simulator_arm64, watchos_x64 +// Compiler version: 2.0.255-SNAPSHOT +// ABI version: 1.8.0 +abstract interface kotlin/Annotation // kotlin/Annotation|null[0] +open class kotlin/Any { // kotlin/Any|null[0] + constructor () // kotlin/Any.|(){}[0] + open fun equals(kotlin/Any?): kotlin/Boolean // kotlin/Any.equals|equals(kotlin.Any?){}[0] + open fun hashCode(): kotlin/Int // kotlin/Any.hashCode|hashCode(){}[0] + open fun toString(): kotlin/String // kotlin/Any.toString|toString(){}[0] +} diff --git a/src/test/resources/merge/webTargets/js.abi b/src/test/resources/merge/webTargets/js.abi new file mode 100644 index 00000000..0afbc790 --- /dev/null +++ b/src/test/resources/merge/webTargets/js.abi @@ -0,0 +1,16 @@ +// Rendering settings: +// - Signature version: 2 +// - Show manifest properties: false +// - Show declarations: true + +// Library unique name: +// Platform: JS +// Compiler version: 1.9.22 +// ABI version: 1.8.0 +final class org.example/ShardedClass { // org.example/ShardedClass|null[0] + final val value // org.example/ShardedClass.value|{}value[0] + final fun (): kotlin/Int // org.example/ShardedClass.value.|(){}[0] + constructor (kotlin/Int) // org.example/ShardedClass.|(kotlin.Int){}[0] + final fun add(kotlin/Int): kotlin/Int // org.example/ShardedClass.add|add(kotlin.Int){}[0] +} +final fun org.example/ShardedClass(kotlin/Int, kotlin/Float, kotlin/Long): org.example/ShardedClass // org.example/ShardedClass|ShardedClass(kotlin.Int;kotlin.Float;kotlin.Long){}[0] diff --git a/src/test/resources/merge/webTargets/js.ext.abi b/src/test/resources/merge/webTargets/js.ext.abi new file mode 100644 index 00000000..fdb21462 --- /dev/null +++ b/src/test/resources/merge/webTargets/js.ext.abi @@ -0,0 +1,15 @@ +// Klib ABI Dump +// Targets: [js] +// Rendering settings: +// - Signature version: 2 +// - Show manifest properties: false +// - Show declarations: true + +// Library unique name: +final class org.example/ShardedClass { // org.example/ShardedClass|null[0] + constructor (kotlin/Int) // org.example/ShardedClass.|(kotlin.Int){}[0] + final fun add(kotlin/Int): kotlin/Int // org.example/ShardedClass.add|add(kotlin.Int){}[0] + final val value // org.example/ShardedClass.value|{}value[0] + final fun (): kotlin/Int // org.example/ShardedClass.value.|(){}[0] +} +final fun org.example/ShardedClass(kotlin/Int, kotlin/Float, kotlin/Long): org.example/ShardedClass // org.example/ShardedClass|ShardedClass(kotlin.Int;kotlin.Float;kotlin.Long){}[0] diff --git a/src/test/resources/merge/webTargets/merged.abi b/src/test/resources/merge/webTargets/merged.abi new file mode 100644 index 00000000..29f24c8f --- /dev/null +++ b/src/test/resources/merge/webTargets/merged.abi @@ -0,0 +1,15 @@ +// Klib ABI Dump +// Targets: [js, wasmJs, wasmWasi] +// Rendering settings: +// - Signature version: 2 +// - Show manifest properties: false +// - Show declarations: true + +// Library unique name: +final class org.example/ShardedClass { // org.example/ShardedClass|null[0] + constructor (kotlin/Int) // org.example/ShardedClass.|(kotlin.Int){}[0] + final fun add(kotlin/Int): kotlin/Int // org.example/ShardedClass.add|add(kotlin.Int){}[0] + final val value // org.example/ShardedClass.value|{}value[0] + final fun (): kotlin/Int // org.example/ShardedClass.value.|(){}[0] +} +final fun org.example/ShardedClass(kotlin/Int, kotlin/Float, kotlin/Long): org.example/ShardedClass // org.example/ShardedClass|ShardedClass(kotlin.Int;kotlin.Float;kotlin.Long){}[0] diff --git a/src/test/resources/merge/webTargets/wasmJs.abi b/src/test/resources/merge/webTargets/wasmJs.abi new file mode 100644 index 00000000..56ba4e9b --- /dev/null +++ b/src/test/resources/merge/webTargets/wasmJs.abi @@ -0,0 +1,16 @@ +// Rendering settings: +// - Signature version: 2 +// - Show manifest properties: false +// - Show declarations: true + +// Library unique name: +// Platform: WASM +// Compiler version: 1.9.22 +// ABI version: 1.8.0 +final class org.example/ShardedClass { // org.example/ShardedClass|null[0] + final val value // org.example/ShardedClass.value|{}value[0] + final fun (): kotlin/Int // org.example/ShardedClass.value.|(){}[0] + constructor (kotlin/Int) // org.example/ShardedClass.|(kotlin.Int){}[0] + final fun add(kotlin/Int): kotlin/Int // org.example/ShardedClass.add|add(kotlin.Int){}[0] +} +final fun org.example/ShardedClass(kotlin/Int, kotlin/Float, kotlin/Long): org.example/ShardedClass // org.example/ShardedClass|ShardedClass(kotlin.Int;kotlin.Float;kotlin.Long){}[0] diff --git a/src/test/resources/merge/webTargets/wasmJs.ext.abi b/src/test/resources/merge/webTargets/wasmJs.ext.abi new file mode 100644 index 00000000..44cb7a7f --- /dev/null +++ b/src/test/resources/merge/webTargets/wasmJs.ext.abi @@ -0,0 +1,15 @@ +// Klib ABI Dump +// Targets: [wasmJs] +// Rendering settings: +// - Signature version: 2 +// - Show manifest properties: false +// - Show declarations: true + +// Library unique name: +final class org.example/ShardedClass { // org.example/ShardedClass|null[0] + constructor (kotlin/Int) // org.example/ShardedClass.|(kotlin.Int){}[0] + final fun add(kotlin/Int): kotlin/Int // org.example/ShardedClass.add|add(kotlin.Int){}[0] + final val value // org.example/ShardedClass.value|{}value[0] + final fun (): kotlin/Int // org.example/ShardedClass.value.|(){}[0] +} +final fun org.example/ShardedClass(kotlin/Int, kotlin/Float, kotlin/Long): org.example/ShardedClass // org.example/ShardedClass|ShardedClass(kotlin.Int;kotlin.Float;kotlin.Long){}[0] diff --git a/src/test/resources/merge/webTargets/wasmWasi.abi b/src/test/resources/merge/webTargets/wasmWasi.abi new file mode 100644 index 00000000..56ba4e9b --- /dev/null +++ b/src/test/resources/merge/webTargets/wasmWasi.abi @@ -0,0 +1,16 @@ +// Rendering settings: +// - Signature version: 2 +// - Show manifest properties: false +// - Show declarations: true + +// Library unique name: +// Platform: WASM +// Compiler version: 1.9.22 +// ABI version: 1.8.0 +final class org.example/ShardedClass { // org.example/ShardedClass|null[0] + final val value // org.example/ShardedClass.value|{}value[0] + final fun (): kotlin/Int // org.example/ShardedClass.value.|(){}[0] + constructor (kotlin/Int) // org.example/ShardedClass.|(kotlin.Int){}[0] + final fun add(kotlin/Int): kotlin/Int // org.example/ShardedClass.add|add(kotlin.Int){}[0] +} +final fun org.example/ShardedClass(kotlin/Int, kotlin/Float, kotlin/Long): org.example/ShardedClass // org.example/ShardedClass|ShardedClass(kotlin.Int;kotlin.Float;kotlin.Long){}[0] diff --git a/src/test/resources/merge/webTargets/wasmWasi.ext.abi b/src/test/resources/merge/webTargets/wasmWasi.ext.abi new file mode 100644 index 00000000..6e7083ed --- /dev/null +++ b/src/test/resources/merge/webTargets/wasmWasi.ext.abi @@ -0,0 +1,15 @@ +// Klib ABI Dump +// Targets: [wasmWasi] +// Rendering settings: +// - Signature version: 2 +// - Show manifest properties: false +// - Show declarations: true + +// Library unique name: +final class org.example/ShardedClass { // org.example/ShardedClass|null[0] + constructor (kotlin/Int) // org.example/ShardedClass.|(kotlin.Int){}[0] + final fun add(kotlin/Int): kotlin/Int // org.example/ShardedClass.add|add(kotlin.Int){}[0] + final val value // org.example/ShardedClass.value|{}value[0] + final fun (): kotlin/Int // org.example/ShardedClass.value.|(){}[0] +} +final fun org.example/ShardedClass(kotlin/Int, kotlin/Float, kotlin/Long): org.example/ShardedClass // org.example/ShardedClass|ShardedClass(kotlin.Int;kotlin.Float;kotlin.Long){}[0]