Skip to content

Latest commit

 

History

History
301 lines (243 loc) · 18 KB

20170312_gradle.rule.source.plugin.md

File metadata and controls

301 lines (243 loc) · 18 KB

Gradle RuleSource Plugin 筆記

如果你因為看了 Part VI. The Software model :: Rule based model configuration 而覺得感到迷惘,這是相當正常的事。因為它實在太簡短,無法讓人感受到新 Model 方式的組態設定的威力。Rule based configuration 其實出來有一陣子了,在 2015 年的 Gradle Summit 有場 workshop 在教你怎麼使用它:

在上半場講主要概念,下半場帶 lab 穿插實際上它怎麼使用,對照 投影片 算是蠻清楚的介紹,看完這場 workshop 會對於文件中的內容比較有概念。如果你對過去怎麼實作與為什麼要用新方法比較有興趣,還有另一場主題演講 The New Gradle Model 可以觀看。

為什麼有這份筆記

主要的動機來自於 試著寫一個 plugin 用來展示 Android Plugin 的 ProductFlavor 使用時,能將共用的 class 放在 src/main 路徑下的可能性。其中就使用到相關的 API,但似乎沒有比較多討論的資料,想想是時候該寫份筆記了。

筆記內容完全以濃縮影片的重點為主,完全沒有新的知識,如果你已經看懂影片要傳達的東西,那就可以跳過筆記本身囉。

為什麼需要有新 API

Gradle 有很多方法可以實作 Plugin 間的資料讀取,但在多個 Plugin 間互相合作時並沒有標準的方法『保證』所有事情發生的時間符合期待。所以,有時撰寫 Plugin 變成在玩一個『搶最後一名』的遊戲:

如文件中的 Project Evaluation 例子

allprojects {
    afterEvaluate { project ->
        if (project.hasTests) {
            println "Adding test task to $project"
            project.task('test') {
                doLast {
                    println "Running tests for $project"
                }
            }
        }
    }
}

對於一些『增強』其他 Plugin 功能的 Plugin 都希望它自己的邏輯能發生在『宿主』之後。這件事有時候有點難保證,因為所有的狀態可能會被重新計算,如果他沒有確保『我終於搶到最後一名』了,那麼會有些 side effect 會出現。像是 Plugin 的行為不如預期,糟一點就噴 Exception 然後壞掉。

即使不使用 afterEvaluate 你的 Plugin 也無法保證相依的『狀態』(通常是其它 Plugin 的內部狀態,實務上來說就是 extension 變數內的東西)不再變更,這同樣會存在著 side effect。它可能會在 Closure 內被 lazy execution,這件事要在 Gradle 執行到那一段時才會真的知道它發生,這件事實在太突然會無法事件掌握所有『狀態』的相依關係,就是問題所在。

新 API 的解法

過去的狀態無法追蹤,是因為我們沒有明確告知 Gradle 這是需要管理的(加上 Groovy Object 實在太自由自在),所以得讓 Gradle 以某種型式知道我們有些狀態是需要管理。

在新的 Rule Based Configuration API 裡選用了 ModelRule 作為核心概念:

  • Model 即為狀態,在 runtime 時它是物件。實作上它分成 Managed 與 Unmanaged,差別在於 Gradle 對它的控制權與『承諾』的高低。Managed 的 Model 必需是 interfaceabstract class 這樣 Gradle 自動提供它實作控制存取權限,而 Unmanaged 則是一個實體的 class,實作由開發者自己決定。
  • Rule 則是針對 Model 動作的邏輯,它可以新增 Model 或修改 Model 內部的元素。

直接看文件上的例子 Example 68.1. applying a rule source plugin

@Managed
interface Person {
  void setFirstName(String name)
  String getFirstName()

  void setLastName(String name)
  String getLastName()
}

以上是個 Model,在這裡 Plugin 它只『談論』有唯一的一個 Model Person,它就是這個 Plugin 裡主要的狀態,而操作 Model 的 Plugin 需要『直接』繼承自 RuleSource

class PersonRules extends RuleSource {
  @Model void person(Person p) {}

  //Create a rule that modifies a Person and takes no other inputs
  @Mutate void setFirstName(Person p) {
    p.firstName = "John"
  }

  //Create a rule that modifies a ModelMap<Task> and takes as input a Person
  @Mutate void createHelloTask(ModelMap<Task> tasks, Person p) {
    tasks.create("hello") {
      doLast {
        println "Hello $p.firstName $p.lastName!"
      }
    }
  }
}

apply plugin: PersonRules

