Gradle plugins and extensions

469 阅读4分钟

在 Gradle 插件中创建自定义嵌套 DSL

// app/build.gradle
theState {
  theDeepState {
    theDeepestState {
      undermine 'the will of the people'
    }
  }
}

特定领域语言

我们将从查看扩展本身开始,然后回溯到它是如何配置和使用的,最后是如何声明和构建它。

// TheStateExtension.kt
package mutual.aid.gradle

import org.gradle.api.Action
import org.gradle.api.Project
import org.gradle.api.model.ObjectFactory
import javax.inject.Inject

open class TheStateExtension @Inject constructor(
  objects: ObjectFactory
) {

  /** Configure the inner DSL object, [TheDeepStateHandler]. */
  val theDeepState: TheDeepStateHandler = objects.newInstance(TheDeepStateHandler::class.java)

  /** Configure the inner DSL object, [TheDeepStateHandler]. */
  fun theDeepState(action: Action<TheDeepStateHandler>) {
    action.execute(theDeepState)
  }

  companion object {
    fun Project.theState(): TheStateExtension {
      return extensions.create("theState", TheStateExtension::class.java)
    }
  }
}

/**
 * An inner DSL object.
 */
open class TheDeepStateHandler @Inject constructor(
  objects: ObjectFactory
) {

  /** Configure the innermost DSL object, [TheDeepestStateHandler]. */
  val theDeepestState: TheDeepestStateHandler = objects.newInstance(TheDeepestStateHandler::class.java)

  /** Configure the innermost DSL object, [TheDeepestStateHandler]. */
  fun theDeepestState(action: Action<TheDeepestStateHandler>) {
    action.execute(theDeepestState)
  }
}

/**
 * An even-more inner-er DSL object.
 */
open class TheDeepestStateHandler {

  private val whoToUndermine = mutableListOf<String>()
  internal val victims: List<String> get() = whoToUndermine.toList()

  /** Tells the app who - or which groups - it should undermine. */
  fun undermine(who: String) {
    whoToUndermine.add(who)
  }
}
  • 我喜欢将最外层的扩展类命名为 FooExtension,而将内部的 DSL 对象命名为 BarHandler。拥有这样的约定可以更轻松地在大型代码库中导航。
  • 可以使用各种服务(如 ObjectFactory)以及您提供的完全任意的对象来注入所有这些类型。只要记住@Inject 构造函数!
  • 不要尝试使用 Groovy 闭包或 Kotlin lambdas-with-receivers 来获得创意——只需使用 ObjectFactory 和 Action 接口。
  • 可以直接公开处理程序(就像我在示例中所做的那样),也可以通过函数公开它们,这使您的用户可以使用点符号和带花括号的类似 DSL 的语法。

实例化扩展

如何创建和配置最外层的扩展?

// ThePluginOfOppression.kt
package mutual.aid.gradle

import mutual.aid.gradle.TheStateExtension.Companion.theState
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.tasks.JavaExec

class ThePluginOfOppression : Plugin<Project> {

  override fun apply(project: Project): Unit = project.run {
    val theState = theState()
  }
}

TheStateExtension.theState() 是一个简单的伴随函数 project.extensions.create("theState", TheStateExtension::class.java)。我喜欢将该函数与类本身一起封装,作为工厂方法。同样重要的是要注意,即使我没有使用我创建的 theState 实例,我仍然需要在此处创建它,以便在应用此插件时可以在构建脚本中访问它。

在构建脚本中应用插件并配置扩展

// app/build.gradle
plugins {
  id 'mutual.aid.oppression-plugin'
}

// 1: DSL-like
theState {
  theDeepState {
    theDeepestState {
      undermine 'the will of the people'
    }
  }
}

// 2: With dot-notation for the laconic
theState
  .theDeepState
  .theDeepestState
  .undermine 'the will of the people'

// 3: Mix and match
theState.theDeepState.theDeepestState {
  undermine 'the will of the people'
}

应用插件并配置扩展。现在是讨论启用 DSL 语法的那些 Action 函数的好时机。

import org.gradle.api.Action

fun theDeepState(action: Action<TheDeepStateHandler>) {
  action.execute(theDeepState)
}

Gradle 对这些类型有特殊处理。在(构建)运行时,Gradle 动态重写您的构建代码(使用 ASM),使得方法签名 theDeepState(action: Action) 变为有效的 theDeepState(action: T.() -> Unit)。

利用用户提供的配置

class ThePluginOfOppression : Plugin<Project> {

