Gradle Plugin: What, Why and How?

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. .





  1. , , ! Gradle-, ?  





  2. Gradle Plugin: , ?





  3. 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:





  1. script- , Gradle 7.1 - , Kotlin DSL. , , Gradle 7.1 .





  2. Script-, Kotlin, , Gradle 6 .





  3. Script-, Groovy, , Gradle 5 . 





  4. 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-? . , .





Github.





opensource-, :





  1. Releases Hub Plugin –





  2. Swagger Codegen Plugin – swagger-.





  3. Dokka – Kotlin-





!








All Articles