Model 的建立透過 creation rule 的 Annotation @Model 達成,因為它是 Managed Model 所以不需要有實作:

@Model void person(Person p) {}

假設它現在是 Unmanaged Model 你可能會寫成這樣子:

@Model Person person() {
    return new Person()
}

你的 Plugin 建立好 Model 後,Gradle 會去掃 Plugin 中任何 RuleSource 的類別,它會先註冊它起來(但不會實體化它,一切是 lazy 的)。那什麼時候會真的實體化為物件呢?當有人相依它時才會。直接看下面這段 code:

  //Create a rule that modifies a ModelMap<Task> and takes as input a Person
  @Mutate void createHelloTask(ModelMap<Task> tasks, Person p) {
    tasks.create("hello") {
      doLast {
        println "Hello $p.firstName $p.lastName!"
      }
    }
  }

@Mutate 是一種 mutation rule(簡單說就是操作 Model 的 Rule,有別於 creation rule 它至少要有 1 個參數)。它是個簡單的 method,參數的位置是有意義的!

  • 第 1 個參數為 subject,實際上就是某個 Plugin 建立出來的 Model(有別於 input 這是可以修改的)
  • 第 2 個開始為 input,它也必需是 Model,但 Gradle 保證它都是完成 evaluate 並且 immutable(你修改它會噴 Exception)

Gradle 就是透過 mutation rule 的參數來計算『狀態』間的『相依』關係,並保證它會合理的時間被操作。你不需要自己再去呼叫 lifecycle 相關的 DAG 操作。

實務上怎麼用

看完了 API 與簡短地說明後,實際上要切入仍有一點難度。最大的問題是:

我想要修改變人的 Plugin 行為,該怎麼開始。有哪些 Model 能用呢?

  1. 透常你有興趣的是 Plugin 某些 Task 的狀態修改,大部分的情況你只需要認識 ModelMap<Task> 這個 Model 就好
  2. 除了直接操作 Task 越來越多的新 Plugin 有宣告它自己的 Model,並由於這 API 仍有點新,也許得查一下 Source Code。

由先前的介紹你可以知道 @Model 是 creation rule,所以你查 Source Code 時可以用這個 Annotation 作為關鍵字去查詢,例如:

qty:android-platform-tools-base qrtt1$ git remote -v
origin	https://android.googlesource.com/platform/tools/base (fetch)
origin	https://android.googlesource.com/platform/tools/base (push)
qty:android-platform-tools-base qrtt1$ grep -r @Model *
build-system/gradle-experimental/src/main/groovy/com/android/build/gradle/model/AndroidComponentModelPlugin.java:        @Model("android")
build-system/gradle-experimental/src/main/groovy/com/android/build/gradle/model/AndroidComponentModelPlugin.java:        @Model
build-system/gradle-experimental/src/main/groovy/com/android/build/gradle/model/AndroidComponentModelPlugin.java:        @Model
build-system/gradle-experimental/src/main/groovy/com/android/build/gradle/model/AppComponentModelPlugin.java:        @Model(IS_APPLICATION)
build-system/gradle-experimental/src/main/groovy/com/android/build/gradle/model/AppComponentModelPlugin.java:        @Model(TASK_MANAGER)
build-system/gradle-experimental/src/main/groovy/com/android/build/gradle/model/AppComponentModelPlugin.java:        @Model
build-system/gradle-experimental/src/main/groovy/com/android/build/gradle/model/BaseComponentModelPlugin.java:        @Model(EXTRA_MODEL_INFO)
build-system/gradle-experimental/src/main/groovy/com/android/build/gradle/model/BaseComponentModelPlugin.java:        @Model
build-system/gradle-experimental/src/main/groovy/com/android/build/gradle/model/BaseComponentModelPlugin.java:        @Model(ANDROID_BUILDER)
build-system/gradle-experimental/src/main/groovy/com/android/build/gradle/model/BaseComponentModelPlugin.java:        @Model(ANDROID_CONFIG_ADAPTOR)
build-system/gradle-experimental/src/main/groovy/com/android/build/gradle/model/LibraryComponentModelPlugin.java:        @Model(IS_APPLICATION)
build-system/gradle-experimental/src/main/groovy/com/android/build/gradle/model/LibraryComponentModelPlugin.java:        @Model(TASK_MANAGER)
build-system/gradle-experimental/src/main/groovy/com/android/build/gradle/model/LibraryComponentModelPlugin.java:        @Model
build-system/gradle-experimental/src/main/groovy/com/android/build/gradle/model/NdkComponentModelPlugin.java:        @Model(ModelConstants.NDK_HANDLER)