  override fun apply(project: Project): Unit = project.run {
    // 1: Apply additional plugins    
    pluginManager.apply("org.jetbrains.kotlin.jvm")
    pluginManager.apply("application")

    // 2: Create our extension
    val theState = theState()

    // 3: Wait for the DSL to be evaluated, and use the information provided
    afterEvaluate {
      tasks.named("run", JavaExec::class.java) {
        it.args = theState.theDeepState.theDeepestState.victims
      }
    }
  }
}
  1. 应用额外的插件。通过我们的插件应用这些其他插件并不是绝对必要的,但它展示了约定插件的多功能性,并且还有助于使我们的示例更加封装。
  2. 创建我们的扩展。和之前一样。
  3. 利用用户提供的数据。有时无法使用 Provider API 并且必须等待用户数据——这就是 afterEvaluate 的目的。将数据推送到标准的 JavaExec 任务中。
$ ./gradlew -q app:run
Now undermining: the will of the people

域对象容器

android {
  buildTypes {
    release { ... }
    debug { ... }
    myCustomBuildType { ... }
  }
}

当查看上述 Kotlin DSL 版本时,情况会变得更加清晰:

android {
  buildTypes {
    getByName("release") { ... }
    getByName("debug") { ... }
    create("myCustomBuildType") { ... }
  }
}

buildTypes 是一个提供 NamedDomainObjectContainer 的函数。 Groovy 风格的 Gradle 具有将 debug {} 转换为 getByName("debug") {} 或 create("debug") {} 的语法糖(如果尚未创建该命名类型)。

使用域对象容器

让我们从一个新的扩展开始,ThePeopleExtension:

package mutual.aid.gradle.people

import org.gradle.api.Action
import org.gradle.api.Named
import org.gradle.api.NamedDomainObjectContainer
import org.gradle.api.Project
import org.gradle.api.model.ObjectFactory
import org.gradle.api.provider.Property
import javax.inject.Inject

open class ThePeopleExtension @Inject constructor(objects: ObjectFactory) {

  val problems = objects.domainObjectContainer(ProblemHandler::class.java)

  fun problems(action: Action<NamedDomainObjectContainer<ProblemHandler>>) {
    action.execute(problems)
  }

  companion object {
    internal fun Project.thePeople(): ThePeopleExtension =
      extensions.create("thePeople", ThePeopleExtension::class.java)
  }
}

open class ProblemHandler @Inject constructor(
  private val name: String,
  objects: ObjectFactory
) : Named {

  override fun getName(): String = name

  internal val description: Property<String> = objects.property(String::class.java)
  val solutions = objects.domainObjectContainer(SolutionHandler::class.java)

  fun solutions(action: Action<NamedDomainObjectContainer<SolutionHandler>>) {
    action.execute(solutions)
  }

  fun description(description: String) {
    this.description.set(description)
    this.description.disallowChanges()
  }
}

open class SolutionHandler @Inject constructor(
  private val name: String,
  objects: ObjectFactory
) : Named {

  override fun getName(): String = name

  internal val action: Property<String> = objects.property(String::class.java)
  internal val description: Property<String> = objects.property(String::class.java)
  internal val rank: Property<Int> = objects.property(Int::class.java)

  fun action(action: String) {
    this.action.set(action)
    this.action.disallowChanges()
  }

  fun description(description: String) {
    this.description.set(description)
    this.description.disallowChanges()
  }

  fun rank(rank: Int) {
    this.rank.set(rank)
    this.rank.disallowChanges()
  }
}

首先,请注意在 NamedDomainObjectContainer 中的类型都实现了 Named 接口。这不是绝对必要的,但类型必须具有 getName(): String 函数,否则它们不能进入命名的域对象容器。

其次,我们使用 ObjectFactory.domainObjectContainer(Class) 方法创建这样一个容器。

上面最后一个有趣的模式是这样的:

fun description(description: String) {
  this.description.set(action)
  this.description.disallowChanges()
}

由于 description 是一个 Property,我更喜欢将这些值保留在内部并通过函数公开它们。然后用户有一个很好的 DSL,比如 description 'my description(在 Groovy 中)或 description("my description")(在 Kotlin 中)。封装这些字段还可以让我做一些额外的事情,比如调用 disallowChanges(),我认为这对于防止违反最小惊讶原则很重要。否则,用户可能会从多个位置重复调用 description() 方法,并且很难判断数据的真正来源。当我们这样做时,有人尝试多次调用该方法,构建将失败。

