Skip to content

Commit

Permalink
Documentation & code cleanup (#69)
Browse files Browse the repository at this point in the history
  • Loading branch information
qwwdfsad authored Oct 4, 2021
1 parent 6a3180b commit 5ef3f74
Show file tree
Hide file tree
Showing 2 changed files with 105 additions and 76 deletions.
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ The plugin provides two tasks:
in project `api` subfolder. This task is automatically inserted into `check` pipeline, so both `build` and `check`
tasks will start checking public API upon their execution.

> For projects with multiple JVM targets, multiple subfolders will be created, e.g. `api/jvm` and `api/android`
### Optional parameters

Binary compatibility validator can be additionally configured with the following DSL:
Expand Down Expand Up @@ -145,7 +147,7 @@ When starting to validate your library public API, we recommend the following wo

### Classes

A class is considered to be effectively public if all of the following conditions are met:
A class is considered to be effectively public if all the following conditions are met:

- it has public or protected JVM access (`ACC_PUBLIC` or `ACC_PROTECTED`)
- it has one of the following visibilities in Kotlin:
Expand All @@ -163,7 +165,7 @@ A class is considered to be effectively public if all of the following condition
### Members

A member of the class (i.e. a field or a method) is considered to be effectively public
if all of the following conditions are met:
if all the following conditions are met:

- it has public or protected JVM access (`ACC_PUBLIC` or `ACC_PROTECTED`)
- it has one of the following visibilities in Kotlin:
Expand Down
175 changes: 101 additions & 74 deletions src/main/kotlin/BinaryCompatibilityValidatorPlugin.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,13 @@

package kotlinx.validation

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
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 java.io.*

const val API_DIR = "api"

Expand All @@ -45,67 +36,96 @@ class BinaryCompatibilityValidatorPlugin : Plugin<Project> {
}

private fun configureProject(project: Project, extension: ApiValidationExtension) {
project.pluginManager.withPlugin("kotlin") {
if (project.name in extension.ignoredProjects) return@withPlugin
project.sourceSets.all { sourceSet ->
if (sourceSet.name != SourceSet.MAIN_SOURCE_SET_NAME) {
return@all
}
project.configureApiTasks(sourceSet, extension, TargetConfig(project))
}
}

project.pluginManager.withPlugin("kotlin-android") {
if (project.name in extension.ignoredProjects) return@withPlugin
val androidExtension = project.extensions.getByName("kotlin") as KotlinAndroidProjectExtension
androidExtension.target.compilations.matching {
it.compilationName == "release"
}.all {
project.configureKotlinCompilation(it, extension, useOutput = true)
}
}
configureKotlinPlugin(project, extension)
configureAndroidPlugin(project, extension)
configureMultiplatformPlugin(project, extension)
}

project.pluginManager.withPlugin("kotlin-multiplatform") {
if (project.name in extension.ignoredProjects) return@withPlugin
val kotlin = project.extensions.getByName("kotlin") as KotlinMultiplatformExtension
private fun configurePlugin(
name: String,
project: Project,
extension: ApiValidationExtension,
action: Action<AppliedPlugin>
) = project.pluginManager.withPlugin(name) {
if (project.name in extension.ignoredProjects) return@withPlugin
action.execute(it)
}

private fun configureMultiplatformPlugin(
project: Project,
extension: ApiValidationExtension
) = configurePlugin("kotlin-multiplatform", project, extension) {
if (project.name in extension.ignoredProjects) return@configurePlugin
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"
}

// 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<Task>? = 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 commonApiCheck: TaskProvider<Task>? = 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 dirConfig = jvmTargetCountProvider.map {
if (it == 1) DirConfig.COMMON else DirConfig.TARGET_DIR
}

val jvmTargetCountProvider = project.provider {
kotlin.targets.count {
it.platformType in arrayOf(KotlinPlatformType.jvm,
KotlinPlatformType.androidJvm)
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, targetConfig, commonApiDump, commonApiCheck)
}
} else if (target.platformType == KotlinPlatformType.androidJvm) {
target.compilations.matching { it.name == "release" }.all {
project.configureKotlinCompilation(
it,
extension,
targetConfig,
commonApiDump,
commonApiCheck,
useOutput = true
)
}
}
}
}

val dirConfig = jvmTargetCountProvider.map {
if (it == 1) DirConfig.COMMON else DirConfig.TARGET_DIR
}
private fun configureAndroidPlugin(
project: Project,
extension: ApiValidationExtension
) = 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)
}
}

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, targetConfig, commonApiDump, commonApiCheck)
}
} else if (target.platformType == KotlinPlatformType.androidJvm) {
target.compilations.matching { it.name == "release" }.all {
project.configureKotlinCompilation(it, extension, targetConfig, commonApiDump, commonApiCheck, useOutput = true)
}
}
private fun configureKotlinPlugin(
project: Project,
extension: ApiValidationExtension
) = configurePlugin("kotlin", project, extension) {
project.sourceSets.all { sourceSet ->
if (sourceSet.name != SourceSet.MAIN_SOURCE_SET_NAME) {
return@all
}
project.configureApiTasks(sourceSet, extension, TargetConfig(project))
}
}
}
Expand All @@ -125,18 +145,25 @@ private class TargetConfig constructor(

val apiDir
get() = dirConfig?.map { dirConfig ->
when {
dirConfig == DirConfig.COMMON -> API_DIR

when (dirConfig) {
DirConfig.COMMON -> API_DIR
else -> "$API_DIR/$targetName"
}
} ?: API_DIR_PROVIDER

}


enum class DirConfig {
private enum class DirConfig {
/**
* `api` directory for .api files.
* Used in single target projects
*/
COMMON,
/**
* Target-based directory, used in multitarget setups.
* E.g. for the project with targets jvm and android,
* the resulting paths will be
* `/api/jvm/project.api` and `/api/android/project.api`
*/
TARGET_DIR,
}

Expand All @@ -156,7 +183,7 @@ private fun Project.configureKotlinCompilation(
// Do not enable task for empty umbrella modules
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
// '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) {
Expand Down Expand Up @@ -213,7 +240,7 @@ private fun Project.configureCheckTasks(
val projectName = project.name
val apiCheckDir = targetConfig.apiDir.map {
projectDir.resolve(it).also { r ->
logger.lifecycle("Configuring api for ${targetConfig.targetName} to $r")
logger.debug("Configuring api for ${targetConfig.targetName ?: "jvm"} to $r")
}
}
val apiCheck = task<ApiCompareCompareTask>(targetConfig.apiTaskName("Check")) {
Expand Down

0 comments on commit 5ef3f74

Please sign in to comment.