From 9c1e7f2ee6137d8ff9e2a32d95a8fd767abbcf78 Mon Sep 17 00:00:00 2001 From: Filipp Zhinkin Date: Mon, 18 Mar 2024 13:07:33 +0100 Subject: [PATCH] [ABI Validation] Klib support Added experimental Klib ABI dump validation support. Validation could be performed using the Gradle plugin which provides almost the same validation and dump workflow as for JVM ABI validation. Also, a public API was added to allow users who don't want or can't use the plugin to implement their own workflows. --------- Co-authored-by: Leonid Startsev Co-authored-by: ilya-g Pull request https://github.com/Kotlin/binary-compatibility-validator/pull/183 --- libraries/tools/abi-validation/README.md | 54 ++ .../api/binary-compatibility-validator.api | 132 ++- .../tools/abi-validation/build.gradle.kts | 7 +- .../abi-validation/docs/design/KLibSupport.md | 253 ++++++ .../abi-validation/gradle/libs.versions.toml | 1 + .../validation/api/BaseKotlinGradleTest.kt | 4 + .../kotlin/kotlinx/validation/api/TestDsl.kt | 43 + .../validation/test/IgnoredClassesTests.kt | 1 + .../validation/test/KlibVerificationTests.kt | 636 +++++++++++++++ ...> MultiPlatformSingleJvmKlibTargetTest.kt} | 2 +- .../validation/test/NonPublicMarkersTest.kt | 33 + .../validation/test/PublicMarkersTest.kt | 35 + .../AnotherBuildConfig.klib.clash.dump | 17 + .../AnotherBuildConfig.klib.custom.dump | 14 + .../classes/AnotherBuildConfig.klib.dump | 14 + ...AnotherBuildConfig.klib.renamedTarget.dump | 14 + .../classes/AnotherBuildConfig.klib.web.dump | 14 + ...AnotherBuildConfigLinux.klib.grouping.dump | 17 + .../classes/AnotherBuildConfigLinuxArm64.kt | 8 + ...notherBuildConfigLinuxArm64Extra.klib.dump | 16 + .../AnotherBuildConfigModified.klib.dump | 15 + .../classes/AnotherBuildConfigModified.kt | 14 + .../classes/ClassWithPublicMarkers.klib.dump | 45 + .../examples/classes/Empty.klib.dump | 6 + .../classes/HiddenDeclarations.klib.dump | 11 + .../examples/classes/HiddenDeclarations.kt | 43 + .../examples/classes/NonPublicMarkers.kt | 34 + .../examples/classes/Properties.klib.dump | 17 + .../resources/examples/classes/SubPackage.kt | 10 + .../examples/classes/Subclasses.dump | 7 + .../examples/classes/Subclasses.klib.dump | 14 + .../resources/examples/classes/Subclasses.kt | 16 + .../TopLevelDeclarations.klib.all.dump | 67 ++ .../classes/TopLevelDeclarations.klib.dump | 67 ++ .../TopLevelDeclarations.klib.unsup.dump | 67 ++ .../classes/TopLevelDeclarations.klib.v1.dump | 67 ++ ...lDeclarations.klib.with.guessed.linux.dump | 67 ++ .../TopLevelDeclarations.klib.with.linux.dump | 67 ++ .../examples/classes/TopLevelDeclarations.kt | 38 + .../enableJvmInWithNativePlugin.gradle.kts | 9 + .../gradle/base/withNativePlugin.gradle.kts | 38 + .../withNativePluginAndNoTargets.gradle.kts | 30 + ...withNativePluginAndSingleTarget.gradle.kts | 32 + .../appleTargets/targets.gradle.kts | 20 + .../grouping/clashingTargetNames.gradle.kts | 14 + .../grouping/customTargetNames.gradle.kts | 17 + .../ignoreSubclasses/ignore.gradle.kts | 9 + .../nonNativeKlibTargets/targets.gradle.kts | 10 + .../nonPublicMarkers/klib.gradle.kts | 13 + .../signatures/invalid.gradle.kts | 10 + .../configuration/signatures/v1.gradle.kts | 8 + .../unsupported/enforce.gradle.kts | 8 + .../src/main/kotlin/ApiValidationExtension.kt | 49 ++ .../BinaryCompatibilityValidatorPlugin.kt | 420 +++++++++- .../src/main/kotlin/BuildTaskBase.kt | 58 ++ .../src/main/kotlin/CopyFile.kt | 31 + .../src/main/kotlin/ExperimentalBCVApi.kt | 14 + .../src/main/kotlin/KotlinApiBuildTask.kt | 67 +- .../src/main/kotlin/KotlinApiCompareTask.kt | 78 +- .../src/main/kotlin/KotlinKlibAbiBuildTask.kt | 70 ++ ...otlinKlibExtractSupportedTargetsAbiTask.kt | 68 ++ ...linKlibInferAbiForUnsupportedTargetTask.kt | 133 +++ .../src/main/kotlin/KotlinKlibMergeAbiTask.kt | 61 ++ .../kotlin/api/klib/KlibAbiDumpFileMerger.kt | 768 ++++++++++++++++++ .../src/main/kotlin/api/klib/KlibDump.kt | 283 +++++++ .../main/kotlin/api/klib/KlibDumpFilters.kt | 198 +++++ .../kotlin/api/klib/KlibSignatureVersion.kt | 35 + .../src/main/kotlin/api/klib/KlibTarget.kt | 80 ++ .../main/kotlin/api/klib/TargetHierarchy.kt | 138 ++++ .../test/kotlin/samples/KlibDumpSamples.kt | 290 +++++++ .../kotlin/tests/ClassNameConvertionTest.kt | 44 + .../test/kotlin/tests/KlibAbiMergingTest.kt | 339 ++++++++ .../src/test/kotlin/tests/KlibDumpTest.kt | 659 +++++++++++++++ .../kotlin/tests/KlibSignatureVersionTest.kt | 39 + .../kotlin/tests/KlibTargetHierarchyTest.kt | 69 ++ .../test/kotlin/tests/KlibTargetNameTest.kt | 97 +++ .../merge/diverging/androidNativeArm64.api | 22 + .../resources/merge/diverging/linuxArm64.api | 19 + .../merge/diverging/linuxArm64.extracted.api | 17 + .../resources/merge/diverging/linuxX64.api | 19 + .../test/resources/merge/diverging/merged.abi | 32 + .../merge/diverging/merged_with_aliases.abi | 32 + .../merged_with_aliases_and_custom_names.abi | 32 + .../resources/merge/diverging/tvosX64.api | 19 + .../src/test/resources/merge/guess/common.api | 14 + .../test/resources/merge/guess/guessed.api | 17 + .../merge/guess/linuxArm64Specific.api | 13 + .../src/test/resources/merge/guess/merged.api | 24 + .../resources/merge/header-mismatch/v1.abi | 17 + .../resources/merge/header-mismatch/v2.abi | 17 + .../merge/idempotent/bcv-klib-test.abi | 34 + .../merge/identical/dump_linux_x64.abi | 17 + .../merge/identical/dump_macos_arm64.abi | 17 + .../test/resources/merge/identical/merged.abi | 15 + .../merge/illegalFiles/emptyFile.txt | 0 .../merge/illegalFiles/nonDumpFile.txt | 1 + .../merge/parseNarrowChildrenDecls/merged.abi | 16 + .../withoutLinuxAll.abi | 14 + .../withoutLinuxArm64.abi | 16 + .../resources/merge/stdlib_native_common.abi | 17 + .../test/resources/merge/webTargets/js.abi | 16 + .../resources/merge/webTargets/js.ext.abi | 15 + .../resources/merge/webTargets/merged.abi | 15 + .../resources/merge/webTargets/wasmJs.abi | 16 + .../resources/merge/webTargets/wasmJs.ext.abi | 15 + .../resources/merge/webTargets/wasmWasi.abi | 16 + .../merge/webTargets/wasmWasi.ext.abi | 15 + 107 files changed, 6696 insertions(+), 146 deletions(-) create mode 100644 libraries/tools/abi-validation/docs/design/KLibSupport.md create mode 100644 libraries/tools/abi-validation/src/functionalTest/kotlin/kotlinx/validation/test/KlibVerificationTests.kt rename libraries/tools/abi-validation/src/functionalTest/kotlin/kotlinx/validation/test/{MultiPlatformSingleJvmTargetTest.kt => MultiPlatformSingleJvmKlibTargetTest.kt} (97%) create mode 100644 libraries/tools/abi-validation/src/functionalTest/resources/examples/classes/AnotherBuildConfig.klib.clash.dump create mode 100644 libraries/tools/abi-validation/src/functionalTest/resources/examples/classes/AnotherBuildConfig.klib.custom.dump create mode 100644 libraries/tools/abi-validation/src/functionalTest/resources/examples/classes/AnotherBuildConfig.klib.dump create mode 100644 libraries/tools/abi-validation/src/functionalTest/resources/examples/classes/AnotherBuildConfig.klib.renamedTarget.dump create mode 100644 libraries/tools/abi-validation/src/functionalTest/resources/examples/classes/AnotherBuildConfig.klib.web.dump create mode 100644 libraries/tools/abi-validation/src/functionalTest/resources/examples/classes/AnotherBuildConfigLinux.klib.grouping.dump create mode 100644 libraries/tools/abi-validation/src/functionalTest/resources/examples/classes/AnotherBuildConfigLinuxArm64.kt create mode 100644 libraries/tools/abi-validation/src/functionalTest/resources/examples/classes/AnotherBuildConfigLinuxArm64Extra.klib.dump create mode 100644 libraries/tools/abi-validation/src/functionalTest/resources/examples/classes/AnotherBuildConfigModified.klib.dump create mode 100644 libraries/tools/abi-validation/src/functionalTest/resources/examples/classes/AnotherBuildConfigModified.kt create mode 100644 libraries/tools/abi-validation/src/functionalTest/resources/examples/classes/ClassWithPublicMarkers.klib.dump create mode 100644 libraries/tools/abi-validation/src/functionalTest/resources/examples/classes/Empty.klib.dump create mode 100644 libraries/tools/abi-validation/src/functionalTest/resources/examples/classes/HiddenDeclarations.klib.dump create mode 100644 libraries/tools/abi-validation/src/functionalTest/resources/examples/classes/HiddenDeclarations.kt create mode 100644 libraries/tools/abi-validation/src/functionalTest/resources/examples/classes/NonPublicMarkers.kt create mode 100644 libraries/tools/abi-validation/src/functionalTest/resources/examples/classes/Properties.klib.dump create mode 100644 libraries/tools/abi-validation/src/functionalTest/resources/examples/classes/SubPackage.kt create mode 100644 libraries/tools/abi-validation/src/functionalTest/resources/examples/classes/Subclasses.dump create mode 100644 libraries/tools/abi-validation/src/functionalTest/resources/examples/classes/Subclasses.klib.dump create mode 100644 libraries/tools/abi-validation/src/functionalTest/resources/examples/classes/Subclasses.kt create mode 100644 libraries/tools/abi-validation/src/functionalTest/resources/examples/classes/TopLevelDeclarations.klib.all.dump create mode 100644 libraries/tools/abi-validation/src/functionalTest/resources/examples/classes/TopLevelDeclarations.klib.dump create mode 100644 libraries/tools/abi-validation/src/functionalTest/resources/examples/classes/TopLevelDeclarations.klib.unsup.dump create mode 100644 libraries/tools/abi-validation/src/functionalTest/resources/examples/classes/TopLevelDeclarations.klib.v1.dump create mode 100644 libraries/tools/abi-validation/src/functionalTest/resources/examples/classes/TopLevelDeclarations.klib.with.guessed.linux.dump create mode 100644 libraries/tools/abi-validation/src/functionalTest/resources/examples/classes/TopLevelDeclarations.klib.with.linux.dump create mode 100644 libraries/tools/abi-validation/src/functionalTest/resources/examples/classes/TopLevelDeclarations.kt create mode 100644 libraries/tools/abi-validation/src/functionalTest/resources/examples/gradle/base/enableJvmInWithNativePlugin.gradle.kts create mode 100644 libraries/tools/abi-validation/src/functionalTest/resources/examples/gradle/base/withNativePlugin.gradle.kts create mode 100644 libraries/tools/abi-validation/src/functionalTest/resources/examples/gradle/base/withNativePluginAndNoTargets.gradle.kts create mode 100644 libraries/tools/abi-validation/src/functionalTest/resources/examples/gradle/base/withNativePluginAndSingleTarget.gradle.kts create mode 100644 libraries/tools/abi-validation/src/functionalTest/resources/examples/gradle/configuration/appleTargets/targets.gradle.kts create mode 100644 libraries/tools/abi-validation/src/functionalTest/resources/examples/gradle/configuration/grouping/clashingTargetNames.gradle.kts create mode 100644 libraries/tools/abi-validation/src/functionalTest/resources/examples/gradle/configuration/grouping/customTargetNames.gradle.kts create mode 100644 libraries/tools/abi-validation/src/functionalTest/resources/examples/gradle/configuration/ignoreSubclasses/ignore.gradle.kts create mode 100644 libraries/tools/abi-validation/src/functionalTest/resources/examples/gradle/configuration/nonNativeKlibTargets/targets.gradle.kts create mode 100644 libraries/tools/abi-validation/src/functionalTest/resources/examples/gradle/configuration/nonPublicMarkers/klib.gradle.kts create mode 100644 libraries/tools/abi-validation/src/functionalTest/resources/examples/gradle/configuration/signatures/invalid.gradle.kts create mode 100644 libraries/tools/abi-validation/src/functionalTest/resources/examples/gradle/configuration/signatures/v1.gradle.kts create mode 100644 libraries/tools/abi-validation/src/functionalTest/resources/examples/gradle/configuration/unsupported/enforce.gradle.kts create mode 100644 libraries/tools/abi-validation/src/main/kotlin/BuildTaskBase.kt create mode 100644 libraries/tools/abi-validation/src/main/kotlin/CopyFile.kt create mode 100644 libraries/tools/abi-validation/src/main/kotlin/ExperimentalBCVApi.kt create mode 100644 libraries/tools/abi-validation/src/main/kotlin/KotlinKlibAbiBuildTask.kt create mode 100644 libraries/tools/abi-validation/src/main/kotlin/KotlinKlibExtractSupportedTargetsAbiTask.kt create mode 100644 libraries/tools/abi-validation/src/main/kotlin/KotlinKlibInferAbiForUnsupportedTargetTask.kt create mode 100644 libraries/tools/abi-validation/src/main/kotlin/KotlinKlibMergeAbiTask.kt create mode 100644 libraries/tools/abi-validation/src/main/kotlin/api/klib/KlibAbiDumpFileMerger.kt create mode 100644 libraries/tools/abi-validation/src/main/kotlin/api/klib/KlibDump.kt create mode 100644 libraries/tools/abi-validation/src/main/kotlin/api/klib/KlibDumpFilters.kt create mode 100644 libraries/tools/abi-validation/src/main/kotlin/api/klib/KlibSignatureVersion.kt create mode 100644 libraries/tools/abi-validation/src/main/kotlin/api/klib/KlibTarget.kt create mode 100644 libraries/tools/abi-validation/src/main/kotlin/api/klib/TargetHierarchy.kt create mode 100644 libraries/tools/abi-validation/src/test/kotlin/samples/KlibDumpSamples.kt create mode 100644 libraries/tools/abi-validation/src/test/kotlin/tests/ClassNameConvertionTest.kt create mode 100644 libraries/tools/abi-validation/src/test/kotlin/tests/KlibAbiMergingTest.kt create mode 100644 libraries/tools/abi-validation/src/test/kotlin/tests/KlibDumpTest.kt create mode 100644 libraries/tools/abi-validation/src/test/kotlin/tests/KlibSignatureVersionTest.kt create mode 100644 libraries/tools/abi-validation/src/test/kotlin/tests/KlibTargetHierarchyTest.kt create mode 100644 libraries/tools/abi-validation/src/test/kotlin/tests/KlibTargetNameTest.kt create mode 100644 libraries/tools/abi-validation/src/test/resources/merge/diverging/androidNativeArm64.api create mode 100644 libraries/tools/abi-validation/src/test/resources/merge/diverging/linuxArm64.api create mode 100644 libraries/tools/abi-validation/src/test/resources/merge/diverging/linuxArm64.extracted.api create mode 100644 libraries/tools/abi-validation/src/test/resources/merge/diverging/linuxX64.api create mode 100644 libraries/tools/abi-validation/src/test/resources/merge/diverging/merged.abi create mode 100644 libraries/tools/abi-validation/src/test/resources/merge/diverging/merged_with_aliases.abi create mode 100644 libraries/tools/abi-validation/src/test/resources/merge/diverging/merged_with_aliases_and_custom_names.abi create mode 100644 libraries/tools/abi-validation/src/test/resources/merge/diverging/tvosX64.api create mode 100644 libraries/tools/abi-validation/src/test/resources/merge/guess/common.api create mode 100644 libraries/tools/abi-validation/src/test/resources/merge/guess/guessed.api create mode 100644 libraries/tools/abi-validation/src/test/resources/merge/guess/linuxArm64Specific.api create mode 100644 libraries/tools/abi-validation/src/test/resources/merge/guess/merged.api create mode 100644 libraries/tools/abi-validation/src/test/resources/merge/header-mismatch/v1.abi create mode 100644 libraries/tools/abi-validation/src/test/resources/merge/header-mismatch/v2.abi create mode 100644 libraries/tools/abi-validation/src/test/resources/merge/idempotent/bcv-klib-test.abi create mode 100644 libraries/tools/abi-validation/src/test/resources/merge/identical/dump_linux_x64.abi create mode 100644 libraries/tools/abi-validation/src/test/resources/merge/identical/dump_macos_arm64.abi create mode 100644 libraries/tools/abi-validation/src/test/resources/merge/identical/merged.abi create mode 100644 libraries/tools/abi-validation/src/test/resources/merge/illegalFiles/emptyFile.txt create mode 100644 libraries/tools/abi-validation/src/test/resources/merge/illegalFiles/nonDumpFile.txt create mode 100644 libraries/tools/abi-validation/src/test/resources/merge/parseNarrowChildrenDecls/merged.abi create mode 100644 libraries/tools/abi-validation/src/test/resources/merge/parseNarrowChildrenDecls/withoutLinuxAll.abi create mode 100644 libraries/tools/abi-validation/src/test/resources/merge/parseNarrowChildrenDecls/withoutLinuxArm64.abi create mode 100644 libraries/tools/abi-validation/src/test/resources/merge/stdlib_native_common.abi create mode 100644 libraries/tools/abi-validation/src/test/resources/merge/webTargets/js.abi create mode 100644 libraries/tools/abi-validation/src/test/resources/merge/webTargets/js.ext.abi create mode 100644 libraries/tools/abi-validation/src/test/resources/merge/webTargets/merged.abi create mode 100644 libraries/tools/abi-validation/src/test/resources/merge/webTargets/wasmJs.abi create mode 100644 libraries/tools/abi-validation/src/test/resources/merge/webTargets/wasmJs.ext.abi create mode 100644 libraries/tools/abi-validation/src/test/resources/merge/webTargets/wasmWasi.abi create mode 100644 libraries/tools/abi-validation/src/test/resources/merge/webTargets/wasmWasi.ext.abi diff --git a/libraries/tools/abi-validation/README.md b/libraries/tools/abi-validation/README.md index 0c0fb9f20df0c..c1efdcbf09a7b 100644 --- a/libraries/tools/abi-validation/README.md +++ b/libraries/tools/abi-validation/README.md @@ -13,6 +13,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) @@ -175,6 +176,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/libraries/tools/abi-validation/api/binary-compatibility-validator.api b/libraries/tools/abi-validation/api/binary-compatibility-validator.api index f226963e2c6d9..07d3bc3d444eb 100644 --- a/libraries/tools/abi-validation/api/binary-compatibility-validator.api +++ b/libraries/tools/abi-validation/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/libraries/tools/abi-validation/build.gradle.kts b/libraries/tools/abi-validation/build.gradle.kts index 1e18b4e008fd4..372c409da43a3 100644 --- a/libraries/tools/abi-validation/build.gradle.kts +++ b/libraries/tools/abi-validation/build.gradle.kts @@ -62,6 +62,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) @@ -78,7 +79,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) @@ -87,7 +87,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" ) } } @@ -162,6 +164,7 @@ testing { implementation(project()) implementation(libs.assertJ.core) implementation(libs.kotlin.test) + implementation(libs.kotlin.compiler.embeddable) } } diff --git a/libraries/tools/abi-validation/docs/design/KLibSupport.md b/libraries/tools/abi-validation/docs/design/KLibSupport.md new file mode 100644 index 0000000000000..1eac44ffd6258 --- /dev/null +++ b/libraries/tools/abi-validation/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/libraries/tools/abi-validation/gradle/libs.versions.toml b/libraries/tools/abi-validation/gradle/libs.versions.toml index bb0286253d47a..1d8e80ee3059f 100644 --- a/libraries/tools/abi-validation/gradle/libs.versions.toml +++ b/libraries/tools/abi-validation/gradle/libs.versions.toml @@ -30,6 +30,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 diff --git a/libraries/tools/abi-validation/src/functionalTest/kotlin/kotlinx/validation/api/BaseKotlinGradleTest.kt b/libraries/tools/abi-validation/src/functionalTest/kotlin/kotlinx/validation/api/BaseKotlinGradleTest.kt index 929f08cb044d2..d60db33d11268 100644 --- a/libraries/tools/abi-validation/src/functionalTest/kotlin/kotlinx/validation/api/BaseKotlinGradleTest.kt +++ b/libraries/tools/abi-validation/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/libraries/tools/abi-validation/src/functionalTest/kotlin/kotlinx/validation/api/TestDsl.kt b/libraries/tools/abi-validation/src/functionalTest/kotlin/kotlinx/validation/api/TestDsl.kt index cfa2579961fa5..42f3c9147311d 100644 --- a/libraries/tools/abi-validation/src/functionalTest/kotlin/kotlinx/validation/api/TestDsl.kt +++ b/libraries/tools/abi-validation/src/functionalTest/kotlin/kotlinx/validation/api/TestDsl.kt @@ -123,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] @@ -195,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/libraries/tools/abi-validation/src/functionalTest/kotlin/kotlinx/validation/test/IgnoredClassesTests.kt b/libraries/tools/abi-validation/src/functionalTest/kotlin/kotlinx/validation/test/IgnoredClassesTests.kt index e2f1a3ef5cc4a..06b3f86cecaf1 100644 --- a/libraries/tools/abi-validation/src/functionalTest/kotlin/kotlinx/validation/test/IgnoredClassesTests.kt +++ b/libraries/tools/abi-validation/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/libraries/tools/abi-validation/src/functionalTest/kotlin/kotlinx/validation/test/KlibVerificationTests.kt b/libraries/tools/abi-validation/src/functionalTest/kotlin/kotlinx/validation/test/KlibVerificationTests.kt new file mode 100644 index 0000000000000..63198ed794488 --- /dev/null +++ b/libraries/tools/abi-validation/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/libraries/tools/abi-validation/src/functionalTest/kotlin/kotlinx/validation/test/MultiPlatformSingleJvmTargetTest.kt b/libraries/tools/abi-validation/src/functionalTest/kotlin/kotlinx/validation/test/MultiPlatformSingleJvmKlibTargetTest.kt similarity index 97% rename from libraries/tools/abi-validation/src/functionalTest/kotlin/kotlinx/validation/test/MultiPlatformSingleJvmTargetTest.kt rename to libraries/tools/abi-validation/src/functionalTest/kotlin/kotlinx/validation/test/MultiPlatformSingleJvmKlibTargetTest.kt index f141fa83d64b6..dde16426ba63e 100644 --- a/libraries/tools/abi-validation/src/functionalTest/kotlin/kotlinx/validation/test/MultiPlatformSingleJvmTargetTest.kt +++ b/libraries/tools/abi-validation/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/libraries/tools/abi-validation/src/functionalTest/kotlin/kotlinx/validation/test/NonPublicMarkersTest.kt b/libraries/tools/abi-validation/src/functionalTest/kotlin/kotlinx/validation/test/NonPublicMarkersTest.kt index 689d93841b810..4bb804b797deb 100644 --- a/libraries/tools/abi-validation/src/functionalTest/kotlin/kotlinx/validation/test/NonPublicMarkersTest.kt +++ b/libraries/tools/abi-validation/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/libraries/tools/abi-validation/src/functionalTest/kotlin/kotlinx/validation/test/PublicMarkersTest.kt b/libraries/tools/abi-validation/src/functionalTest/kotlin/kotlinx/validation/test/PublicMarkersTest.kt index e03ee69e35f55..9a0b9dcf685ac 100644 --- a/libraries/tools/abi-validation/src/functionalTest/kotlin/kotlinx/validation/test/PublicMarkersTest.kt +++ b/libraries/tools/abi-validation/src/functionalTest/kotlin/kotlinx/validation/test/PublicMarkersTest.kt @@ -46,6 +46,41 @@ class PublicMarkersTest : BaseKotlinGradleTest() { } } + // 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") + } + } + @Test fun testFiltrationByPackageLevelAnnotations() { val runner = test { diff --git a/libraries/tools/abi-validation/src/functionalTest/resources/examples/classes/AnotherBuildConfig.klib.clash.dump b/libraries/tools/abi-validation/src/functionalTest/resources/examples/classes/AnotherBuildConfig.klib.clash.dump new file mode 100644 index 0000000000000..78f09cea52008 --- /dev/null +++ b/libraries/tools/abi-validation/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/libraries/tools/abi-validation/src/functionalTest/resources/examples/classes/AnotherBuildConfig.klib.custom.dump b/libraries/tools/abi-validation/src/functionalTest/resources/examples/classes/AnotherBuildConfig.klib.custom.dump new file mode 100644 index 0000000000000..1adbdac86f8e9 --- /dev/null +++ b/libraries/tools/abi-validation/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/libraries/tools/abi-validation/src/functionalTest/resources/examples/classes/AnotherBuildConfig.klib.dump b/libraries/tools/abi-validation/src/functionalTest/resources/examples/classes/AnotherBuildConfig.klib.dump new file mode 100644 index 0000000000000..9511bef956629 --- /dev/null +++ b/libraries/tools/abi-validation/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/libraries/tools/abi-validation/src/functionalTest/resources/examples/classes/AnotherBuildConfig.klib.renamedTarget.dump b/libraries/tools/abi-validation/src/functionalTest/resources/examples/classes/AnotherBuildConfig.klib.renamedTarget.dump new file mode 100644 index 0000000000000..8b84f00ee5537 --- /dev/null +++ b/libraries/tools/abi-validation/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/libraries/tools/abi-validation/src/functionalTest/resources/examples/classes/AnotherBuildConfig.klib.web.dump b/libraries/tools/abi-validation/src/functionalTest/resources/examples/classes/AnotherBuildConfig.klib.web.dump new file mode 100644 index 0000000000000..bebc349d25a44 --- /dev/null +++ b/libraries/tools/abi-validation/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/libraries/tools/abi-validation/src/functionalTest/resources/examples/classes/AnotherBuildConfigLinux.klib.grouping.dump b/libraries/tools/abi-validation/src/functionalTest/resources/examples/classes/AnotherBuildConfigLinux.klib.grouping.dump new file mode 100644 index 0000000000000..f67ac4434d5ba --- /dev/null +++ b/libraries/tools/abi-validation/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/libraries/tools/abi-validation/src/functionalTest/resources/examples/classes/AnotherBuildConfigLinuxArm64.kt b/libraries/tools/abi-validation/src/functionalTest/resources/examples/classes/AnotherBuildConfigLinuxArm64.kt new file mode 100644 index 0000000000000..f5352c0b9eb4f --- /dev/null +++ b/libraries/tools/abi-validation/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/libraries/tools/abi-validation/src/functionalTest/resources/examples/classes/AnotherBuildConfigLinuxArm64Extra.klib.dump b/libraries/tools/abi-validation/src/functionalTest/resources/examples/classes/AnotherBuildConfigLinuxArm64Extra.klib.dump new file mode 100644 index 0000000000000..20292d7e071cc --- /dev/null +++ b/libraries/tools/abi-validation/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/libraries/tools/abi-validation/src/functionalTest/resources/examples/classes/AnotherBuildConfigModified.klib.dump b/libraries/tools/abi-validation/src/functionalTest/resources/examples/classes/AnotherBuildConfigModified.klib.dump new file mode 100644 index 0000000000000..75eb66b2220cc --- /dev/null +++ b/libraries/tools/abi-validation/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/libraries/tools/abi-validation/src/functionalTest/resources/examples/classes/AnotherBuildConfigModified.kt b/libraries/tools/abi-validation/src/functionalTest/resources/examples/classes/AnotherBuildConfigModified.kt new file mode 100644 index 0000000000000..8165117ba9226 --- /dev/null +++ b/libraries/tools/abi-validation/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/libraries/tools/abi-validation/src/functionalTest/resources/examples/classes/ClassWithPublicMarkers.klib.dump b/libraries/tools/abi-validation/src/functionalTest/resources/examples/classes/ClassWithPublicMarkers.klib.dump new file mode 100644 index 0000000000000..b1e8f295d2cc9 --- /dev/null +++ b/libraries/tools/abi-validation/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/libraries/tools/abi-validation/src/functionalTest/resources/examples/classes/Empty.klib.dump b/libraries/tools/abi-validation/src/functionalTest/resources/examples/classes/Empty.klib.dump new file mode 100644 index 0000000000000..40583d92ba8a1 --- /dev/null +++ b/libraries/tools/abi-validation/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/libraries/tools/abi-validation/src/functionalTest/resources/examples/classes/HiddenDeclarations.klib.dump b/libraries/tools/abi-validation/src/functionalTest/resources/examples/classes/HiddenDeclarations.klib.dump new file mode 100644 index 0000000000000..bed1c06fc016e --- /dev/null +++ b/libraries/tools/abi-validation/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/libraries/tools/abi-validation/src/functionalTest/resources/examples/classes/HiddenDeclarations.kt b/libraries/tools/abi-validation/src/functionalTest/resources/examples/classes/HiddenDeclarations.kt new file mode 100644 index 0000000000000..702ed05ad3fe2 --- /dev/null +++ b/libraries/tools/abi-validation/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/libraries/tools/abi-validation/src/functionalTest/resources/examples/classes/NonPublicMarkers.kt b/libraries/tools/abi-validation/src/functionalTest/resources/examples/classes/NonPublicMarkers.kt new file mode 100644 index 0000000000000..fdf828879628b --- /dev/null +++ b/libraries/tools/abi-validation/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/libraries/tools/abi-validation/src/functionalTest/resources/examples/classes/Properties.klib.dump b/libraries/tools/abi-validation/src/functionalTest/resources/examples/classes/Properties.klib.dump new file mode 100644 index 0000000000000..6359372b06faf --- /dev/null +++ b/libraries/tools/abi-validation/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/libraries/tools/abi-validation/src/functionalTest/resources/examples/classes/SubPackage.kt b/libraries/tools/abi-validation/src/functionalTest/resources/examples/classes/SubPackage.kt new file mode 100644 index 0000000000000..c5a298b573677 --- /dev/null +++ b/libraries/tools/abi-validation/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/libraries/tools/abi-validation/src/functionalTest/resources/examples/classes/Subclasses.dump b/libraries/tools/abi-validation/src/functionalTest/resources/examples/classes/Subclasses.dump new file mode 100644 index 0000000000000..04cb15232641f --- /dev/null +++ b/libraries/tools/abi-validation/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/libraries/tools/abi-validation/src/functionalTest/resources/examples/classes/Subclasses.klib.dump b/libraries/tools/abi-validation/src/functionalTest/resources/examples/classes/Subclasses.klib.dump new file mode 100644 index 0000000000000..e13fa3f69e6ae --- /dev/null +++ b/libraries/tools/abi-validation/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/libraries/tools/abi-validation/src/functionalTest/resources/examples/classes/Subclasses.kt b/libraries/tools/abi-validation/src/functionalTest/resources/examples/classes/Subclasses.kt new file mode 100644 index 0000000000000..7f3f6392d6559 --- /dev/null +++ b/libraries/tools/abi-validation/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/libraries/tools/abi-validation/src/functionalTest/resources/examples/classes/TopLevelDeclarations.klib.all.dump b/libraries/tools/abi-validation/src/functionalTest/resources/examples/classes/TopLevelDeclarations.klib.all.dump new file mode 100644 index 0000000000000..aa08591a0d6ec --- /dev/null +++ b/libraries/tools/abi-validation/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/libraries/tools/abi-validation/src/functionalTest/resources/examples/classes/TopLevelDeclarations.klib.dump b/libraries/tools/abi-validation/src/functionalTest/resources/examples/classes/TopLevelDeclarations.klib.dump new file mode 100644 index 0000000000000..c7bb38f6b8872 --- /dev/null +++ b/libraries/tools/abi-validation/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/libraries/tools/abi-validation/src/functionalTest/resources/examples/classes/TopLevelDeclarations.klib.unsup.dump b/libraries/tools/abi-validation/src/functionalTest/resources/examples/classes/TopLevelDeclarations.klib.unsup.dump new file mode 100644 index 0000000000000..9764aec35f364 --- /dev/null +++ b/libraries/tools/abi-validation/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/libraries/tools/abi-validation/src/functionalTest/resources/examples/classes/TopLevelDeclarations.klib.v1.dump b/libraries/tools/abi-validation/src/functionalTest/resources/examples/classes/TopLevelDeclarations.klib.v1.dump new file mode 100644 index 0000000000000..9442fd647f230 --- /dev/null +++ b/libraries/tools/abi-validation/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/libraries/tools/abi-validation/src/functionalTest/resources/examples/classes/TopLevelDeclarations.klib.with.guessed.linux.dump b/libraries/tools/abi-validation/src/functionalTest/resources/examples/classes/TopLevelDeclarations.klib.with.guessed.linux.dump new file mode 100644 index 0000000000000..5b397f221e4db --- /dev/null +++ b/libraries/tools/abi-validation/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/libraries/tools/abi-validation/src/functionalTest/resources/examples/classes/TopLevelDeclarations.klib.with.linux.dump b/libraries/tools/abi-validation/src/functionalTest/resources/examples/classes/TopLevelDeclarations.klib.with.linux.dump new file mode 100644 index 0000000000000..c7bb38f6b8872 --- /dev/null +++ b/libraries/tools/abi-validation/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/libraries/tools/abi-validation/src/functionalTest/resources/examples/classes/TopLevelDeclarations.kt b/libraries/tools/abi-validation/src/functionalTest/resources/examples/classes/TopLevelDeclarations.kt new file mode 100644 index 0000000000000..067bd288b09fa --- /dev/null +++ b/libraries/tools/abi-validation/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/libraries/tools/abi-validation/src/functionalTest/resources/examples/gradle/base/enableJvmInWithNativePlugin.gradle.kts b/libraries/tools/abi-validation/src/functionalTest/resources/examples/gradle/base/enableJvmInWithNativePlugin.gradle.kts new file mode 100644 index 0000000000000..439d731f5c96c --- /dev/null +++ b/libraries/tools/abi-validation/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/libraries/tools/abi-validation/src/functionalTest/resources/examples/gradle/base/withNativePlugin.gradle.kts b/libraries/tools/abi-validation/src/functionalTest/resources/examples/gradle/base/withNativePlugin.gradle.kts new file mode 100644 index 0000000000000..ed1e174ceb715 --- /dev/null +++ b/libraries/tools/abi-validation/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/libraries/tools/abi-validation/src/functionalTest/resources/examples/gradle/base/withNativePluginAndNoTargets.gradle.kts b/libraries/tools/abi-validation/src/functionalTest/resources/examples/gradle/base/withNativePluginAndNoTargets.gradle.kts new file mode 100644 index 0000000000000..5066876634e23 --- /dev/null +++ b/libraries/tools/abi-validation/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/libraries/tools/abi-validation/src/functionalTest/resources/examples/gradle/base/withNativePluginAndSingleTarget.gradle.kts b/libraries/tools/abi-validation/src/functionalTest/resources/examples/gradle/base/withNativePluginAndSingleTarget.gradle.kts new file mode 100644 index 0000000000000..db44005db628c --- /dev/null +++ b/libraries/tools/abi-validation/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/libraries/tools/abi-validation/src/functionalTest/resources/examples/gradle/configuration/appleTargets/targets.gradle.kts b/libraries/tools/abi-validation/src/functionalTest/resources/examples/gradle/configuration/appleTargets/targets.gradle.kts new file mode 100644 index 0000000000000..c3384eb841e79 --- /dev/null +++ b/libraries/tools/abi-validation/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/libraries/tools/abi-validation/src/functionalTest/resources/examples/gradle/configuration/grouping/clashingTargetNames.gradle.kts b/libraries/tools/abi-validation/src/functionalTest/resources/examples/gradle/configuration/grouping/clashingTargetNames.gradle.kts new file mode 100644 index 0000000000000..94581efc843bc --- /dev/null +++ b/libraries/tools/abi-validation/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/libraries/tools/abi-validation/src/functionalTest/resources/examples/gradle/configuration/grouping/customTargetNames.gradle.kts b/libraries/tools/abi-validation/src/functionalTest/resources/examples/gradle/configuration/grouping/customTargetNames.gradle.kts new file mode 100644 index 0000000000000..73aafef947fd3 --- /dev/null +++ b/libraries/tools/abi-validation/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/libraries/tools/abi-validation/src/functionalTest/resources/examples/gradle/configuration/ignoreSubclasses/ignore.gradle.kts b/libraries/tools/abi-validation/src/functionalTest/resources/examples/gradle/configuration/ignoreSubclasses/ignore.gradle.kts new file mode 100644 index 0000000000000..776e96fdb7945 --- /dev/null +++ b/libraries/tools/abi-validation/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/libraries/tools/abi-validation/src/functionalTest/resources/examples/gradle/configuration/nonNativeKlibTargets/targets.gradle.kts b/libraries/tools/abi-validation/src/functionalTest/resources/examples/gradle/configuration/nonNativeKlibTargets/targets.gradle.kts new file mode 100644 index 0000000000000..f6030484bc8ae --- /dev/null +++ b/libraries/tools/abi-validation/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/libraries/tools/abi-validation/src/functionalTest/resources/examples/gradle/configuration/nonPublicMarkers/klib.gradle.kts b/libraries/tools/abi-validation/src/functionalTest/resources/examples/gradle/configuration/nonPublicMarkers/klib.gradle.kts new file mode 100644 index 0000000000000..e85338160ff5b --- /dev/null +++ b/libraries/tools/abi-validation/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/libraries/tools/abi-validation/src/functionalTest/resources/examples/gradle/configuration/signatures/invalid.gradle.kts b/libraries/tools/abi-validation/src/functionalTest/resources/examples/gradle/configuration/signatures/invalid.gradle.kts new file mode 100644 index 0000000000000..ddb2f310b1927 --- /dev/null +++ b/libraries/tools/abi-validation/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/libraries/tools/abi-validation/src/functionalTest/resources/examples/gradle/configuration/signatures/v1.gradle.kts b/libraries/tools/abi-validation/src/functionalTest/resources/examples/gradle/configuration/signatures/v1.gradle.kts new file mode 100644 index 0000000000000..28f0322fc3d1f --- /dev/null +++ b/libraries/tools/abi-validation/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/libraries/tools/abi-validation/src/functionalTest/resources/examples/gradle/configuration/unsupported/enforce.gradle.kts b/libraries/tools/abi-validation/src/functionalTest/resources/examples/gradle/configuration/unsupported/enforce.gradle.kts new file mode 100644 index 0000000000000..99270cdee66b7 --- /dev/null +++ b/libraries/tools/abi-validation/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/libraries/tools/abi-validation/src/main/kotlin/ApiValidationExtension.kt b/libraries/tools/abi-validation/src/main/kotlin/ApiValidationExtension.kt index ed09ba42e6f73..7a87977559fab 100644 --- a/libraries/tools/abi-validation/src/main/kotlin/ApiValidationExtension.kt +++ b/libraries/tools/abi-validation/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/libraries/tools/abi-validation/src/main/kotlin/BinaryCompatibilityValidatorPlugin.kt b/libraries/tools/abi-validation/src/main/kotlin/BinaryCompatibilityValidatorPlugin.kt index c117659fa4168..1548c4db163e6 100644 --- a/libraries/tools/abi-validation/src/main/kotlin/BinaryCompatibilityValidatorPlugin.kt +++ b/libraries/tools/abi-validation/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/libraries/tools/abi-validation/src/main/kotlin/BuildTaskBase.kt b/libraries/tools/abi-validation/src/main/kotlin/BuildTaskBase.kt new file mode 100644 index 0000000000000..a042f2d8abb57 --- /dev/null +++ b/libraries/tools/abi-validation/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/libraries/tools/abi-validation/src/main/kotlin/CopyFile.kt b/libraries/tools/abi-validation/src/main/kotlin/CopyFile.kt new file mode 100644 index 0000000000000..df9f6d79f51d5 --- /dev/null +++ b/libraries/tools/abi-validation/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/libraries/tools/abi-validation/src/main/kotlin/ExperimentalBCVApi.kt b/libraries/tools/abi-validation/src/main/kotlin/ExperimentalBCVApi.kt new file mode 100644 index 0000000000000..7258943a1a0bc --- /dev/null +++ b/libraries/tools/abi-validation/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/libraries/tools/abi-validation/src/main/kotlin/KotlinApiBuildTask.kt b/libraries/tools/abi-validation/src/main/kotlin/KotlinApiBuildTask.kt index 4077dc3dff74b..70881de469929 100644 --- a/libraries/tools/abi-validation/src/main/kotlin/KotlinApiBuildTask.kt +++ b/libraries/tools/abi-validation/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,21 +59,9 @@ public open class KotlinApiBuildTask @Inject constructor( .filterOutNonPublic(ignoredPackages + ignoredPackagesNames, ignoredClasses) .filterOutAnnotated(nonPublicMarkers.map(::replaceDots).toSet()) - outputApiDir.resolve("$projectName.api").bufferedWriter().use { writer -> + outputApiFile.bufferedWriter().use { writer -> filteredSignatures.dump(writer) } } - - private fun cleanup(file: File) { - if (file.exists()) { - val listing = file.listFiles() - if (listing != null) { - for (sub in listing) { - cleanup(sub) - } - } - file.delete() - } - } } diff --git a/libraries/tools/abi-validation/src/main/kotlin/KotlinApiCompareTask.kt b/libraries/tools/abi-validation/src/main/kotlin/KotlinApiCompareTask.kt index ac4eed55fd88d..88e693effd473 100644 --- a/libraries/tools/abi-validation/src/main/kotlin/KotlinApiCompareTask.kt +++ b/libraries/tools/abi-validation/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/libraries/tools/abi-validation/src/main/kotlin/KotlinKlibAbiBuildTask.kt b/libraries/tools/abi-validation/src/main/kotlin/KotlinKlibAbiBuildTask.kt new file mode 100644 index 0000000000000..c591c632347ce --- /dev/null +++ b/libraries/tools/abi-validation/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/libraries/tools/abi-validation/src/main/kotlin/KotlinKlibExtractSupportedTargetsAbiTask.kt b/libraries/tools/abi-validation/src/main/kotlin/KotlinKlibExtractSupportedTargetsAbiTask.kt new file mode 100644 index 0000000000000..58298a186386a --- /dev/null +++ b/libraries/tools/abi-validation/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/libraries/tools/abi-validation/src/main/kotlin/KotlinKlibInferAbiForUnsupportedTargetTask.kt b/libraries/tools/abi-validation/src/main/kotlin/KotlinKlibInferAbiForUnsupportedTargetTask.kt new file mode 100644 index 0000000000000..af0228f0eebae --- /dev/null +++ b/libraries/tools/abi-validation/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/libraries/tools/abi-validation/src/main/kotlin/KotlinKlibMergeAbiTask.kt b/libraries/tools/abi-validation/src/main/kotlin/KotlinKlibMergeAbiTask.kt new file mode 100644 index 0000000000000..ebbf04e2c6786 --- /dev/null +++ b/libraries/tools/abi-validation/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/libraries/tools/abi-validation/src/main/kotlin/api/klib/KlibAbiDumpFileMerger.kt b/libraries/tools/abi-validation/src/main/kotlin/api/klib/KlibAbiDumpFileMerger.kt new file mode 100644 index 0000000000000..a5eddc7ea1e6e --- /dev/null +++ b/libraries/tools/abi-validation/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/libraries/tools/abi-validation/src/main/kotlin/api/klib/KlibDump.kt b/libraries/tools/abi-validation/src/main/kotlin/api/klib/KlibDump.kt new file mode 100644 index 0000000000000..2b8f35c4e8151 --- /dev/null +++ b/libraries/tools/abi-validation/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/libraries/tools/abi-validation/src/main/kotlin/api/klib/KlibDumpFilters.kt b/libraries/tools/abi-validation/src/main/kotlin/api/klib/KlibDumpFilters.kt new file mode 100644 index 0000000000000..ce90824bb7dea --- /dev/null +++ b/libraries/tools/abi-validation/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/libraries/tools/abi-validation/src/main/kotlin/api/klib/KlibSignatureVersion.kt b/libraries/tools/abi-validation/src/main/kotlin/api/klib/KlibSignatureVersion.kt new file mode 100644 index 0000000000000..eee9ba2049855 --- /dev/null +++ b/libraries/tools/abi-validation/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/libraries/tools/abi-validation/src/main/kotlin/api/klib/KlibTarget.kt b/libraries/tools/abi-validation/src/main/kotlin/api/klib/KlibTarget.kt new file mode 100644 index 0000000000000..bfcc78dab5dae --- /dev/null +++ b/libraries/tools/abi-validation/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/libraries/tools/abi-validation/src/main/kotlin/api/klib/TargetHierarchy.kt b/libraries/tools/abi-validation/src/main/kotlin/api/klib/TargetHierarchy.kt new file mode 100644 index 0000000000000..fe5aac63dd59d --- /dev/null +++ b/libraries/tools/abi-validation/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/libraries/tools/abi-validation/src/test/kotlin/samples/KlibDumpSamples.kt b/libraries/tools/abi-validation/src/test/kotlin/samples/KlibDumpSamples.kt new file mode 100644 index 0000000000000..f3cf5e012d66d --- /dev/null +++ b/libraries/tools/abi-validation/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/libraries/tools/abi-validation/src/test/kotlin/tests/ClassNameConvertionTest.kt b/libraries/tools/abi-validation/src/test/kotlin/tests/ClassNameConvertionTest.kt new file mode 100644 index 0000000000000..9777d4fb812c6 --- /dev/null +++ b/libraries/tools/abi-validation/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/libraries/tools/abi-validation/src/test/kotlin/tests/KlibAbiMergingTest.kt b/libraries/tools/abi-validation/src/test/kotlin/tests/KlibAbiMergingTest.kt new file mode 100644 index 0000000000000..b4fdf42ce11ab --- /dev/null +++ b/libraries/tools/abi-validation/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/libraries/tools/abi-validation/src/test/kotlin/tests/KlibDumpTest.kt b/libraries/tools/abi-validation/src/test/kotlin/tests/KlibDumpTest.kt new file mode 100644 index 0000000000000..7915e4df8bc77 --- /dev/null +++ b/libraries/tools/abi-validation/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/libraries/tools/abi-validation/src/test/kotlin/tests/KlibSignatureVersionTest.kt b/libraries/tools/abi-validation/src/test/kotlin/tests/KlibSignatureVersionTest.kt new file mode 100644 index 0000000000000..d08abb750a9ff --- /dev/null +++ b/libraries/tools/abi-validation/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/libraries/tools/abi-validation/src/test/kotlin/tests/KlibTargetHierarchyTest.kt b/libraries/tools/abi-validation/src/test/kotlin/tests/KlibTargetHierarchyTest.kt new file mode 100644 index 0000000000000..52a298be1665e --- /dev/null +++ b/libraries/tools/abi-validation/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/libraries/tools/abi-validation/src/test/kotlin/tests/KlibTargetNameTest.kt b/libraries/tools/abi-validation/src/test/kotlin/tests/KlibTargetNameTest.kt new file mode 100644 index 0000000000000..997cd220672a9 --- /dev/null +++ b/libraries/tools/abi-validation/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/libraries/tools/abi-validation/src/test/resources/merge/diverging/androidNativeArm64.api b/libraries/tools/abi-validation/src/test/resources/merge/diverging/androidNativeArm64.api new file mode 100644 index 0000000000000..970b886073997 --- /dev/null +++ b/libraries/tools/abi-validation/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/libraries/tools/abi-validation/src/test/resources/merge/diverging/linuxArm64.api b/libraries/tools/abi-validation/src/test/resources/merge/diverging/linuxArm64.api new file mode 100644 index 0000000000000..ee8b81c8a1504 --- /dev/null +++ b/libraries/tools/abi-validation/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/libraries/tools/abi-validation/src/test/resources/merge/diverging/linuxArm64.extracted.api b/libraries/tools/abi-validation/src/test/resources/merge/diverging/linuxArm64.extracted.api new file mode 100644 index 0000000000000..f0b0753b08f2c --- /dev/null +++ b/libraries/tools/abi-validation/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/libraries/tools/abi-validation/src/test/resources/merge/diverging/linuxX64.api b/libraries/tools/abi-validation/src/test/resources/merge/diverging/linuxX64.api new file mode 100644 index 0000000000000..e827b6c6a8a26 --- /dev/null +++ b/libraries/tools/abi-validation/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/libraries/tools/abi-validation/src/test/resources/merge/diverging/merged.abi b/libraries/tools/abi-validation/src/test/resources/merge/diverging/merged.abi new file mode 100644 index 0000000000000..de31ec4a1e7ed --- /dev/null +++ b/libraries/tools/abi-validation/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/libraries/tools/abi-validation/src/test/resources/merge/diverging/merged_with_aliases.abi b/libraries/tools/abi-validation/src/test/resources/merge/diverging/merged_with_aliases.abi new file mode 100644 index 0000000000000..de31ec4a1e7ed --- /dev/null +++ b/libraries/tools/abi-validation/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/libraries/tools/abi-validation/src/test/resources/merge/diverging/merged_with_aliases_and_custom_names.abi b/libraries/tools/abi-validation/src/test/resources/merge/diverging/merged_with_aliases_and_custom_names.abi new file mode 100644 index 0000000000000..1013b501fc4c7 --- /dev/null +++ b/libraries/tools/abi-validation/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/libraries/tools/abi-validation/src/test/resources/merge/diverging/tvosX64.api b/libraries/tools/abi-validation/src/test/resources/merge/diverging/tvosX64.api new file mode 100644 index 0000000000000..72bf3f99cf2bc --- /dev/null +++ b/libraries/tools/abi-validation/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/libraries/tools/abi-validation/src/test/resources/merge/guess/common.api b/libraries/tools/abi-validation/src/test/resources/merge/guess/common.api new file mode 100644 index 0000000000000..b2823f75bb8ef --- /dev/null +++ b/libraries/tools/abi-validation/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/libraries/tools/abi-validation/src/test/resources/merge/guess/guessed.api b/libraries/tools/abi-validation/src/test/resources/merge/guess/guessed.api new file mode 100644 index 0000000000000..ebed8a7601ac6 --- /dev/null +++ b/libraries/tools/abi-validation/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/libraries/tools/abi-validation/src/test/resources/merge/guess/linuxArm64Specific.api b/libraries/tools/abi-validation/src/test/resources/merge/guess/linuxArm64Specific.api new file mode 100644 index 0000000000000..f52246ee5f431 --- /dev/null +++ b/libraries/tools/abi-validation/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/libraries/tools/abi-validation/src/test/resources/merge/guess/merged.api b/libraries/tools/abi-validation/src/test/resources/merge/guess/merged.api new file mode 100644 index 0000000000000..adf561ecf74d3 --- /dev/null +++ b/libraries/tools/abi-validation/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/libraries/tools/abi-validation/src/test/resources/merge/header-mismatch/v1.abi b/libraries/tools/abi-validation/src/test/resources/merge/header-mismatch/v1.abi new file mode 100644 index 0000000000000..34fdd0061e8b0 --- /dev/null +++ b/libraries/tools/abi-validation/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/libraries/tools/abi-validation/src/test/resources/merge/header-mismatch/v2.abi b/libraries/tools/abi-validation/src/test/resources/merge/header-mismatch/v2.abi new file mode 100644 index 0000000000000..ca9ca7cf255d5 --- /dev/null +++ b/libraries/tools/abi-validation/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/libraries/tools/abi-validation/src/test/resources/merge/idempotent/bcv-klib-test.abi b/libraries/tools/abi-validation/src/test/resources/merge/idempotent/bcv-klib-test.abi new file mode 100644 index 0000000000000..7a81715c4dfb3 --- /dev/null +++ b/libraries/tools/abi-validation/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/libraries/tools/abi-validation/src/test/resources/merge/identical/dump_linux_x64.abi b/libraries/tools/abi-validation/src/test/resources/merge/identical/dump_linux_x64.abi new file mode 100644 index 0000000000000..ca9ca7cf255d5 --- /dev/null +++ b/libraries/tools/abi-validation/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/libraries/tools/abi-validation/src/test/resources/merge/identical/dump_macos_arm64.abi b/libraries/tools/abi-validation/src/test/resources/merge/identical/dump_macos_arm64.abi new file mode 100644 index 0000000000000..a51050857bb2c --- /dev/null +++ b/libraries/tools/abi-validation/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/libraries/tools/abi-validation/src/test/resources/merge/identical/merged.abi b/libraries/tools/abi-validation/src/test/resources/merge/identical/merged.abi new file mode 100644 index 0000000000000..27a1c6d6947a9 --- /dev/null +++ b/libraries/tools/abi-validation/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/libraries/tools/abi-validation/src/test/resources/merge/illegalFiles/emptyFile.txt b/libraries/tools/abi-validation/src/test/resources/merge/illegalFiles/emptyFile.txt new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/libraries/tools/abi-validation/src/test/resources/merge/illegalFiles/nonDumpFile.txt b/libraries/tools/abi-validation/src/test/resources/merge/illegalFiles/nonDumpFile.txt new file mode 100644 index 0000000000000..1a4cac6c5d6bf --- /dev/null +++ b/libraries/tools/abi-validation/src/test/resources/merge/illegalFiles/nonDumpFile.txt @@ -0,0 +1 @@ +I'm not a dump diff --git a/libraries/tools/abi-validation/src/test/resources/merge/parseNarrowChildrenDecls/merged.abi b/libraries/tools/abi-validation/src/test/resources/merge/parseNarrowChildrenDecls/merged.abi new file mode 100644 index 0000000000000..e9a1408a24294 --- /dev/null +++ b/libraries/tools/abi-validation/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/libraries/tools/abi-validation/src/test/resources/merge/parseNarrowChildrenDecls/withoutLinuxAll.abi b/libraries/tools/abi-validation/src/test/resources/merge/parseNarrowChildrenDecls/withoutLinuxAll.abi new file mode 100644 index 0000000000000..07552376d5886 --- /dev/null +++ b/libraries/tools/abi-validation/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/libraries/tools/abi-validation/src/test/resources/merge/parseNarrowChildrenDecls/withoutLinuxArm64.abi b/libraries/tools/abi-validation/src/test/resources/merge/parseNarrowChildrenDecls/withoutLinuxArm64.abi new file mode 100644 index 0000000000000..f8ae14ba2e70f --- /dev/null +++ b/libraries/tools/abi-validation/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/libraries/tools/abi-validation/src/test/resources/merge/stdlib_native_common.abi b/libraries/tools/abi-validation/src/test/resources/merge/stdlib_native_common.abi new file mode 100644 index 0000000000000..a475212a4a8c7 --- /dev/null +++ b/libraries/tools/abi-validation/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/libraries/tools/abi-validation/src/test/resources/merge/webTargets/js.abi b/libraries/tools/abi-validation/src/test/resources/merge/webTargets/js.abi new file mode 100644 index 0000000000000..0afbc790a232c --- /dev/null +++ b/libraries/tools/abi-validation/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/libraries/tools/abi-validation/src/test/resources/merge/webTargets/js.ext.abi b/libraries/tools/abi-validation/src/test/resources/merge/webTargets/js.ext.abi new file mode 100644 index 0000000000000..fdb214621edee --- /dev/null +++ b/libraries/tools/abi-validation/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/libraries/tools/abi-validation/src/test/resources/merge/webTargets/merged.abi b/libraries/tools/abi-validation/src/test/resources/merge/webTargets/merged.abi new file mode 100644 index 0000000000000..29f24c8f2f284 --- /dev/null +++ b/libraries/tools/abi-validation/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/libraries/tools/abi-validation/src/test/resources/merge/webTargets/wasmJs.abi b/libraries/tools/abi-validation/src/test/resources/merge/webTargets/wasmJs.abi new file mode 100644 index 0000000000000..56ba4e9b35089 --- /dev/null +++ b/libraries/tools/abi-validation/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/libraries/tools/abi-validation/src/test/resources/merge/webTargets/wasmJs.ext.abi b/libraries/tools/abi-validation/src/test/resources/merge/webTargets/wasmJs.ext.abi new file mode 100644 index 0000000000000..44cb7a7fe2e95 --- /dev/null +++ b/libraries/tools/abi-validation/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/libraries/tools/abi-validation/src/test/resources/merge/webTargets/wasmWasi.abi b/libraries/tools/abi-validation/src/test/resources/merge/webTargets/wasmWasi.abi new file mode 100644 index 0000000000000..56ba4e9b35089 --- /dev/null +++ b/libraries/tools/abi-validation/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/libraries/tools/abi-validation/src/test/resources/merge/webTargets/wasmWasi.ext.abi b/libraries/tools/abi-validation/src/test/resources/merge/webTargets/wasmWasi.ext.abi new file mode 100644 index 0000000000000..6e7083ed91a92 --- /dev/null +++ b/libraries/tools/abi-validation/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]