// app/build.gradle
thePeople {
  problems {
    climateChange {
      description 'There is no question of cost, because the cost of doing nothing is everything.'
      solutions {
        cleanEnergy {
          description 'We cannot burn any more fossil energy'
          action 'Replace all fossil sources with clean solutions like wind, solar, and geothermal'
          rank 1
        }
        massTransit {
          description 'Single-occupant vehicles are a major source of carbon pollution'
          action 'Increase density in urban environments and build free public transit for all'
          rank 2
        }
        stopEatingAnimals {
          description 'Animal agriculture is one of the top contributors to carbon pollution'
          action 'Most people can thrive on a plant-based diet and do not need animal protein, and could make such a choice with immediate effect'
          rank 3
        }
        antiRacism {
          description 'People of Western European descent ('white people') have been the primary beneficiaries of burning fossil carbon'
          action 'White people should should bear the responsibility of paying for climate change mitigation'
          rank 4
        }
        seizeGlobalCapital {
          description 'The costs of climate change are inequitably distributed'
          action 'The costs of climate change mitigation should be born primarily by the wealthiest'
          rank 5
        }
        lastResort {
          description 'If the rich and the powerful refuse to get out of the way of legislative reforms of the system killing us all, there is, unfortunately, always a last resort'
          action 'It starts with 'g' and rhymes with 'poutine''
          rank 6
        }
      }
    }
  }
}
class ThePluginOfThePeople : Plugin<Project> {

  override fun apply(project: Project): Unit = project.run {
    val thePeople = thePeople()

    thePeople.problems.all { problem ->
      tasks.register("listSolutionsFor${problem.name.capitalize()}", ListSolutionsTask::class.java) {
        it.problem.set(problem)
      }
    }
  }
}

abstract class ListSolutionsTask : DefaultTask() {

  init {
    group = "People"
    description = "Prints list of solutions for a given problem"
  }

  @get:Input
  abstract val problem: Property<ProblemsHandler>

  @TaskAction fun action() {
    val problem = problem.get()

    val msg = buildString {
      appendLine(problem.name.capitalize())
      appendLine(problem.description.get())
      appendLine()
      appendLine("Solutions:")
      problem.solutions.sortedBy { it.rank.get() }.forEachIndexed { i, sol ->
        appendLine("${i + 1}. ${sol.name}")
        appendLine("   ${sol.description.get()}")
        appendLine("   ${sol.action.get()}")
      }
    }

    logger.quiet(msg)
  }
}

我们可以很容易地看到我们的新插件注册的所有任务:

$ ./gradlew app:tasks --group people -q

------------------------------------------------------------
Tasks runnable from project ':app'
------------------------------------------------------------

People tasks
------------
listSolutionsForClimateChange - Prints list of solutions for a given problem

在我们的插件中,我们使用 thePeople.problems.all(Action) 来响应用户提供的配置。 all(Action) 对给定集合的所有元素以及可能添加的所有未来元素执行提供的操作;从这个意义上说,它是懒惰的。对我们来说,它很有用,因为插件的 apply() 方法会在插件应用时立即运行(在 plugins 块中),这意味着用户数据还不能用于做出反应。 all() 优雅地解决了这个问题,而不需要使用 afterEvaluate。

在我们的problems.all 块中,我们注册一个任务——每个problem一个任务——并通过在 Provider 上将其一个输入设置为给定的 ProblemHandler 来配置该任务。这是完全可序列化的,有效的@Input 属性也是如此,并且与实验配置缓存兼容。

我们的任务定义很简单。它是一个抽象类,让我们可以使用托管类型(我们的 @Input 抽象验证问题),并且有一个简单的操作。这里最重要的是记住在各种 Provider 实例上调用 get(),否则我们会得到像属性“description$fancy_plugin”这样的有趣输出。

最后,让我们像这样运行其中一个生成的任务:

$ ./gradlew app:listSolutionsForClimateChange
Configuration cache is an incubating feature.
Calculating task graph as configuration cache cannot be reused because file 'app/build.gradle' has changed.

> Task :app:listSolutionsForclimateChange
ClimateChange
There is no question of cost, because the cost of doing nothing is everything.

Solutions:
1. cleanEnergy
   We cannot burn any more fossil energy
   Replace all fossil sources with clean solutions like wind, solar, and geothermal
2. massTransit
   Single-occupant vehicles are a major source of carbon pollution
   Increase density in urban environments and build free public transit for all
3. stopEatingAnimals
   Animal agriculture is one of the top contributors to carbon pollution
   Most people can thrive on a plant-based diet and do not need animal protein, and could make such a choice with immediate effect
4. antiRacism
   People of Western European descent ('white people') have been the primary beneficiaries of burning fossil carbon
   White people should should bear the responsibility of paying for climate change mitigation
5. seizeGlobalCapital
   The costs of climate change are inequitably distributed
   The costs of climate change mitigation should be born primarily by the wealthiest
6. lastResort
   If the rich and the powerful refuse to get out of the way of legislative reforms of the system killing us all, there is, unfortunately, always a last resort
   It starts with 'g' and rhymes with 'poutine'