其實不查 Source Code 也有 Model Reporter 可以用,但由於這功能很新得看 Gradle 實作的完整度了,以 android project 用的 Gradle 版本 (2.3) 來看:

qty:MultipleFlavor qrtt1$ ./gradlew model
Starting a Gradle Daemon (subsequent builds will be faster)
NDK is missing a "platforms" directory.
If you are using NDK, verify the ndk.dir is set to a valid NDK directory.  It is currently set to /Users/qrtt1/Library/Android/sdk/ndk-bundle.
If you are not using NDK, unset the NDK variable from ANDROID_NDK_HOME or local.properties to remove this warning.

:model

------------------------------------------------------------
Root project
------------------------------------------------------------

+ tasks
      | Type:           org.gradle.model.ModelMap<org.gradle.api.Task>
      | Creator:        Project.<init>.tasks()
    + buildEnvironment
          | Type:       org.gradle.api.tasks.diagnostics.BuildEnvironmentReportTask
          | Value:      task ':buildEnvironment'
          | Creator:    tasks.addPlaceholderAction(buildEnvironment)
          | Rules:
             ⤷ copyToTaskContainer
    + clean
          | Type:       org.gradle.api.tasks.Delete
          | Value:      task ':clean'
          | Creator:    Project.<init>.tasks.clean()
          | Rules:
             ⤷ copyToTaskContainer
    + components
          | Type:       org.gradle.api.reporting.components.ComponentReport
          | Value:      task ':components'
          | Creator:    tasks.addPlaceholderAction(components)
          | Rules:
             ⤷ copyToTaskContainer
    + dependencies
          | Type:       org.gradle.api.tasks.diagnostics.DependencyReportTask
          | Value:      task ':dependencies'
          | Creator:    tasks.addPlaceholderAction(dependencies)
          | Rules:
             ⤷ copyToTaskContainer
    + dependencyInsight
          | Type:       org.gradle.api.tasks.diagnostics.DependencyInsightReportTask
          | Value:      task ':dependencyInsight'
          | Creator:    tasks.addPlaceholderAction(dependencyInsight)
          | Rules:
             ⤷ HelpTasksPlugin.Rules#addDefaultDependenciesReportConfiguration(DependencyInsightReportTask, ServiceRegistry)
             ⤷ copyToTaskContainer
    + dependentComponents
          | Type:       org.gradle.api.reporting.dependents.DependentComponentsReport
          | Value:      task ':dependentComponents'
          | Creator:    tasks.addPlaceholderAction(dependentComponents)
          | Rules:
             ⤷ copyToTaskContainer
    + help
          | Type:       org.gradle.configuration.Help
          | Value:      task ':help'
          | Creator:    tasks.addPlaceholderAction(help)
          | Rules:
             ⤷ copyToTaskContainer
    + init
          | Type:       org.gradle.buildinit.tasks.InitBuild
          | Value:      task ':init'
          | Creator:    tasks.addPlaceholderAction(init)
          | Rules:
             ⤷ copyToTaskContainer
    + model
          | Type:       org.gradle.api.reporting.model.ModelReport
          | Value:      task ':model'
          | Creator:    tasks.addPlaceholderAction(model)
          | Rules:
             ⤷ copyToTaskContainer
    + projects
          | Type:       org.gradle.api.tasks.diagnostics.ProjectReportTask
          | Value:      task ':projects'
          | Creator:    tasks.addPlaceholderAction(projects)
          | Rules:
             ⤷ copyToTaskContainer
    + properties
          | Type:       org.gradle.api.tasks.diagnostics.PropertyReportTask
          | Value:      task ':properties'
          | Creator:    tasks.addPlaceholderAction(properties)
          | Rules:
             ⤷ copyToTaskContainer
    + tasks
          | Type:       org.gradle.api.tasks.diagnostics.TaskReportTask
          | Value:      task ':tasks'
          | Creator:    tasks.addPlaceholderAction(tasks)
          | Rules:
             ⤷ copyToTaskContainer
    + wrapper
          | Type:       org.gradle.api.tasks.wrapper.Wrapper
          | Value:      task ':wrapper'
          | Creator:    tasks.addPlaceholderAction(wrapper)
          | Rules:
             ⤷ copyToTaskContainer

BUILD SUCCESSFUL

Total time: 9.71 secs

有沒有看到熟悉的 org.gradle.model.ModelMap<org.gradle.api.Task> 了呢?更新版的 Gradle 會回報地更加詳細。

與 extension 互動

