diff --git a/api/binary-compatibility-validator.api b/api/binary-compatibility-validator.api index 0c58f030..6586f4f0 100644 --- a/api/binary-compatibility-validator.api +++ b/api/binary-compatibility-validator.api @@ -30,7 +30,7 @@ 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 abstract class kotlinx/validation/BuildTaskBase : kotlinx/validation/WorkerAwareTaskBase { public fun ()V public final fun getIgnoredClasses ()Lorg/gradle/api/provider/SetProperty; public final fun getIgnoredPackages ()Lorg/gradle/api/provider/SetProperty; @@ -86,28 +86,36 @@ public abstract class kotlinx/validation/KotlinKlibAbiBuildTask : kotlinx/valida public abstract fun getTarget ()Lorg/gradle/api/provider/Property; } -public abstract class kotlinx/validation/KotlinKlibExtractAbiTask : org/gradle/api/DefaultTask { +public abstract class kotlinx/validation/KotlinKlibExtractAbiTask : kotlinx/validation/WorkerAwareTaskBase { public fun ()V + public abstract fun getExecutor ()Lorg/gradle/workers/WorkerExecutor; public abstract fun getInputAbiFile ()Lorg/gradle/api/file/RegularFileProperty; public abstract fun getOutputAbiFile ()Lorg/gradle/api/file/RegularFileProperty; public final fun getStrictValidation ()Lorg/gradle/api/provider/Property; public abstract fun getTargetsToRemove ()Lorg/gradle/api/provider/SetProperty; } -public abstract class kotlinx/validation/KotlinKlibInferAbiTask : org/gradle/api/DefaultTask { +public abstract class kotlinx/validation/KotlinKlibInferAbiTask : kotlinx/validation/WorkerAwareTaskBase { public fun ()V + public abstract fun getExecutor ()Lorg/gradle/workers/WorkerExecutor; public abstract fun getInputDumps ()Lorg/gradle/api/provider/SetProperty; public abstract fun getOldMergedKlibDump ()Lorg/gradle/api/file/RegularFileProperty; public abstract fun getOutputAbiFile ()Lorg/gradle/api/file/RegularFileProperty; public abstract fun getTarget ()Lorg/gradle/api/provider/Property; } -public abstract class kotlinx/validation/KotlinKlibMergeAbiTask : org/gradle/api/DefaultTask { +public abstract class kotlinx/validation/KotlinKlibMergeAbiTask : kotlinx/validation/WorkerAwareTaskBase { public fun ()V public abstract fun getDumps ()Lorg/gradle/api/provider/SetProperty; + public abstract fun getExecutor ()Lorg/gradle/workers/WorkerExecutor; public abstract fun getMergedApiFile ()Lorg/gradle/api/file/RegularFileProperty; } +public abstract class kotlinx/validation/WorkerAwareTaskBase : org/gradle/api/DefaultTask { + public fun ()V + public abstract fun getRuntimeClasspath ()Lorg/gradle/api/file/ConfigurableFileCollection; +} + public final class kotlinx/validation/_UtilsKt { public static final fun toKlibTarget (Lorg/jetbrains/kotlin/gradle/plugin/KotlinTarget;)Lkotlinx/validation/api/klib/KlibTarget; } diff --git a/build.gradle.kts b/build.gradle.kts index e75f2758..f8b1dbbd 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -64,10 +64,10 @@ val createClasspathManifest = tasks.register("createClasspathManifest") { dependencies { implementation(gradleApi()) - implementation(libs.kotlinx.metadata) + compileOnly(libs.kotlinx.metadata) compileOnly(libs.kotlin.compiler.embeddable) - implementation(libs.ow2.asm) - implementation(libs.ow2.asmTree) + compileOnly(libs.ow2.asm) + compileOnly(libs.ow2.asmTree) implementation(libs.javaDiffUtils) compileOnly(libs.gradlePlugin.kotlin) @@ -168,12 +168,17 @@ testing { implementation(project()) implementation(libs.assertJ.core) implementation(libs.kotlin.test) - implementation(libs.kotlin.compiler.embeddable) } } val test by getting(JvmTestSuite::class) { description = "Regular unit tests" + dependencies { + implementation(libs.kotlinx.metadata) + implementation(libs.kotlin.compiler.embeddable) + implementation(libs.ow2.asm) + implementation(libs.ow2.asmTree) + } } val functionalTest by creating(JvmTestSuite::class) { @@ -183,6 +188,8 @@ testing { dependencies { implementation(files(createClasspathManifest)) + implementation(libs.kotlinx.metadata) + implementation(libs.kotlin.compiler.embeddable) implementation(gradleApi()) implementation(gradleTestKit()) } diff --git a/src/functionalTest/kotlin/kotlinx/validation/api/TestDsl.kt b/src/functionalTest/kotlin/kotlinx/validation/api/TestDsl.kt index 43d96770..5bd29cc4 100644 --- a/src/functionalTest/kotlin/kotlinx/validation/api/TestDsl.kt +++ b/src/functionalTest/kotlin/kotlinx/validation/api/TestDsl.kt @@ -190,6 +190,7 @@ internal class AppendableScope(val filePath: String) { internal class Runner(withConfigurationCache: Boolean = true) { val arguments: MutableList = mutableListOf().apply { + add("--stacktrace") if (!koverEnabled && withConfigurationCache) { // Configuration cache is incompatible with javaagents being enabled for Gradle // See https://github.com/gradle/gradle/issues/25979 diff --git a/src/functionalTest/kotlin/kotlinx/validation/test/DefaultConfigTests.kt b/src/functionalTest/kotlin/kotlinx/validation/test/DefaultConfigTests.kt index 1b68f12d..b9041f62 100644 --- a/src/functionalTest/kotlin/kotlinx/validation/test/DefaultConfigTests.kt +++ b/src/functionalTest/kotlin/kotlinx/validation/test/DefaultConfigTests.kt @@ -97,6 +97,29 @@ internal class DefaultConfigTests : BaseKotlinGradleTest() { } } + @Test + fun `apiCheck should succeed when public classes match api file with K2`() { + val runner = test { + buildGradleKts { + resolve("/examples/gradle/base/withPluginK2.gradle.kts") + } + kotlin("AnotherBuildConfig.kt") { + resolve("/examples/classes/AnotherBuildConfig.kt") + } + apiFile(projectName = rootProjectDir.name) { + resolve("/examples/classes/AnotherBuildConfig.dump") + } + + runner { + arguments.add(":apiCheck") + } + } + + runner.build().apply { + assertTaskSuccess(":apiCheck") + } + } + @Test fun `apiCheck should fail when public classes match api file ignoring case`() { Assume.assumeTrue(underlyingFsIsCaseSensitive()) diff --git a/src/functionalTest/kotlin/kotlinx/validation/test/KlibVerificationTests.kt b/src/functionalTest/kotlin/kotlinx/validation/test/KlibVerificationTests.kt index 64e1ebb8..b47fe4e4 100644 --- a/src/functionalTest/kotlin/kotlinx/validation/test/KlibVerificationTests.kt +++ b/src/functionalTest/kotlin/kotlinx/validation/test/KlibVerificationTests.kt @@ -13,8 +13,6 @@ import org.assertj.core.api.Assertions import org.gradle.testkit.runner.BuildResult import org.jetbrains.kotlin.konan.target.HostManager import org.jetbrains.kotlin.konan.target.KonanTarget -import org.jetbrains.kotlin.utils.addToStdlib.butIf -import org.junit.Assert import org.junit.Assume import org.junit.Test import java.io.File @@ -91,6 +89,22 @@ internal class KlibVerificationTests : BaseKotlinGradleTest() { checkKlibDump(runner.build(), "/examples/classes/TopLevelDeclarations.klib.with.linux.dump") } + @Test + fun `apiDump for native targets in K2`() { + val runner = test { + settingsGradleKts { + resolve("/examples/gradle/settings/settings-name-testproject.gradle.kts") + } + buildGradleKts { + resolve("/examples/gradle/base/withNativePluginK2.gradle.kts") + } + 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 { diff --git a/src/functionalTest/resources/examples/gradle/base/withNativePluginK2.gradle.kts b/src/functionalTest/resources/examples/gradle/base/withNativePluginK2.gradle.kts new file mode 100644 index 00000000..8ec43d3c --- /dev/null +++ b/src/functionalTest/resources/examples/gradle/base/withNativePluginK2.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 "2.0.0" + id("org.jetbrains.kotlinx.binary-compatibility-validator") +} + +repositories { + mavenCentral() +} + +kotlin { + linuxX64() + linuxArm64() + mingwX64() + androidNativeArm32() + androidNativeArm64() + androidNativeX64() + androidNativeX86() + + sourceSets { + val commonMain by getting + val commonTest by getting { + dependencies { + implementation(kotlin("stdlib")) + implementation(kotlin("test-common")) + implementation(kotlin("test-annotations-common")) + } + } + } +} + +apiValidation { + klib.enabled = true +} diff --git a/src/functionalTest/resources/examples/gradle/base/withPluginK2.gradle.kts b/src/functionalTest/resources/examples/gradle/base/withPluginK2.gradle.kts new file mode 100644 index 00000000..3fbc72e2 --- /dev/null +++ b/src/functionalTest/resources/examples/gradle/base/withPluginK2.gradle.kts @@ -0,0 +1,17 @@ +/* + * 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. + */ + +plugins { + kotlin("jvm") version "2.0.0" + id("org.jetbrains.kotlinx.binary-compatibility-validator") +} + +repositories { + mavenCentral() +} + +dependencies { + implementation(kotlin("stdlib-jdk8")) +} diff --git a/src/main/kotlin/-Utils.kt b/src/main/kotlin/-Utils.kt index 98ee94e8..030c40fb 100644 --- a/src/main/kotlin/-Utils.kt +++ b/src/main/kotlin/-Utils.kt @@ -14,6 +14,7 @@ import org.jetbrains.kotlin.gradle.plugin.KotlinTarget import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget import org.jetbrains.kotlin.gradle.targets.js.KotlinWasmTargetType import org.jetbrains.kotlin.gradle.targets.js.ir.KotlinJsIrTarget +import java.io.File import java.io.Serializable /** @@ -60,3 +61,9 @@ public class KlibDumpMetadata( @get:PathSensitive(PathSensitivity.RELATIVE) public val dumpFile: RegularFileProperty ) : Serializable + +// Workaround for serialization exception occurring when KlibDumpMetadata is supplied to WorkerParameters. +internal class KlibMetadataLocal( + val target: KlibTarget, + val dumpFile: File +) : Serializable diff --git a/src/main/kotlin/BinaryCompatibilityValidatorPlugin.kt b/src/main/kotlin/BinaryCompatibilityValidatorPlugin.kt index 3930a340..97d76475 100644 --- a/src/main/kotlin/BinaryCompatibilityValidatorPlugin.kt +++ b/src/main/kotlin/BinaryCompatibilityValidatorPlugin.kt @@ -7,6 +7,7 @@ package kotlinx.validation import kotlinx.validation.api.klib.KlibTarget import org.gradle.api.* +import org.gradle.api.artifacts.Configuration import org.gradle.api.plugins.* import org.gradle.api.provider.* import org.gradle.api.tasks.* @@ -17,6 +18,7 @@ import org.jetbrains.kotlin.konan.target.HostManager import org.jetbrains.kotlin.library.abi.ExperimentalLibraryAbiReader import org.jetbrains.kotlin.library.abi.LibraryAbiReader import java.io.* +import java.util.* @OptIn(ExperimentalBCVApi::class, ExperimentalLibraryAbiReader::class) public class BinaryCompatibilityValidatorPlugin : Plugin { @@ -51,9 +53,10 @@ public class BinaryCompatibilityValidatorPlugin : Plugin { } private fun configureProject(project: Project, extension: ApiValidationExtension) { - configureKotlinPlugin(project, extension) - configureAndroidPlugin(project, extension) - configureMultiplatformPlugin(project, extension) + val jvmRuntimeClasspath = project.prepareJvmValidationClasspath() + configureKotlinPlugin(project, extension, jvmRuntimeClasspath) + configureAndroidPlugin(project, extension, jvmRuntimeClasspath) + configureMultiplatformPlugin(project, extension, jvmRuntimeClasspath) } private fun configurePlugin( @@ -68,7 +71,8 @@ public class BinaryCompatibilityValidatorPlugin : Plugin { private fun configureMultiplatformPlugin( project: Project, - extension: ApiValidationExtension + extension: ApiValidationExtension, + jvmRuntimeClasspath: NamedDomainObjectProvider ) = configurePlugin("kotlin-multiplatform", project, extension) { if (project.name in extension.ignoredProjects) return@configurePlugin val kotlin = project.kotlinMultiplatform @@ -102,13 +106,16 @@ public class BinaryCompatibilityValidatorPlugin : Plugin { val targetConfig = TargetConfig(project, extension, target.name, jvmDirConfig) if (target.platformType == KotlinPlatformType.jvm) { target.mainCompilationOrNull?.also { - project.configureKotlinCompilation(it, extension, targetConfig, commonApiDump, commonApiCheck) + project.configureKotlinCompilation( + it, extension, jvmRuntimeClasspath, targetConfig, commonApiDump, commonApiCheck + ) } } else if (target.platformType == KotlinPlatformType.androidJvm) { target.compilations.matching { it.name == "release" }.all { project.configureKotlinCompilation( it, extension, + jvmRuntimeClasspath, targetConfig, commonApiDump, commonApiCheck, @@ -122,30 +129,32 @@ public class BinaryCompatibilityValidatorPlugin : Plugin { private fun configureAndroidPlugin( project: Project, - extension: ApiValidationExtension + extension: ApiValidationExtension, + jvmRuntimeClasspath: NamedDomainObjectProvider ) { - configureAndroidPluginForKotlinLibrary(project, extension) - + configureAndroidPluginForKotlinLibrary(project, extension, jvmRuntimeClasspath) } private fun configureAndroidPluginForKotlinLibrary( project: Project, - extension: ApiValidationExtension + extension: ApiValidationExtension, + jvmRuntimeClasspath: NamedDomainObjectProvider ) = configurePlugin("kotlin-android", project, extension) { val androidExtension = project.extensions .getByName("kotlin") as KotlinAndroidProjectExtension androidExtension.target.compilations.matching { it.compilationName == "release" }.all { - project.configureKotlinCompilation(it, extension, useOutput = true) + project.configureKotlinCompilation(it, extension, jvmRuntimeClasspath, useOutput = true) } } private fun configureKotlinPlugin( project: Project, - extension: ApiValidationExtension + extension: ApiValidationExtension, + jvmRuntimeClasspath: NamedDomainObjectProvider ) = configurePlugin("kotlin", project, extension) { - project.configureApiTasks(extension, TargetConfig(project, extension)) + project.configureApiTasks(extension, TargetConfig(project, extension), jvmRuntimeClasspath) } } @@ -203,6 +212,7 @@ private enum class DirConfig { private fun Project.configureKotlinCompilation( compilation: KotlinCompilation, extension: ApiValidationExtension, + jvmRuntimeClasspath: NamedDomainObjectProvider, targetConfig: TargetConfig = TargetConfig(this, extension), commonApiDump: TaskProvider? = null, commonApiCheck: TaskProvider? = null, @@ -226,6 +236,7 @@ private fun Project.configureKotlinCompilation( inputDependencies.from(compilation.compileDependencyFiles) } outputApiFile.fileProvider(apiBuildDir.map { it.resolve(dumpFileName) }) + runtimeClasspath.from(jvmRuntimeClasspath) } configureCheckTasks(apiBuild, extension, targetConfig, commonApiDump, commonApiCheck) } @@ -249,6 +260,7 @@ private fun klibAbiCheckEnabled(projectName: String, extension: ApiValidationExt private fun Project.configureApiTasks( extension: ApiValidationExtension, targetConfig: TargetConfig = TargetConfig(this, extension), + jvmRuntimeClasspath: NamedDomainObjectProvider, ) { val projectName = project.name val dumpFileName = project.jvmDumpFileName @@ -266,6 +278,7 @@ private fun Project.configureApiTasks( "Builds Kotlin API for 'main' compilations of $projectName. Complementary task and shouldn't be called manually" inputClassesDirs.from(sourceSetsOutputsProvider) outputApiFile.fileProvider(apiBuildDir.map { it.resolve(dumpFileName) }) + runtimeClasspath.from(jvmRuntimeClasspath) } configureCheckTasks(apiBuild, extension, targetConfig) @@ -371,10 +384,13 @@ private class KlibValidationPipelineBuilder( val klibMergeInferredDir = projectBuildDir.flatMap { pd -> klibInferDumpConfig.apiDir.map { pd.resolve(it) } } val klibExtractedFileDir = klibMergeInferredDir.map { it.resolve("extracted") } - val klibMerge = project.mergeKlibsUmbrellaTask(klibDumpConfig, klibMergeDir) - val klibMergeInferred = project.mergeInferredKlibsUmbrellaTask(klibDumpConfig, klibMergeInferredDir) + val runtimeClasspath = project.prepareKlibValidationClasspath() + + val klibMerge = project.mergeKlibsUmbrellaTask(klibDumpConfig, klibMergeDir, runtimeClasspath) + val klibMergeInferred = project.mergeInferredKlibsUmbrellaTask(klibDumpConfig, klibMergeInferredDir, runtimeClasspath) val klibDump = project.dumpKlibsTask(klibDumpConfig) - val klibExtractAbiForSupportedTargets = project.extractAbi(klibDumpConfig, klibApiDir, klibExtractedFileDir) + val klibExtractAbiForSupportedTargets = + project.extractAbi(klibDumpConfig, klibApiDir, klibExtractedFileDir, runtimeClasspath) val klibCheck = project.checkKlibsTask(klibDumpConfig) klibDump.configure { it.from.set(klibMergeInferred.flatMap { it.mergedApiFile }) @@ -387,7 +403,7 @@ private class KlibValidationPipelineBuilder( } commonApiDump.configure { it.dependsOn(klibDump) } commonApiCheck.configure { it.dependsOn(klibCheck) } - project.configureTargets(klibApiDir, klibMerge, klibMergeInferred) + project.configureTargets(klibApiDir, klibMerge, klibMergeInferred, runtimeClasspath) } private fun Project.checkKlibsTask(klibDumpConfig: TargetConfig) = @@ -412,7 +428,8 @@ private class KlibValidationPipelineBuilder( private fun Project.extractAbi( klibDumpConfig: TargetConfig, klibApiDir: Provider, - klibOutputDir: Provider + klibOutputDir: Provider, + runtimeClasspath: NamedDomainObjectProvider ) = project.task( klibDumpConfig.apiTaskName("ExtractForValidation") ) @@ -425,11 +442,13 @@ private class KlibValidationPipelineBuilder( targetsToRemove.addAll(unsupportedTargets()) inputAbiFile.fileProvider(klibApiDir.map { it.resolve(klibDumpFileName) }) outputAbiFile.fileProvider(klibOutputDir.map { it.resolve(klibDumpFileName) }) + this.runtimeClasspath.from(runtimeClasspath) } private fun Project.mergeInferredKlibsUmbrellaTask( klibDumpConfig: TargetConfig, klibMergeDir: Provider, + runtimeClasspath: NamedDomainObjectProvider ) = project.task( klibDumpConfig.apiTaskName("MergeInferred") ) @@ -439,16 +458,19 @@ private class KlibValidationPipelineBuilder( "different targets (including inferred dumps for unsupported targets) " + "into a single merged KLib ABI dump" mergedApiFile.fileProvider(klibMergeDir.map { it.resolve(klibDumpFileName) }) + this.runtimeClasspath.from(runtimeClasspath) } private fun Project.mergeKlibsUmbrellaTask( klibDumpConfig: TargetConfig, - klibMergeDir: Provider + klibMergeDir: Provider, + runtimeClasspath: NamedDomainObjectProvider ) = 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" mergedApiFile.fileProvider(klibMergeDir.map { it.resolve(klibDumpFileName) }) + this.runtimeClasspath.from(runtimeClasspath) } fun Project.bannedTargets(): Set { @@ -467,7 +489,8 @@ private class KlibValidationPipelineBuilder( fun Project.configureTargets( klibApiDir: Provider, mergeTask: TaskProvider, - mergeInferredTask: TaskProvider + mergeInferredTask: TaskProvider, + runtimeClasspath: NamedDomainObjectProvider ) { val kotlin = project.kotlinMultiplatform @@ -493,8 +516,12 @@ private class KlibValidationPipelineBuilder( // If a target is supported, the workflow is simple: create a dump, then merge it along with other dumps. if (targetSupported) { val buildTargetAbi = configureKlibCompilation( - mainCompilation, extension, targetConfig, - target, apiBuildDir + mainCompilation, + extension, + targetConfig, + target, + apiBuildDir, + runtimeClasspath ) generatedDumps.add( KlibDumpMetadata(target, @@ -557,7 +584,8 @@ private class KlibValidationPipelineBuilder( extension: ApiValidationExtension, targetConfig: TargetConfig, target: KlibTarget, - apiBuildDir: Provider + apiBuildDir: Provider, + runtimeClasspath: NamedDomainObjectProvider ): TaskProvider { val projectName = project.name val buildTask = project.task(targetConfig.apiTaskName("Build")) { @@ -569,6 +597,7 @@ private class KlibValidationPipelineBuilder( klibFile.from(compilation.output.classesDirs) signatureVersion.set(extension.klib.signatureVersion) outputAbiFile.fileProvider(apiBuildDir.map { it.resolve(klibDumpFileName) }) + this.runtimeClasspath.from(runtimeClasspath) } return buildTask } @@ -629,3 +658,113 @@ private val Project.jvmDumpFileName: String get() = "$name.api" private val Project.klibDumpFileName: String get() = "$name.klib.api" + +private fun Project.prepareKlibValidationClasspath(): NamedDomainObjectProvider { + val compilerVersion = project.objects.property(String::class.java).convention("2.0.0") + project.withKotlinPluginVersion { version -> + compilerVersion.set(version) + } + + val dependencyConfiguration = + project.configurations.create("bcv-rt-klib-cp") { + it.description = "Runtime classpath for running binary-compatibility-validator." + it.isCanBeResolved = false + it.isCanBeConsumed = false + it.isCanBeDeclared = true + it.isVisible = false + } + + project.dependencies.addProvider(dependencyConfiguration.name, compilerVersion.map { version -> "org.jetbrains.kotlin:kotlin-compiler-embeddable:$version" }) + + return project.configurations.register("bcv-rt-klib-cp-resolver") { + it.description = "Resolve the runtime classpath for running binary-compatibility-validator." + it.isCanBeResolved = true + it.isCanBeConsumed = false + it.isCanBeDeclared = false + it.isVisible = false + it.extendsFrom(dependencyConfiguration) + } +} + +private fun Project.prepareJvmValidationClasspath(): NamedDomainObjectProvider { + val metadataDependencyVersion = project.objects.property(String::class.java).convention("2.0.0") + project.withKotlinPluginVersion { version -> + if (version != null && !version.startsWith("1.")) { + metadataDependencyVersion.set(version) + } + } + + + val dependencyConfiguration = + project.configurations.create("bcv-rt-jvm-cp") { + it.description = "Runtime classpath for running binary-compatibility-validator." + it.isCanBeResolved = false + it.isCanBeConsumed = false + it.isCanBeDeclared = true + it.isVisible = false + } + + project.dependencies.add(dependencyConfiguration.name, "org.ow2.asm:asm:9.6") + project.dependencies.add(dependencyConfiguration.name, "org.ow2.asm:asm-tree:9.6") + project.dependencies.addProvider(dependencyConfiguration.name, metadataDependencyVersion.map { version -> "org.jetbrains.kotlin:kotlin-metadata-jvm:$version" }) + + return project.configurations.register("bcv-rt-jvm-cp-resolver") { + it.description = "Resolve the runtime classpath for running binary-compatibility-validator." + it.isCanBeResolved = true + it.isCanBeConsumed = false + it.isCanBeDeclared = false + it.isVisible = false + it.extendsFrom(dependencyConfiguration) + } +} + +private fun Project.withKotlinPluginVersion(block: (String?) -> Unit) { + // execute block if any of a Kotlin plugin applied + plugins.withId("org.jetbrains.kotlin.jvm") { + block(readVersion()) + } + plugins.withId("org.jetbrains.kotlin.multiplatform") { + block(readVersion()) + } + plugins.withId("org.jetbrains.kotlin.android") { + block(readVersion()) + } +} + +/** + * Explicitly reading the version from the resources because the BCV and the Kotlin plugins may be located in different class loaders, + * and we cannot invoke functions from KGP directly. + */ +private fun Project.readVersion(): String? { + val extension = extensions.findByName("kotlin") + if (extension == null) { + logger.warn("Binary compatibility plugin could not find Kotlin extension") + return null + } + + val inputStream = + extension::class.java.classLoader.getResourceAsStream("project.properties") + + if (inputStream == null) { + logger.warn("Binary compatibility plugin could not find resource 'project.properties'") + return null + } + + val properties = Properties() + try { + inputStream.use { + properties.load(it) + } + } catch (e: Exception) { + logger.warn("Binary compatibility plugin could not read resource 'project.properties'") + return null + } + + val version = properties.getProperty("project.version") + if (version == null) { + logger.warn("Binary compatibility plugin could not find property 'project.version' in resource 'project.properties'") + return null + } + + return version +} diff --git a/src/main/kotlin/BuildTaskBase.kt b/src/main/kotlin/BuildTaskBase.kt index e4dd551c..8e93bd61 100644 --- a/src/main/kotlin/BuildTaskBase.kt +++ b/src/main/kotlin/BuildTaskBase.kt @@ -5,14 +5,19 @@ package kotlinx.validation -import org.gradle.api.DefaultTask import org.gradle.api.provider.SetProperty import org.gradle.api.tasks.Input import org.gradle.api.tasks.Internal +import org.gradle.workers.WorkParameters +import org.gradle.workers.WorkerExecutor +import javax.inject.Inject -public abstract class BuildTaskBase : DefaultTask() { +public abstract class BuildTaskBase : WorkerAwareTaskBase() { private val extension = project.apiValidationExtensionOrNull + @get:Inject + internal abstract val executor: WorkerExecutor + private fun stringSetProperty(provider: ApiValidationExtension.() -> Set): SetProperty { return project.objects.setProperty(String::class.java).convention( project.provider { @@ -45,4 +50,22 @@ public abstract class BuildTaskBase : DefaultTask() { @get:Internal internal val projectName = project.name + + internal fun fillCommonParams(params: BuildParametersBase) { + params.ignoredPackages.set(ignoredPackages) + params.nonPublicMarkers.set(nonPublicMarkers) + params.ignoredClasses.set(ignoredClasses) + params.publicPackages.set(publicPackages) + params.publicMarkers.set(publicMarkers) + params.publicClasses.set(publicClasses) + } +} + +internal interface BuildParametersBase : WorkParameters { + val ignoredPackages: SetProperty + val nonPublicMarkers: SetProperty + val ignoredClasses: SetProperty + val publicPackages: SetProperty + val publicMarkers: SetProperty + val publicClasses: SetProperty } diff --git a/src/main/kotlin/KotlinApiBuildTask.kt b/src/main/kotlin/KotlinApiBuildTask.kt index 9cdf97c1..87a780fb 100644 --- a/src/main/kotlin/KotlinApiBuildTask.kt +++ b/src/main/kotlin/KotlinApiBuildTask.kt @@ -9,12 +9,16 @@ import kotlinx.validation.api.* import org.gradle.api.* import org.gradle.api.file.* import org.gradle.api.tasks.* +import org.gradle.workers.WorkAction +import org.gradle.workers.WorkerExecutor import java.io.File import java.util.jar.JarFile import javax.inject.Inject -private const val MIGRATION_GUIDE_LINK = "https://github.com/Kotlin/binary-compatibility-validator/blob/master/docs/design/0.15.0-migration-guide.md" -private const val OUTPUT_API_DIR_ERROR = "Property outputApiDir was replaced with outputApiFile. Please refer to the migration guide for migration details: $MIGRATION_GUIDE_LINK" +private const val MIGRATION_GUIDE_LINK = + "https://github.com/Kotlin/binary-compatibility-validator/blob/master/docs/design/0.15.0-migration-guide.md" +private const val OUTPUT_API_DIR_ERROR = + "Property outputApiDir was replaced with outputApiFile. Please refer to the migration guide for migration details: $MIGRATION_GUIDE_LINK" public abstract class KotlinApiBuildTask @Inject constructor( ) : BuildTaskBase() { @@ -42,13 +46,35 @@ public abstract class KotlinApiBuildTask @Inject constructor( @get:PathSensitive(PathSensitivity.RELATIVE) public abstract val inputDependencies: ConfigurableFileCollection + @TaskAction internal fun generate() { - val inputClassesDirs = inputClassesDirs + val workQueue = executor.classLoaderIsolation { + it.classpath.from(runtimeClasspath) + } + workQueue.submit(AbiBuildWorker::class.java) { params -> + fillCommonParams(params) + + params.inputJar.set(inputJar) + params.inputClassesDirs.from(inputClassesDirs) + params.outputApiFile.set(outputApiFile) + } + } +} + +internal interface ApiBuildParameters : BuildParametersBase { + val outputApiFile: RegularFileProperty + val inputClassesDirs: ConfigurableFileCollection + val inputJar: RegularFileProperty +} + +internal abstract class AbiBuildWorker : WorkAction { + override fun execute() { + val inputClassesDirs = parameters.inputClassesDirs val signatures = when { // inputJar takes precedence if specified - inputJar.isPresent -> - JarFile(inputJar.get().asFile).use { it.loadApiFromJvmClasses() } + parameters.inputJar.isPresent -> + JarFile(parameters.inputJar.get().asFile).use { it.loadApiFromJvmClasses() } inputClassesDirs.any() -> inputClassesDirs.asFileTree.asSequence() @@ -62,21 +88,26 @@ public abstract class KotlinApiBuildTask @Inject constructor( throw GradleException("KotlinApiBuildTask should have either inputClassesDirs, or inputJar property set") } - val publicPackagesNames = signatures.extractAnnotatedPackages(publicMarkers.get().map(::replaceDots).toSet()) + val publicMarkers = parameters.publicMarkers.get() + val publicClasses = parameters.publicClasses.get() + val publicPackages = parameters.publicPackages.get() + val nonPublicMarkers = parameters.nonPublicMarkers.get() + val ignoredClasses = parameters.ignoredClasses.get() + val ignoredPackages = parameters.ignoredPackages.get() + + val publicPackagesNames = signatures.extractAnnotatedPackages(publicMarkers.map(::replaceDots).toSet()) val ignoredPackagesNames = - signatures.extractAnnotatedPackages(nonPublicMarkers.get().map(::replaceDots).toSet()) + signatures.extractAnnotatedPackages(nonPublicMarkers.map(::replaceDots).toSet()) val filteredSignatures = signatures .retainExplicitlyIncludedIfDeclared( - publicPackages.get() + publicPackagesNames, - publicClasses.get(), publicMarkers.get() + publicPackages + publicPackagesNames, publicClasses, publicMarkers ) - .filterOutNonPublic(ignoredPackages.get() + ignoredPackagesNames, ignoredClasses.get()) - .filterOutAnnotated(nonPublicMarkers.get().map(::replaceDots).toSet()) + .filterOutNonPublic(ignoredPackages + ignoredPackagesNames, ignoredClasses) + .filterOutAnnotated(nonPublicMarkers.map(::replaceDots).toSet()) - outputApiFile.asFile.get().bufferedWriter().use { writer -> + parameters.outputApiFile.asFile.get().bufferedWriter().use { writer -> filteredSignatures.dump(writer) } } } - diff --git a/src/main/kotlin/KotlinKlibAbiBuildTask.kt b/src/main/kotlin/KotlinKlibAbiBuildTask.kt index f5fbe7c8..7c0861f9 100644 --- a/src/main/kotlin/KotlinKlibAbiBuildTask.kt +++ b/src/main/kotlin/KotlinKlibAbiBuildTask.kt @@ -10,6 +10,7 @@ import org.gradle.api.file.ConfigurableFileCollection import org.gradle.api.file.RegularFileProperty import org.gradle.api.provider.Property import org.gradle.api.tasks.* +import org.gradle.workers.WorkAction /** * Generates a text file with a KLib ABI dump for a single klib. @@ -51,19 +52,43 @@ public abstract class KotlinKlibAbiBuildTask : BuildTaskBase() { @get:OutputFile public abstract val outputAbiFile: RegularFileProperty - @OptIn(ExperimentalBCVApi::class) @TaskAction internal fun generate() { - val outputFile = outputAbiFile.asFile.get() + val workQueue = executor.classLoaderIsolation { + it.classpath.from(runtimeClasspath) + } + workQueue.submit(KlibAbiBuildWorker::class.java) { params -> + fillCommonParams(params) + + params.klibFile.from(klibFile) + params.target.set(target) + params.signatureVersion.set(signatureVersion) + params.outputAbiFile.set(outputAbiFile) + } + } +} + +internal interface KlibAbiBuildParameters : BuildParametersBase { + val klibFile: ConfigurableFileCollection + val signatureVersion: Property + val target: Property + val outputAbiFile: RegularFileProperty +} + +internal abstract class KlibAbiBuildWorker : WorkAction { + @OptIn(ExperimentalBCVApi::class) + override fun execute() { + val outputFile = parameters.outputAbiFile.asFile.get() outputFile.delete() outputFile.parentFile.mkdirs() - val dump = KlibDump.fromKlib(klibFile.singleFile, target.get().configurableName, KlibDumpFilters { - ignoredClasses.addAll(this@KotlinKlibAbiBuildTask.ignoredClasses.get()) - ignoredPackages.addAll(this@KotlinKlibAbiBuildTask.ignoredPackages.get()) - nonPublicMarkers.addAll(this@KotlinKlibAbiBuildTask.nonPublicMarkers.get()) - signatureVersion = this@KotlinKlibAbiBuildTask.signatureVersion.get() - }) + val dump = KlibDump.fromKlib(parameters.klibFile.singleFile, parameters.target.get().configurableName, + KlibDumpFilters { + ignoredClasses.addAll(parameters.ignoredClasses.get()) + ignoredPackages.addAll(parameters.ignoredPackages.get()) + nonPublicMarkers.addAll(parameters.nonPublicMarkers.get()) + signatureVersion = parameters.signatureVersion.get() + }) dump.saveTo(outputFile) } diff --git a/src/main/kotlin/KotlinKlibExtractAbiTask.kt b/src/main/kotlin/KotlinKlibExtractAbiTask.kt index 18920da4..b6391748 100644 --- a/src/main/kotlin/KotlinKlibExtractAbiTask.kt +++ b/src/main/kotlin/KotlinKlibExtractAbiTask.kt @@ -8,13 +8,17 @@ package kotlinx.validation import kotlinx.validation.api.klib.KlibDump import kotlinx.validation.api.klib.KlibTarget import kotlinx.validation.api.klib.saveTo -import org.gradle.api.DefaultTask +import org.gradle.api.file.DirectoryProperty import org.gradle.api.file.RegularFileProperty import org.gradle.api.provider.Property import org.gradle.api.provider.SetProperty import org.gradle.api.tasks.* +import org.gradle.workers.WorkAction +import org.gradle.workers.WorkParameters +import org.gradle.workers.WorkerExecutor import java.nio.file.Files import java.nio.file.StandardCopyOption +import javax.inject.Inject /** * Extracts dump for targets supported by the host compiler from a merged API dump stored in a project. @@ -23,7 +27,7 @@ import java.nio.file.StandardCopyOption * only supported tasks could be extracted for further validation. */ @CacheableTask -public abstract class KotlinKlibExtractAbiTask : DefaultTask() { +public abstract class KotlinKlibExtractAbiTask : WorkerAwareTaskBase() { /** * Merged KLib dump that should be filtered by this task. */ @@ -49,31 +53,59 @@ public abstract class KotlinKlibExtractAbiTask : DefaultTask() { @get:OutputFile public abstract val outputAbiFile: RegularFileProperty + @get:Inject + public abstract val executor: WorkerExecutor + private val rootDir = project.rootDir - @OptIn(ExperimentalBCVApi::class) @TaskAction internal fun generate() { - val inputFile = inputAbiFile.asFile.get() + val q = executor.classLoaderIsolation { + it.classpath.from(runtimeClasspath) + } + q.submit(KlibExtractAbiWorker::class.java) { params -> + params.inputAbiFile.set(inputAbiFile) + params.targetsToRemove.set(targetsToRemove) + params.strictValidation.set(strictValidation) + params.outputAbiFile.set(outputAbiFile) + params.rootDir.set(rootDir) + } + q.await() + } +} + +internal interface KlibExtractAbiParameters : WorkParameters { + val inputAbiFile: RegularFileProperty + val targetsToRemove: SetProperty + val strictValidation: Property + val outputAbiFile: RegularFileProperty + val rootDir: DirectoryProperty +} + +internal abstract class KlibExtractAbiWorker : WorkAction { + @OptIn(ExperimentalBCVApi::class) + override fun execute() { + val inputFile = parameters.inputAbiFile.asFile.get() + val rootDir = parameters.rootDir.asFile.get() if (!inputFile.exists()) { error("File with project's API declarations '${inputFile.relativeTo(rootDir)}' does not exist.\n" + "Please ensure that ':apiDump' was executed in order to get API dump to compare the build against") } if (inputFile.length() == 0L) { - Files.copy(inputFile.toPath(), outputAbiFile.asFile.get().toPath(), StandardCopyOption.REPLACE_EXISTING) + Files.copy(inputFile.toPath(), parameters.outputAbiFile.asFile.get().toPath(), StandardCopyOption.REPLACE_EXISTING) return } val dump = KlibDump.from(inputFile) - val unsupportedTargets = targetsToRemove.get().map(KlibTarget::targetName).toSet() + val unsupportedTargets = parameters.targetsToRemove.get().map(KlibTarget::targetName).toSet() // Filter out only unsupported files. // That ensures that target renaming will be caught and reported as a change. - if (unsupportedTargets.isNotEmpty() && strictValidation.get()) { + if (unsupportedTargets.isNotEmpty() && parameters.strictValidation.get()) { throw IllegalStateException( - "Validation could not be performed as some targets (namely, $targetsToRemove) are not available " + - "and the strictValidation mode was enabled." + "Validation could not be performed as some targets (namely, ${parameters.targetsToRemove}) " + + "are not available and the strictValidation mode was enabled." ) } dump.remove(unsupportedTargets.map(KlibTarget::parse)) - dump.saveTo(outputAbiFile.asFile.get()) + dump.saveTo(parameters.outputAbiFile.asFile.get()) } } diff --git a/src/main/kotlin/KotlinKlibInferAbiTask.kt b/src/main/kotlin/KotlinKlibInferAbiTask.kt index 889b33ec..5494cb21 100644 --- a/src/main/kotlin/KotlinKlibInferAbiTask.kt +++ b/src/main/kotlin/KotlinKlibInferAbiTask.kt @@ -12,6 +12,11 @@ import org.gradle.api.file.RegularFileProperty import org.gradle.api.provider.Property import org.gradle.api.provider.SetProperty import org.gradle.api.tasks.* +import org.gradle.workers.WorkAction +import org.gradle.workers.WorkParameters +import org.gradle.workers.WorkerExecutor +import org.slf4j.LoggerFactory +import javax.inject.Inject /** * Task infers a possible KLib ABI dump for an unsupported target. @@ -24,7 +29,7 @@ import org.gradle.api.tasks.* * The resulting dump is then used as an inferred dump for the unsupported target. */ @CacheableTask -public abstract class KotlinKlibInferAbiTask : DefaultTask() { +public abstract class KotlinKlibInferAbiTask : WorkerAwareTaskBase() { /** * The name of a target to infer a dump for. */ @@ -50,15 +55,44 @@ public abstract class KotlinKlibInferAbiTask : DefaultTask() { @get:OutputFile public abstract val outputAbiFile: RegularFileProperty - @OptIn(ExperimentalBCVApi::class) + @get:Inject + public abstract val executor: WorkerExecutor + @TaskAction internal fun generate() { - val availableDumps = inputDumps.get().map { - it.target to it.dumpFile.asFile.get() + val q = executor.classLoaderIsolation { + it.classpath.from(runtimeClasspath) + } + q.submit(KlibInferAbiWorker::class.java) { params -> + params.target.set(target) + params.inputDumps.set(inputDumps.get().map { + KlibMetadataLocal(it.target, it.dumpFile.get().asFile) + }) + params.oldMergedKlibDump.set(oldMergedKlibDump) + params.outputAbiFile.set(outputAbiFile) + } + q.await() + } +} + +internal interface KlibInferAbiParameters : WorkParameters { + val target: Property + val inputDumps: SetProperty + val oldMergedKlibDump: RegularFileProperty + val outputAbiFile: RegularFileProperty +} + +internal abstract class KlibInferAbiWorker : WorkAction { + private val logger = LoggerFactory.getLogger(KlibInferAbiWorker::class.java) + + @OptIn(ExperimentalBCVApi::class) + override fun execute() { + val availableDumps = parameters.inputDumps.get().map { + it.target to it.dumpFile }.filter { it.second.exists() }.toMap() // Find a set of supported targets that are closer to unsupported target in the hierarchy. // Note that dumps are stored using configurable name, but grouped by the canonical target name. - val matchingTargets = findMatchingTargets(availableDumps.keys, target.get()) + val matchingTargets = findMatchingTargets(availableDumps.keys, parameters.target.get()) // Load dumps that are a good fit for inference val supportedTargetDumps = matchingTargets.map { target -> val dumpFile = availableDumps[target]!! @@ -69,7 +103,7 @@ public abstract class KotlinKlibInferAbiTask : DefaultTask() { // Load an old dump, if any var image: KlibDump? = null - val oldDumpFile = oldMergedKlibDump.asFile.get() + val oldDumpFile = parameters.oldMergedKlibDump.asFile.get() if (oldDumpFile.exists()) { if (oldDumpFile.length() > 0L) { image = KlibDump.from(oldDumpFile) @@ -77,18 +111,18 @@ public abstract class KotlinKlibInferAbiTask : DefaultTask() { logger.warn( "Project's ABI file exists, but empty: $oldDumpFile. " + "The file will be ignored during ABI dump inference for the unsupported target " + - target.get() + parameters.target.get() ) } } - inferAbi(target.get(), supportedTargetDumps, image).saveTo(outputAbiFile.asFile.get()) + inferAbi(parameters.target.get(), supportedTargetDumps, image).saveTo(parameters.outputAbiFile.asFile.get()) logger.warn( - "An ABI dump for target ${target.get()} was inferred from the ABI generated for the following targets " + + "An ABI dump for target ${parameters.target.get()} was inferred from the ABI generated for the following targets " + "as the former target is not supported by the host compiler: " + "[${matchingTargets.joinToString(",")}]. " + - "Inferred dump may not reflect an actual ABI for the target ${target.get()}. " + + "Inferred dump may not reflect an actual ABI for the target ${parameters.target.get()}. " + "It is recommended to regenerate the dump on the host supporting all required compilation target." ) } diff --git a/src/main/kotlin/KotlinKlibMergeAbiTask.kt b/src/main/kotlin/KotlinKlibMergeAbiTask.kt index 0a136338..2ac04504 100644 --- a/src/main/kotlin/KotlinKlibMergeAbiTask.kt +++ b/src/main/kotlin/KotlinKlibMergeAbiTask.kt @@ -6,17 +6,23 @@ package kotlinx.validation import kotlinx.validation.api.klib.KlibDump +import kotlinx.validation.api.klib.KlibTarget import kotlinx.validation.api.klib.saveTo -import org.gradle.api.DefaultTask import org.gradle.api.file.RegularFileProperty import org.gradle.api.provider.SetProperty import org.gradle.api.tasks.* +import org.gradle.workers.WorkAction +import org.gradle.workers.WorkParameters +import org.gradle.workers.WorkerExecutor +import java.io.File +import java.io.Serializable +import javax.inject.Inject /** * Merges multiple individual KLib ABI dumps into a single merged dump. */ @CacheableTask -public abstract class KotlinKlibMergeAbiTask : DefaultTask() { +public abstract class KotlinKlibMergeAbiTask : WorkerAwareTaskBase() { /** * Dumps to merge. * @@ -31,16 +37,39 @@ public abstract class KotlinKlibMergeAbiTask : DefaultTask() { @get:OutputFile public abstract val mergedApiFile: RegularFileProperty - @OptIn(ExperimentalBCVApi::class) + @get:Inject + public abstract val executor: WorkerExecutor + @TaskAction internal fun merge() { + val q = executor.classLoaderIsolation { + it.classpath.from(runtimeClasspath) + } + q.submit(KlibMergeAbiWorker::class.java) { params -> + params.dumps.set(dumps.get().map { + KlibMetadataLocal(it.target, it.dumpFile.get().asFile) + }) + params.mergedApiFile.set(mergedApiFile) + } + q.await() + } +} + +internal interface KlibMergeAbiParameters : WorkParameters { + val dumps: SetProperty + val mergedApiFile: RegularFileProperty +} + +internal abstract class KlibMergeAbiWorker : WorkAction { + @OptIn(ExperimentalBCVApi::class) + override fun execute() { KlibDump().apply { - dumps.get().forEach { dump -> - val dumpFile = dump.dumpFile.asFile.get() + parameters.dumps.get().forEach { dump -> + val dumpFile = dump.dumpFile if (dumpFile.exists()) { merge(dumpFile, dump.target.configurableName) } } - }.saveTo(mergedApiFile.asFile.get()) + }.saveTo(parameters.mergedApiFile.asFile.get()) } } diff --git a/src/main/kotlin/WorkerAwareTaskBase.kt b/src/main/kotlin/WorkerAwareTaskBase.kt new file mode 100644 index 00000000..c34fe297 --- /dev/null +++ b/src/main/kotlin/WorkerAwareTaskBase.kt @@ -0,0 +1,15 @@ +/* + * 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.file.ConfigurableFileCollection +import org.gradle.api.tasks.Classpath + +public abstract class WorkerAwareTaskBase : DefaultTask() { + @get:Classpath + public abstract val runtimeClasspath: ConfigurableFileCollection +}