diff --git a/build.gradle.kts b/build.gradle.kts index 521f6147..352d0ebd 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -36,6 +36,34 @@ tasks.register("functionalTest") { } tasks.check { dependsOn(tasks["functionalTest"]) } +// While gradle testkit supports injection of the plugin classpath it doesn't allow using dependency notation +// to determine the actual runtime classpath for the plugin. It uses isolation, so plugins applied by the build +// script are not visible in the plugin classloader. This means optional dependencies (dependent on applied plugins - +// for example kotlin multiplatform) are not visible even if they are in regular gradle use. This hack will allow +// extending the classpath. It is based upon: https://docs.gradle.org/6.0/userguide/test_kit.html#sub:test-kit-classpath-injection + +// Create a configuration to register the dependencies against +val testPluginRuntimeConfiguration = configurations.register("testPluginRuntime") + +// The task that will create a file that stores the classpath needed for the plugin to have additional runtime dependencies +// This file is then used in to tell TestKit which classpath to use. +val createClasspathManifest = tasks.register("createClasspathManifest") { + val outputDir = buildDir.resolve("cpManifests") + inputs.files(testPluginRuntimeConfiguration) + .withPropertyName("runtimeClasspath") + .withNormalizer(ClasspathNormalizer::class) + + outputs.dir(outputDir) + .withPropertyName("outputDir") + + doLast { + outputDir.mkdirs() + file(outputDir.resolve("plugin-classpath.txt")).writeText(testPluginRuntimeConfiguration.get().joinToString("\n")) + } +} + +val kotlinVersion: String by project + dependencies { implementation(gradleApi()) implementation("org.jetbrains.kotlinx:kotlinx-metadata-jvm:0.3.0") @@ -43,7 +71,13 @@ dependencies { implementation("org.ow2.asm:asm-tree:9.0") implementation("com.googlecode.java-diff-utils:diffutils:1.3.0") compileOnly("org.jetbrains.kotlin.multiplatform:org.jetbrains.kotlin.multiplatform.gradle.plugin:1.3.61") + + // The test needs the full kotlin multiplatform plugin loaded as it has no visibility of previously loaded plugins, + // unlike the regular way gradle loads plugins. + add(testPluginRuntimeConfiguration.name, "org.jetbrains.kotlin.multiplatform:org.jetbrains.kotlin.multiplatform.gradle.plugin:$kotlinVersion") + testImplementation(kotlin("test-junit")) + "functionalTestImplementation"(files(createClasspathManifest)) "functionalTestImplementation"("org.assertj:assertj-core:3.18.1") "functionalTestImplementation"(gradleTestKit()) diff --git a/src/functionalTest/kotlin/kotlinx/validation/api/pluginTestRuntimeClasspath.kt b/src/functionalTest/kotlin/kotlinx/validation/api/pluginTestRuntimeClasspath.kt new file mode 100644 index 00000000..f11901e5 --- /dev/null +++ b/src/functionalTest/kotlin/kotlinx/validation/api/pluginTestRuntimeClasspath.kt @@ -0,0 +1,22 @@ +/* + * Copyright 2016-2021 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 + +import org.gradle.testkit.runner.GradleRunner +import java.io.File +import java.io.InputStreamReader + + +fun GradleRunner.addPluginTestRuntimeClasspath() = apply { + + val cpResource = javaClass.classLoader.getResourceAsStream("plugin-classpath.txt") + ?.let { InputStreamReader(it) } + ?: throw IllegalStateException("Could not find classpath resource") + + val pluginClasspath = pluginClasspath + cpResource.readLines().map { File(it) } + withPluginClasspath(pluginClasspath) + +} diff --git a/src/functionalTest/kotlin/kotlinx/validation/api/testDsl.kt b/src/functionalTest/kotlin/kotlinx/validation/api/testDsl.kt index 49121653..d3d7f31e 100644 --- a/src/functionalTest/kotlin/kotlinx/validation/api/testDsl.kt +++ b/src/functionalTest/kotlin/kotlinx/validation/api/testDsl.kt @@ -35,14 +35,14 @@ internal fun BaseKotlinGradleTest.test(fn: BaseKotlinScope.() -> Unit): GradleRu } /** - * same as [file][FileContainer.file], but prepends "src/main/java" before given `classFileName` + * same as [file][FileContainer.file], but prepends "src/${sourceSet}/kotlin" before given `classFileName` */ -internal fun FileContainer.kotlin(classFileName: String, fn: AppendableScope.() -> Unit) { +internal fun FileContainer.kotlin(classFileName: String, sourceSet:String = "main", fn: AppendableScope.() -> Unit) { require(classFileName.endsWith(".kt")) { "ClassFileName must end with '.kt'" } - val fileName = "src/main/java/$classFileName" + val fileName = "src/${sourceSet}/kotlin/$classFileName" file(fileName, fn) } diff --git a/src/functionalTest/kotlin/kotlinx/validation/test/MultiPlatformSingleJvmTargetTest.kt b/src/functionalTest/kotlin/kotlinx/validation/test/MultiPlatformSingleJvmTargetTest.kt new file mode 100644 index 00000000..ebc7876d --- /dev/null +++ b/src/functionalTest/kotlin/kotlinx/validation/test/MultiPlatformSingleJvmTargetTest.kt @@ -0,0 +1,121 @@ +/* + * Copyright 2016-2021 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 org.assertj.core.api.Assertions.assertThat +import org.junit.Test +import java.io.File + +internal class MultiPlatformSingleJvmTargetTest : BaseKotlinGradleTest() { + private fun BaseKotlinScope.createProjectHierarchyWithPluginOnRoot() { + settingsGradleKts { + resolve("examples/gradle/settings/settings-name-testproject.gradle.kts") + } + buildGradleKts { + resolve("examples/gradle/base/multiplatformWithSingleJvmTarget.gradle.kts") + } + } + + @Test + fun testApiCheckPasses() { + val runner = test { + createProjectHierarchyWithPluginOnRoot() + runner { + arguments.add(":apiCheck") + arguments.add("--stacktrace") + } + + dir("api/") { + file("testproject.api") { + resolve("examples/classes/Subsub1Class.dump") + resolve("examples/classes/Subsub2Class.dump") + } + } + + dir("src/jvmMain/kotlin") {} + kotlin("Subsub1Class.kt", "commonMain") { + resolve("examples/classes/Subsub1Class.kt") + } + kotlin("Subsub2Class.kt", "jvmMain") { + resolve("examples/classes/Subsub2Class.kt") + } + + }.addPluginTestRuntimeClasspath() + + runner.build().apply { + assertTaskSuccess(":apiCheck") + } + } + + @Test + fun testApiCheckFails() { + val runner = test { + createProjectHierarchyWithPluginOnRoot() + runner { + arguments.add("--continue") + arguments.add(":check") + arguments.add("--stacktrace") + } + + dir("api/") { + file("testproject.api") { + resolve("examples/classes/Subsub2Class.dump") + resolve("examples/classes/Subsub1Class.dump") + } + } + + dir("src/jvmMain/kotlin") {} + kotlin("Subsub1Class.kt", "commonMain") { + resolve("examples/classes/Subsub1Class.kt") + } + kotlin("Subsub2Class.kt", "jvmMain") { + resolve("examples/classes/Subsub2Class.kt") + } + + }.addPluginTestRuntimeClasspath() + + runner.buildAndFail().apply { + assertTaskFailure(":jvmApiCheck") + assertTaskNotRun(":apiCheck") + assertThat(output).contains("API check failed for project testproject") + assertTaskNotRun(":check") + } + } + + @Test + fun testApiDumpPasses() { + val runner = test { + createProjectHierarchyWithPluginOnRoot() + + runner { + arguments.add(":apiDump") + arguments.add("--stacktrace") + } + + dir("src/jvmMain/kotlin") {} + kotlin("Subsub1Class.kt", "commonMain") { + resolve("examples/classes/Subsub1Class.kt") + } + kotlin("Subsub2Class.kt", "jvmMain") { + resolve("examples/classes/Subsub2Class.kt") + } + + }.addPluginTestRuntimeClasspath() + + runner.build().apply { + assertTaskSuccess(":apiDump") + + val commonExpectedApi = readFileList("examples/classes/Subsub1Class.dump") + + val mainExpectedApi = commonExpectedApi + "\n" + readFileList("examples/classes/Subsub2Class.dump") + assertThat(jvmApiDump.readText()).isEqualToIgnoringNewLines(mainExpectedApi) + } + } + + private val jvmApiDump: File get() = rootProjectDir.resolve("api/testproject.api") + +} diff --git a/src/functionalTest/kotlin/kotlinx/validation/test/MultipleJvmTargetsTest.kt b/src/functionalTest/kotlin/kotlinx/validation/test/MultipleJvmTargetsTest.kt new file mode 100644 index 00000000..9b27a773 --- /dev/null +++ b/src/functionalTest/kotlin/kotlinx/validation/test/MultipleJvmTargetsTest.kt @@ -0,0 +1,140 @@ +/* + * Copyright 2016-2021 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 org.assertj.core.api.Assertions.assertThat +import org.gradle.testkit.runner.GradleRunner +import org.junit.Test +import java.io.File +import java.io.InputStreamReader + +internal class MultipleJvmTargetsTest : BaseKotlinGradleTest() { + private fun BaseKotlinScope.createProjectHierarchyWithPluginOnRoot() { + settingsGradleKts { + resolve("examples/gradle/settings/settings-name-testproject.gradle.kts") + } + buildGradleKts { + resolve("examples/gradle/base/multiplatformWithJvmTargets.gradle.kts") + } + } + + @Test + fun testApiCheckPasses() { + val runner = test { + createProjectHierarchyWithPluginOnRoot() + runner { + arguments.add(":apiCheck") + } + + dir("api/jvm/") { + file("testproject.api") { + resolve("examples/classes/Subsub1Class.dump") + resolve("examples/classes/Subsub2Class.dump") + } + } + + dir("api/anotherJvm/") { + file("testproject.api") { + resolve("examples/classes/Subsub1Class.dump") + } + } + + dir("src/jvmMain/kotlin") {} + kotlin("Subsub1Class.kt", "commonMain") { + resolve("examples/classes/Subsub1Class.kt") + } + kotlin("Subsub2Class.kt", "jvmMain") { + resolve("examples/classes/Subsub2Class.kt") + } + + }.addPluginTestRuntimeClasspath() + + runner.build().apply { + assertTaskSuccess(":apiCheck") + assertTaskSuccess(":jvmApiCheck") + assertTaskSuccess(":anotherJvmApiCheck") + } + } + + @Test + fun testApiCheckFails() { + val runner = test { + createProjectHierarchyWithPluginOnRoot() + runner { + arguments.add("--continue") + arguments.add(":check") + } + + dir("api/jvm/") { + file("testproject.api") { + resolve("examples/classes/Subsub2Class.dump") + resolve("examples/classes/Subsub1Class.dump") + } + } + + dir("api/anotherJvm/") { + file("testproject.api") { + resolve("examples/classes/Subsub2Class.dump") + } + } + + dir("src/jvmMain/kotlin") {} + kotlin("Subsub1Class.kt", "commonMain") { + resolve("examples/classes/Subsub1Class.kt") + } + kotlin("Subsub2Class.kt", "jvmMain") { + resolve("examples/classes/Subsub2Class.kt") + } + + }.addPluginTestRuntimeClasspath() + + runner.buildAndFail().apply { + assertTaskNotRun(":apiCheck") + assertTaskFailure(":jvmApiCheck") + assertTaskFailure(":anotherJvmApiCheck") + assertThat(output).contains("API check failed for project testproject") + assertTaskNotRun(":check") + } + } + + @Test + fun testApiDumpPasses() { + val runner = test { + createProjectHierarchyWithPluginOnRoot() + + runner { + arguments.add(":apiDump") + } + + dir("src/jvmMain/kotlin") {} + kotlin("Subsub1Class.kt", "commonMain") { + resolve("examples/classes/Subsub1Class.kt") + } + kotlin("Subsub2Class.kt", "jvmMain") { + resolve("examples/classes/Subsub2Class.kt") + } + + }.addPluginTestRuntimeClasspath() + runner.build().apply { + assertTaskSuccess(":apiDump") + assertTaskSuccess(":jvmApiDump") + assertTaskSuccess(":anotherJvmApiDump") + + System.err.println(output) + + val anotherExpectedApi = readFileList("examples/classes/Subsub1Class.dump") + assertThat(anotherApiDump.readText()).isEqualToIgnoringNewLines(anotherExpectedApi) + + val mainExpectedApi = anotherExpectedApi + "\n" + readFileList("examples/classes/Subsub2Class.dump") + assertThat(jvmApiDump.readText()).isEqualToIgnoringNewLines(mainExpectedApi) + } + } + + private val jvmApiDump: File get() = rootProjectDir.resolve("api/jvm/testproject.api") + private val anotherApiDump: File get() = rootProjectDir.resolve("api/anotherJvm/testproject.api") + +} diff --git a/src/functionalTest/resources/examples/gradle/base/multiplatformWithJvmTargets.gradle.kts b/src/functionalTest/resources/examples/gradle/base/multiplatformWithJvmTargets.gradle.kts new file mode 100644 index 00000000..8762a653 --- /dev/null +++ b/src/functionalTest/resources/examples/gradle/base/multiplatformWithJvmTargets.gradle.kts @@ -0,0 +1,57 @@ +/* + * Copyright 2016-2021 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.5.20" + id("org.jetbrains.kotlinx.binary-compatibility-validator") +} + +repositories { + mavenCentral() +} + +kotlin { + targets { + jvm { + compilations.all { + kotlinOptions.jvmTarget = "1.8" + } + testRuns["test"].executionTask.configure { + useJUnit() + } + } + jvm("anotherJvm") { + compilations.all { + kotlinOptions.jvmTarget = "1.8" + } + testRuns["test"].executionTask.configure { + useJUnit() + } + } + } + sourceSets { + val commonMain by getting + val commonTest by getting { + dependencies { + implementation(kotlin("stdlib")) + implementation(kotlin("test-common")) + implementation(kotlin("test-annotations-common")) + } + } + val jvmMain by getting + val jvmTest by getting { + dependencies { + implementation(kotlin("stdlib")) + implementation(kotlin("test-junit")) + } + } + val anotherJvmMain by getting + val anotherJvmTest by getting { + dependencies { + implementation(kotlin("test-junit")) + } + } + } +} diff --git a/src/functionalTest/resources/examples/gradle/base/multiplatformWithSingleJvmTarget.gradle.kts b/src/functionalTest/resources/examples/gradle/base/multiplatformWithSingleJvmTarget.gradle.kts new file mode 100644 index 00000000..610c6aac --- /dev/null +++ b/src/functionalTest/resources/examples/gradle/base/multiplatformWithSingleJvmTarget.gradle.kts @@ -0,0 +1,46 @@ +/* + * Copyright 2016-2021 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.5.20" + id("org.jetbrains.kotlinx.binary-compatibility-validator") +} + +repositories { + mavenCentral() +} + +kotlin { + targets { + jvm { + compilations.all { + kotlinOptions.jvmTarget = "1.8" + } + testRuns["test"].executionTask.configure { + useJUnit() + } + } +// android { +// +// } + } + sourceSets { + val commonMain by getting + val commonTest by getting { + dependencies { + implementation(kotlin("stdlib")) + implementation(kotlin("test-common")) + implementation(kotlin("test-annotations-common")) + } + } + val jvmMain by getting + val jvmTest by getting { + dependencies { + implementation(kotlin("stdlib")) + implementation(kotlin("test-junit")) + } + } + } +} diff --git a/src/main/kotlin/BinaryCompatibilityValidatorPlugin.kt b/src/main/kotlin/BinaryCompatibilityValidatorPlugin.kt index 3addaf5d..9ae989eb 100644 --- a/src/main/kotlin/BinaryCompatibilityValidatorPlugin.kt +++ b/src/main/kotlin/BinaryCompatibilityValidatorPlugin.kt @@ -5,12 +5,22 @@ package kotlinx.validation -import org.gradle.api.* -import org.gradle.api.plugins.* -import org.gradle.api.tasks.* -import org.jetbrains.kotlin.gradle.dsl.* -import org.jetbrains.kotlin.gradle.plugin.* -import java.io.* +import org.gradle.api.Action +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.api.Task +import org.gradle.api.plugins.JavaPluginConvention +import org.gradle.api.provider.Provider +import org.gradle.api.tasks.SourceSet +import org.gradle.api.tasks.SourceSetContainer +import org.gradle.api.tasks.Sync +import org.gradle.api.tasks.TaskProvider +import org.jetbrains.kotlin.gradle.dsl.KotlinAndroidProjectExtension +import org.jetbrains.kotlin.gradle.dsl.KotlinCommonOptions +import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension +import org.jetbrains.kotlin.gradle.plugin.KotlinCompilation +import org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType +import java.io.File const val API_DIR = "api" @@ -41,7 +51,7 @@ class BinaryCompatibilityValidatorPlugin : Plugin { if (sourceSet.name != SourceSet.MAIN_SOURCE_SET_NAME) { return@all } - project.configureApiTasks(sourceSet, extension) + project.configureApiTasks(sourceSet, extension, TargetConfig(project)) } } @@ -58,16 +68,41 @@ class BinaryCompatibilityValidatorPlugin : Plugin { project.pluginManager.withPlugin("kotlin-multiplatform") { if (project.name in extension.ignoredProjects) return@withPlugin val kotlin = project.extensions.getByName("kotlin") as KotlinMultiplatformExtension + + + // Create common tasks for multiplatform + val commonApiDump = project.tasks.register("apiDump") { + it.group = "other" + it.description = "Task that collects all target specific dump tasks" + } + + val commonApiCheck: TaskProvider? = project.tasks.register("apiCheck") { + it.group = "verification" + it.description = "Shortcut task that depends on all specific check tasks" + }.apply { project.tasks.named("check") { it.dependsOn(this) } } + + val jvmTargetCountProvider = project.provider { + kotlin.targets.count { + it.platformType in arrayOf(KotlinPlatformType.jvm, + KotlinPlatformType.androidJvm) + } + } + + val dirConfig = jvmTargetCountProvider.map { + if (it == 1) DirConfig.COMMON else DirConfig.TARGET_DIR + } + kotlin.targets.matching { it.platformType == KotlinPlatformType.jvm || it.platformType == KotlinPlatformType.androidJvm }.all { target -> + val targetConfig = TargetConfig(project, target.name, dirConfig) if (target.platformType == KotlinPlatformType.jvm) { target.compilations.matching { it.name == "main" }.all { - project.configureKotlinCompilation(it, extension) + project.configureKotlinCompilation(it, extension, targetConfig, commonApiDump, commonApiCheck) } } else if (target.platformType == KotlinPlatformType.androidJvm) { target.compilations.matching { it.name == "release" }.all { - project.configureKotlinCompilation(it, extension, useOutput = true) + project.configureKotlinCompilation(it, extension, targetConfig, commonApiDump, commonApiCheck, useOutput = true) } } } @@ -75,30 +110,70 @@ class BinaryCompatibilityValidatorPlugin : Plugin { } } +private class TargetConfig constructor( + project: Project, + val targetName: String? = null, + private val dirConfig: Provider? = null, +) { + + private val API_DIR_PROVIDER = project.provider { API_DIR } + + fun apiTaskName(suffix: String) = when (targetName) { + null, "" -> "api$suffix" + else -> "${targetName}Api$suffix" + } + + val apiDir + get() = dirConfig?.map { dirConfig -> + when { + dirConfig == DirConfig.COMMON -> API_DIR + + else -> "$API_DIR/$targetName" + } + } ?: API_DIR_PROVIDER + +} + + +enum class DirConfig { + COMMON, + TARGET_DIR, +} + private fun Project.configureKotlinCompilation( compilation: KotlinCompilation, extension: ApiValidationExtension, - useOutput: Boolean = false + targetConfig: TargetConfig = TargetConfig(this), + commonApiDump: TaskProvider? = null, + commonApiCheck: TaskProvider? = null, + useOutput: Boolean = false, ) { val projectName = project.name - val apiBuildDir = file(buildDir.resolve(API_DIR)) - val apiBuild = task("apiBuild", extension) { + val apiDirProvider = targetConfig.apiDir + val apiBuildDir = apiDirProvider.map { buildDir.resolve(it) } + + val apiBuild = task(targetConfig.apiTaskName("Build"), extension) { // Do not enable task for empty umbrella modules - isEnabled = apiCheckEnabled(extension) && compilation.allKotlinSourceSets.any { it.kotlin.srcDirs.any { it.exists() } } + isEnabled = + apiCheckEnabled(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" if (useOutput) { // Workaround for #4 - inputClassesDirs = files(provider { if (isEnabled) compilation.output.classesDirs else emptyList() }) - inputDependencies = files(provider { if (isEnabled) compilation.output.classesDirs else emptyList() }) + inputClassesDirs = + files(provider { if (isEnabled) compilation.output.classesDirs else emptyList() }) + inputDependencies = + files(provider { if (isEnabled) compilation.output.classesDirs else emptyList() }) } else { - inputClassesDirs = files(provider { if (isEnabled) compilation.output.classesDirs else emptyList() }) - inputDependencies = files(provider { if (isEnabled) compilation.compileDependencyFiles else emptyList() }) + inputClassesDirs = + files(provider { if (isEnabled) compilation.output.classesDirs else emptyList() }) + inputDependencies = + files(provider { if (isEnabled) compilation.compileDependencyFiles else emptyList() }) } - outputApiDir = apiBuildDir + outputApiDir = apiBuildDir.get() } - configureCheckTasks(apiBuildDir, apiBuild, extension) + configureCheckTasks(apiBuildDir, apiBuild, extension, targetConfig, commonApiDump, commonApiCheck) } val Project.sourceSets: SourceSetContainer @@ -107,68 +182,86 @@ val Project.sourceSets: SourceSetContainer fun Project.apiCheckEnabled(extension: ApiValidationExtension): Boolean = name !in extension.ignoredProjects && !extension.validationDisabled -private fun Project.configureApiTasks(sourceSet: SourceSet, extension: ApiValidationExtension) { +private fun Project.configureApiTasks( + sourceSet: SourceSet, + extension: ApiValidationExtension, + targetConfig: TargetConfig = TargetConfig(this), +) { val projectName = project.name - val apiBuildDir = file(buildDir.resolve(API_DIR)) - val apiBuild = task("apiBuild", extension) { + val apiBuildDir = targetConfig.apiDir.map { buildDir.resolve(it) } + val apiBuild = task(targetConfig.apiTaskName("Build"), extension) { isEnabled = apiCheckEnabled(extension) // '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" inputClassesDirs = files(provider { if (isEnabled) sourceSet.output.classesDirs else emptyList() }) inputDependencies = files(provider { if (isEnabled) sourceSet.output.classesDirs else emptyList() }) - outputApiDir = apiBuildDir + outputApiDir = apiBuildDir.get() } - configureCheckTasks(apiBuildDir, apiBuild, extension) + configureCheckTasks(apiBuildDir, apiBuild, extension, targetConfig) } private fun Project.configureCheckTasks( - apiBuildDir: File, + apiBuildDir: Provider, apiBuild: TaskProvider, - extension: ApiValidationExtension + extension: ApiValidationExtension, + targetConfig: TargetConfig, + commonApiDump: TaskProvider? = null, + commonApiCheck: TaskProvider? = null, ) { val projectName = project.name - val apiCheckDir = file(projectDir.resolve(API_DIR)) - val apiCheck = task("apiCheck") { + val apiCheckDir = targetConfig.apiDir.map { + projectDir.resolve(it).also { r -> + logger.lifecycle("Configuring api for ${targetConfig.targetName} to $r") + } + } + val apiCheck = task(targetConfig.apiTaskName("Check")) { isEnabled = apiCheckEnabled(extension) && apiBuild.map { it.enabled }.getOrElse(true) group = "verification" description = "Checks signatures of public API against the golden value in API folder for $projectName" - projectApiDir = if (apiCheckDir.exists()) { - apiCheckDir - } else { - nonExistingProjectApiDir = apiCheckDir.toString() - null + run { + val d = apiCheckDir.get() + projectApiDir = if (d.exists()) { + d + } else { + nonExistingProjectApiDir = d.toString() + null + } + this.apiBuildDir = apiBuildDir.get() } - this.apiBuildDir = apiBuildDir dependsOn(apiBuild) } - task("apiDump") { + val apiDump = task(targetConfig.apiTaskName("Dump")) { isEnabled = apiCheckEnabled(extension) && apiBuild.map { it.enabled }.getOrElse(true) group = "other" - description = "Syncs API from build dir to $API_DIR dir for $projectName" + description = "Syncs API from build dir to ${targetConfig.apiDir} dir for $projectName" from(apiBuildDir) into(apiCheckDir) dependsOn(apiBuild) doFirst { - apiCheckDir.mkdirs() + apiCheckDir.get().mkdirs() } } - project.tasks.named("check").configure { - it.dependsOn(apiCheck) + + commonApiDump?.configure { it.dependsOn(apiDump) } + + when (commonApiCheck) { + null -> project.tasks.named("check").configure { it.dependsOn(apiCheck) } + else -> commonApiCheck.configure { it.dependsOn(apiCheck) } } } inline fun Project.task( name: String, - noinline configuration: T.() -> Unit + noinline configuration: T.() -> Unit, ): TaskProvider = tasks.register(name, T::class.java, Action(configuration)) inline fun Project.task( name: String, extension: ApiValidationExtension, - noinline configuration: T.() -> Unit + noinline configuration: T.() -> Unit, ): TaskProvider = tasks.register(name, T::class.java, extension).also { it.configure(Action(configuration)) }