Gradleプラグインと拡張モジュール


このプロジェクトのすべてのコードはGithub . ( Github drop ICE .)
最近では、誰もが自分の時間を無駄にしていないので、GradleプラグインでカスタムネストされたDSLSを作成する方法を知らないかもしれません.驚くほど役に立つが、これは、より重要な、非常に美的です.
// app/build.gradle
theState {
  theDeepState {
    theDeepestState {
      undermine 'the will of the people'
    }
  }
}
IDE KINDAが良いol ' Groovy DSLでさえ、ほとんどの型ヒントを提供することを証明します

IDEがGroovyバージョンよりも優れていることを証明する

Soon after I took those screencaps, I upgraded this project's version of Gradle from 7.1.1 to 7.2, and my IDE (IntelliJ IDEA Ultimate) got confused and no longer gives me DSL hints for Groovy scripts. ¯\_(ツ)_/¯


我々が人々の意志を損ねたいと思う理由を脇に残してください.どうやってやるの?

これが誰


This is for anyone looking for non-trivial examples for one of the fundamental building blocks of Gradle plugin design. I wouldn't go so far as to say they're production-ready (sure as hell I'm not going to be writing any tests!), but I am currently using techniques like these for building a 2+ million LOC application, so… 1

秘密の官僚組織のためのドメイン特有の言語


私たちは、拡張そのものを見ることから始めます.そして、それがどのように構成されて、使用されるか、そして最終的にどのように宣言して、それを構築するかについて、働いています.
// 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 . そのような慣例を持つことは、大きなコードベースで移動することをより簡単にします.
  • これらのタイプのすべてを、variety of services 同様 ObjectFactory ), あなたが供給する完全に任意のオブジェクトと同様に.ちょうど覚えて@Inject そのコンストラクタ!
  • グラックル領域に深く何かのために、Gradle APIはあなたのために仕事をしましょう.Groovy閉鎖または受信機でKotlinラムダで創造的になるようにしないでくださいObjectFactoryAction<T> インターフェイス.私は、これについてもう少し詳しく説明します.
  • この例では、ハンドラを直接公開することもできますし、関数を介して公開することもできます.これにより、ユーザはドット表記とDSLのような構文をCurlyブレースで使用することができます.
  • 拡張子のインスタンス化


    簡単な内部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'
    }
    
    簡単なpeasy.プラグインを適用し、拡張モジュールを構成します.今、それらについて話す良い時間ですAction<T> DSL構文を有効にする関数.思い出させるものとして、以下のようなものがあります.
    import org.gradle.api.Action
    
    fun theDeepState(action: Action<TheDeepStateHandler>) {
      action.execute(theDeepState)
    }
    
    I keep including the import statements in these code snippets because it's important to note the precise API we're using here — the org.gradle.api API!2 Gradleはこれらの型に特別な扱いをします.実行時に、GradleはビルドコードをASM ), メソッドシグネチャtheDeepState(action: Action<T>) 効果的になるtheDeepState(action: T.() -> Unit) . 実際には、両方を得ると言うのは正確です.私のGroovy DSLスクリプトでは、私も使用できますit. 好きなら自由に.
    さて、IDEがなぜこのタイプのヒントを持っているのかを知っています.標準のSAMインターフェースを指定するソースコードを見ます.それは、オンザフライで提供されるレシーバーによるラムダを見ません.
    It's unclear why it looks better with the Kotlin DSL. 3 あなたが探索するならばgenerated type-safe accessors , また、Action<T> . 私は、我々が決して知らないと思います.

    ユーザが提供する設定の利用:誰が今日抑圧するべきか?


    我々のカスタムDSLで我々のユーザーによって提供される情報を使用するために現在拡大された我々のプラグイン定義に戻りましょう.
    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 task .
    プログラムを実行し、何が起こるかを見ましょう.
    $ ./gradlew -q app:run
    Now undermining: the will of the people
    
    弾圧は達成!

    ドメインオブジェクトコンテナ


    もしAndroid開発者なら、Gradleのこのビットに慣れています.
    android {
      buildTypes {
        release { ... }
        debug { ... }
        myCustomBuildType { ... }
      }
    }
    
    これらのビルドタイプはどこから来ますか?入れ子のDSLオブジェクトを生成して使用する方法を知っていますが、これらの値はユーザーが提供しています!上記のKotlin DSL版を見ると、状況はもう少し明確になります.
    android {
      buildTypes {
        getByName("release") { ... }
        getByName("debug") { ... }
        create("myCustomBuildType") { ... }
      }
    }
    
    buildTypes は、NamedDomainObjectContainer<BuildType> . Groovy風味のGradleは変換する構文上の砂糖を持っていますdebug {} into getByName("debug") {} OR create("debug") {} を返します.Kotlinでは、あなたは明示的でなければなりません.これもBTW、私はどのようにデフォルトのインスタンスは“リリース”という名前のないことを学んだsigningConfig .
    我々は今、粗い用語で、何を知っている NamedDomainObjectContainer そうです.どうやって作るの?どのように、我々は1から新しいインスタンスを得ますか?どうやって使うの?どのように我々のユーザーはそれを使用しますか?

    ドメインオブジェクトコンテナの使用


    この次の最終的な例については、それを切り替えましょう.圧迫は退屈だどのように我々は代わりに助けることができますか?
    新しい拡張子から始めましょう.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()
      }
    }
    
    我々が続く前に、パターンのいくつかについて話しましょう.
    最初に、aにあるはずである型に注意してくださいNamedDomainObjectContainer すべて実装Named インターフェイス.これは厳密に必要ではありませんが、getName(): String 関数、あるいは命名されたドメインオブジェクトコンテナに入ることはできません.
    次に、そのようなコンテナをメソッドで作成しますObjectFactory.domainObjectContainer(Class<T>) .
    上記の最終的な興味深いパターンはこうです.
    fun description(description: String) {
      this.description.set(action)
      this.description.disallowChanges()
    }
    
    ASdescriptionProperty<String> , 私はそれらの価値を保つのを好むinternal 関数を通してそれらを公開します.ユーザーは、良いdslのようなdescription 'my description ( Groovyで)description("my description") (皇太子)これらのフィールドをカプセル化することも、私に呼び出しのような余分なものをすることを許しますdisallowChanges() , 私が考えることは、違反を防ぐのに重要ですprinciple of least astonishment . それがなければ、ユーザはdescription() 複数の場所から繰り返して、データが本当にどこから来ていたかを伝えるのは難しいでしょう.これを行うと、誰かが一度以上メソッドを呼び出すしようとすると、ビルドが失敗します.
    続けましょう.どのように、このDSLは「行動において」見えますか?
    // 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
            }
          }
        }
      }
    }
    
    私は、我々がモデル化しようとしているドメインの複雑さを考えると、それはかなり読みやすいと思います.
    しかし、今どのように我々は、プラグインに反応するのですか?いつものように、私は学習が最良の例で行われると思いますので、どのように新しいプラグインを見て、それをまとめてみましょう.ThePluginOfThePeople , このユーザーが提供するデータに基づいてタスクを設定します.
    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<T>) ユーザーが提供する設定に反応する.all(Action<T>) 指定したコレクションのすべての要素に対する指定されたアクションを実行します.この意味では怠け者だ.私たちにとっては、プラグインのapply() プラグインが適用されるとすぐにメソッドが実行されますplugins これは、ユーザデータがまだ利用できないことを意味します.all() 優雅に、この問題を解決するには、と言う.afterEvaluate .
    中でproblems.all ブロック、1つのタスクを登録-問題ごとに1つのタスク-とそのタスクを構成する1つの入力を指定してProblemHandler , でProvider<ProblemHandler> . これは完全にシリアル化可能であり、有効です@Input プロパティ、および実験と互換性があるconfiguration cache .
    我々のタスク定義は簡単です.それは抽象的なクラスですmanaged types 我々@Input abstract val problem ), そして、単純な行動をします.ここで最大のフットガンはコールを覚えているget() 様々にProvider<String> インスタンス、その他のような面白い出力を取得しますproperty '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'
    

    ラッピング


    このポストでは、Gradleを使用して、複雑なドメインを入れ子になったドメイン固有の言語、またはDSLでモデル化する方法を学びましたNamedDomainObjectContainer . 私は、あなたが簡単にするためにこのポストから残されたスクリプトとプロジェクトレイアウト決定を構築することを含むGithubの完全なサンプルを調査するのを奨励します.

    自律アプリケーション / GradleネストドDSL



    エンドノート


    1私のひどい技術のために解雇されることを嘆いているこのスペースを見てください.up
    2私は非常に深くこれらのパッケージを探索することをお勧めします.up
    3 Jetbrainsは、Kotlin言語の作成者です.up