Good time, reader! In the previous article, we looked at how to effectively use standard Gradle tools in projects to solve everyday tasks and touched a little on the engine compartment.
Gradle-, , , Kotlin- Gradle-.
, Kotlin-. Gradle- Kotlin. .
-
Gradle Plugin: , ?
Gradle-
Gradle-
, Gradle- , .
Gradle-. , . / / / CI / .
, apply:
class MyPlugin : Plugin<Project> {
override fun apply(project: Project) {
//β¦
}
}
Project- Gradle- ( Gradle-), . Project God Object, Gradle API, , , .
Gradle ( Gradle). - .
build.gradle(.kts)
β . , build.gradle(.kts). , , , Gradle project- . .
-, , . -, , . , . .
buildSrc
buildSrc jar, buildSrc, Gradle- . - c , buildSrc , buildSrc - , .
standalone- .
, . buildSrc Kotlin DSL. Kotlin, Gradle API .
plugins { `kotlin-dsl` }
P.S. .kts embedded- Kotlin, Gradle, Kotlin . , .
buildSrc/src/main/kotlin. , . apply
:
apply<MyPlugin>()
c id, . Kotlin DSL Java Gradle Plugin Development Plugin, :
gradlePlugin {
plugins {
register(βfirst-pluginβ) {
description = "My first plugin"
displayName = "Does nothing"
id = βru.myorg.demo.my-pluginβ
implementationClass = "ru.myorg.demo.MyPlugin"
}
}
}
:
src/main/resources/META-INF/gradle-plugins/ru.myorg.demo.my-plugin.properties
implementation-class=ru.myorg.demo.MyPlugin
, Gradle jar id . :
plugins {
//...
id("ru.myorg.demo.my-plugin")
}
Script-
Gradle . build.gradle(.kts) Groovy, Kotlin. Kotlin β buildSrc, standalone Gradle-, .
Groovy , , , , apply from
.
script- id. Gradle Plugin Development Plugin, .
: script- , Gradle:
script- , Gradle 7.1 - , Kotlin DSL. , , Gradle 7.1 .
Script-, Kotlin, , Gradle 6 .
Script-, Groovy, , Gradle 5 .
Script- Kotlin, Gradle 6.0, Groov, Gradle 6.4.
Gradle .
script-
. script- Kotlin Android- buildSrc.
Android Gradle Plugin:
implementation("com.android.tools.build:gradle:$agpVersion")
//4.1.2
:
buildSrc/src/main/kotlin/android-common.gradle.kts:
plugins {
id("com.android.library")
id("kotlin-android")
}
android {
compileSdkVersion(androidCompileSdkVersion)
buildToolsVersion(androidBuildToolsVersion)
defaultConfig {
minSdkVersion(androidMinSdkVersion)
targetSdkVersion(androidTargetSdkVersion)
}
sourceSets["main"].java.srcDirs("src/main/kotlin")
sourceSets["test"].java.srcDirs("src/test/kotlin")
}
Gradle Sync : BUILD SUCCESSFUL IN 2m 6s
? buildSrc/build, :
, , plugins {}
Kotlin- .kts-. com.android.library.
, Kotlin DSL Gradle-, , .gradle .
, c script- , . , , . :
class AndroidLibraryPlugin: Plugin<Project> {
override fun apply(target: Project) = target.applyAndroid()
private fun Project.applyAndroid() {
plugins.run {
apply("com.android.library")
apply("kotlin-android")
}
android {
//...
}
}
}
android {}
Kotlin-:
fun Project.android(action: BaseExtension.() -> Unit) =
(extensions["android"] as BaseExtension).run(action)
: BUILD SUCCESSFUL in 48s
. Not bad, not terrible. , . , id buildSrc Kotlin-:
val PluginDependenciesSpec.`android-library-common`: PluginDependencySpec
get() = id("ru.myorg.demo.android-library-common")
:
plugins { `android-library-common` }
, .
, Kotlin-
, Kotlin-. , Gradle, buildSrc. standalone-, . Gradle 7.1. , .
standalone-.
Standalone-
Standalone- Gradle-. , buildSrc script-. git- , .
. , :
plugins {
`kotlin-dsl`
}
repositories {
mavenCentral()
}
dependencies {
implementation("com.squareup:kotlinpoet:1.7.2")
implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:1.5.10")
}
Kotlin Poet, Kotlin Gradle Plugin, JVM-.
, .
- . . .
Gradle extension-, , , . extension- :
open class CodegenPluginExtension(project: Project) {
private val objects = project.objects
private val packageNameProp: Property<String> = objects.property<String>()
.convention("ru.myorg.demo")
private val messageFileProperty = objects.fileProperty()
private val outputDirProperty = objects.directoryProperty()
.convention(project.layout.buildDirectory.dir("src-gen"))
var messageFile: RegularFile
get() = messageFileProperty.get()
set(value) = messageFileProperty.set(value)
var outputDir: Directory
get() = outputDirProperty.get()
set(value) = outputDirProperty.set(value)
var packageName: String
get() = packageNameProp.get()
set(value) = packageNameProp.set(value)
}
abstract open , Gradle .
Gradle properties. , (, Gradle-). , .
extension- :
override fun apply(target: Project) {
//...
val extension: CodegenPluginExtension =
target.extensions.create("simpleCodegen", target)
}
( ) . , project, . extension build.gradle(.kts) :
simpleCodegen {
//...
}
Gradle-, :
open class CodegenTask : DefaultTask() {
@get:InputFile
lateinit var messageFile: File
@get:Input
lateinit var packageName: String
@get:OutputDirectory
lateinit var outputDir: File
@TaskAction
fun invoke() {
val messageProvider = FileMessageProvider(messageFile)
val simpleFileGenerator = SimpleMessageClassGenerator(messageProvider)
simpleFileGenerator.generate(packageName, outputDir)
}
}
@TaskAction , . Kotlin- Gradle-, , , . , Github.
, . , , sourceSet. , , . afterEvaluate
, , sourceSets .
target.afterEvaluate {
(target.extensions["sourceSets"] as SourceSetContainer)["main"]
.java
.srcDir(extension.outputDir)
}
-, :
with(target.tasks) {
val codegenTask = register<CodegenTask>("codegenSimpleClass") {
group = "Code generation"
description = "Generates Kotlin class with single message property"
packageName = extension.packageName
messageFile = extension.messageFile.asFile
outputDir = extension.outputDir.asFile
}
target.tasks.withType<KotlinCompile>().configureEach {
dependsOn(codegenTask)
}
}
. :
Gradle-
class CodegenPlugin : Plugin<Project> {
override fun apply(target: Project) {
val extension = target.extensions.create<CodegenPluginExtension>("simpleCodegen", target)
with(target.tasks) {
val codegenTask = register<CodegenTask>("codegenSimpleClass") {
group = "Code generation"
description = "Generates simple Kotlin class with single message property"
packageName = extension.packageName
messageFile = extension.messageFile.asFile
outputDir = extension.outputDir.asFile
}
target.tasks.withType<KotlinCompile>().configureEach {
dependsOn(codegenTask)
}
}
target.afterEvaluate {
(target.extensions["sourceSets"] as SourceSetContainer)["main"]
.java
.srcDir(extension.outputDir)
}
}
}
, :
settings.gradle.kts:
pluginManagement {
includeBuild("../standalone-codegen-plugin")
}
:
simpleCodegen {
messageFile = layout.projectDirectory.file("message.txt")
packageName = "ru.myorg.example"
}
Build, . build/src-gen .
! . .
, Gradle . , .
, , Run Configurations Debug. Gradle-.
Gradle- : , .
β , , . Gradle - . sourceSet test
, .
sourceSets. , sourceSet .
sourceSet, Gradle-. , check. :
build.gradle.kts (Gradle- ):
val integrationTest: SourceSet by sourceSets.creating
val functionalTest: SourceSet by sourceSets.creating
val integrationTestTask = tasks.register<Test>("integrationTest") {
description = "Runs the integration tests."
group = "verification"
testClassesDirs = integrationTest.output.classesDirs
classpath = integrationTest.runtimeClasspath
mustRunAfter(tasks.test)
}
val functionalTestTask = tasks.register<Test>("functionalTest") {
description = "Runs the functional tests."
group = "verification"
testClassesDirs = functionalTest.output.classesDirs
classpath = functionalTest.runtimeClasspath
mustRunAfter(tasks.test)
}
tasks.check {
dependsOn(integrationTestTask)
dependsOn(functionalTestTask)
}
dependencies {
"integrationTestImplementation"(project)
"integrationTestImplementation"("junit:junit:4.12")
"functionalTestImplementation"("junit:junit:4.12")
}
, , . Gradle -.
sourceSet /src .
src/integrationTest/kotlin/{packageName}:
class SimpleMessageClassGeneratorTest {
@get:Rule
val tempFolder = TemporaryFolder()
@Test
fun verifyCorrectKotlinFileCreated() {
val message = "Hello!"
val messageProvider = MessageProvider { message }
val simpleMessageClassGenerator = SimpleMessageClassGenerator(messageProvider)
val expectedKotlinClass = "package ru.myorg.demo\n" +
"\n" +
"import kotlin.String\n" +
"\n" +
"public class SimpleClass {\n" +
" public val message: String = \"Hello!\"\n" +
"}\n"
simpleMessageClassGenerator.generate(
packageName = "ru.myorg.demo",
outputDir = tempFolder.root
)
val generatedFile = File(
tempFolder.root,
"/ru/myorg/demo/SimpleClass.kt"
)
assert(
generatedFile.exists() &&
generatedFile.readText() == expectedKotlinClass
)
}
β . Gradle . Gradle Daemon. , , Gradle-.
Gradle Daemon, Gradle Runner. Gradle Runner API classpath :
gradlePlugin {
//...
testSourceSets(functionalTest)
}
, Gradle Runner, . :
src/functionalTest/kotlin/{packageName}:
class CodegenPluginTest {
@get:Rule
val tempFolder = TemporaryFolder()
@Test
fun canSuccessfullyPrintMessageToFileInProjectDir() {
/**
* message
*/
val messageFileName = "message.txt"
val messageFile = tempFolder.newFile(messageFileName)
messageFile.bufferedWriter().write("Hello!")
/**
* build.gradle
*/
val generatedPackageName = "ru.myorg.demo.example"
val buildGradleFile = tempFolder.newFile("build.gradle.kts")
buildGradleFile.printWriter()
.use {
it.print(
"plugins {\n" +
" kotlin(\"jvm\") version \"1.5.10\"\n" +
" id(\"ru.myorg.demo.codegen-plugin\")\n" +
"}\n" +
"\n" +
"simpleCodegen {\n" +
" messageFile = layout.projectDirectory.file(\"$messageFileName\")\n" +
" packageName = \"$generatedPackageName\"\n" +
"}\n" +
"\n" +
"repositories {\n" +
" mavenCentral()\n" +
"}\n" +
"\n" +
"dependencies {\n" +
" implementation(kotlin(\"stdlib\"))\n" +
"}"
)
}
/**
* Gradle Daemon .
*/
GradleRunner.create()
.withProjectDir(tempFolder.root)
.withArguments("build")
.withPluginClasspath()
.build()
/**
* , . .
*/
val outputFile =
File(
tempFolder.root,
"/build/src-gen/" +
generatedPackageName.replace('.', '/') +
"/SimpleClass.kt"
)
assert(outputFile.exists())
}
}
! , - . β .
Maven Publish. , :
plugins {
//...
`maven-publish`
}
publishing {
publications {
create<MavenPublication>("codegen-plugin") {
artifactId = "codegen-plugin"
groupId = "ru.myorg.demo"
version = "1.0.0"
from(components["kotlin"])
}
}
repositories {
maven {
name = "remote"
url = uri("https://some-remote-repo")
credentials {
username = project.ext["MY_REMOTE_REPO_USERNAME"] as String
password = project.ext["MY_REMOTE_REPO_PASSWORD"] as String
}
}
}
}
Gradle- . , Gradle API - , . , Gradle .
Gradle- , , CI. Gradle-? . , .
opensource-, :
!