在舊的實作過渡到新的 Model 之類,我們仍需要跟 Plugin Extension 互動,以先前實作過的 Plugin 為例

    @Model
    public LambdaConfigExtension lambdaConfig(ExtensionContainer container) {
        return container.getByType(LambdaConfigExtension.class)
    }

ExtensionContainer 是一個隱藏的 Model 它並沒有出現在 Report 內(不確定是不是刻意隱藏的),有些 legacy 的東西 Gradle 團隊不太推薦使用,但在這過渡時期我們需要它,至少它仍是合法的 Model。我不記得是在哪個 Source Code 看到這用法,但你可以由 Gradle 的 Source Code 查到相關的 Model,不過不是用 @Model 查,它們是用內部 API 註冊的:

qty:src qrtt1$ grep -r 'ModelReference.of' *
core/org/gradle/api/internal/project/AbstractProject.java:                ModelCreators.bridgedInstance(ModelReference.of("serviceRegistry", ServiceRegistry.class), services)
core/org/gradle/api/internal/project/AbstractProject.java:                ModelCreators.unmanagedInstance(ModelReference.of("buildDir", File.class), new Factory<File>() {
core/org/gradle/api/internal/project/AbstractProject.java:                ModelCreators.bridgedInstance(ModelReference.of("projectIdentifier", ProjectIdentifier.class), this)
core/org/gradle/api/internal/project/AbstractProject.java:                ModelCreators.unmanagedInstance(ModelReference.of("extensions", ExtensionContainer.class), new Factory<ExtensionContainer>() {
core/org/gradle/model/collection/internal/PolymorphicDomainObjectContainerModelProjection.java:                        ModelReference.of(modelPath, containerType),
model-core/org/gradle/model/internal/inspect/DefaultMethodRuleDefinition.java:        return ModelReference.of(path == null ? null : validPath(path), cast, String.format("parameter %s", i + 1));
model-core/org/gradle/model/internal/inspect/ManagedModelCreationRuleDefinitionHandler.java:        return ModelCreators.of(ModelReference.of(modelPath, managedType), transformer)
model-core/org/gradle/model/internal/inspect/ManagedModelInitializer.java:        return ModelCreators.of(ModelReference.of(path, schema.getType()), new ManagedModelInitializer<T>(descriptor, schema, modelInstantiator, schemaStore, proxyFactory, initializer))
model-core/org/gradle/model/internal/inspect/UnmanagedModelCreationRuleDefinitionHandler.java:        modelRegistry.create(ModelCreators.of(ModelReference.of(ModelPath.path(modelName), returnType), transformer)
platform-base/org/gradle/language/base/plugins/ComponentModelBasePlugin.java:                ModelCreators.bridgedInstance(ModelReference.of("components", DefaultComponentSpecContainer.class), components)
platform-base/org/gradle/language/base/plugins/LanguageBasePlugin.java:                ModelCreators.bridgedInstance(ModelReference.of("binaries", BinaryContainer.class), binaries)
platform-base/org/gradle/language/base/plugins/LanguageBasePlugin.java:                        .inputs(Collections.singletonList(ModelReference.of(ExtensionContainer.class)))
platform-base/org/gradle/platform/base/internal/registry/BinaryTasksRuleDefinitionHandler.java:            final ModelReference<TaskContainer> tasks = ModelReference.of(ModelPath.path("tasks"), new ModelType<TaskContainer>() {
platform-base/org/gradle/platform/base/internal/registry/BinaryTasksRuleDefinitionHandler.java:            super(subject, binaryType, ruleDefinition, ModelReference.of("binaries", BinaryContainer.class));
platform-base/org/gradle/platform/base/internal/registry/ComponentBinariesRuleDefinitionHandler.java:            final ModelReference<BinaryContainer> subject = ModelReference.of(ModelPath.path("binaries"), new ModelType<BinaryContainer>() {
platform-base/org/gradle/platform/base/internal/registry/ComponentBinariesRuleDefinitionHandler.java:            super(subject, componentType, ruleDefinition, ModelReference.of(ComponentSpecContainer.class));
platform-base/org/gradle/platform/base/internal/registry/ComponentModelRuleDefinitionHandler.java:            subject = ModelReference.of("extensions", ExtensionContainer.class);
platform-base/org/gradle/platform/base/internal/registry/ComponentModelRuleDefinitionHandler.java:            inputs = ImmutableList.<ModelReference<?>>of(ModelReference.of(ProjectIdentifier.class));
qty:src qrtt1$

在 gradle 2.3 下有這些預設的 model 可以使用,越新的版本會越多,並能觀察到越來越多的 Plugin 含有 @Model 建立 Model。