如果你因為看了 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,但似乎沒有比較多討論的資料,想想是時候該寫份筆記了。
筆記內容完全以濃縮影片的重點為主,完全沒有新的知識,如果你已經看懂影片要傳達的東西,那就可以跳過筆記本身囉。
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 執行到那一段時才會真的知道它發生,這件事實在太突然會無法事件掌握所有『狀態』的相依關係,就是問題所在。
過去的狀態無法追蹤,是因為我們沒有明確告知 Gradle 這是需要管理的(加上 Groovy Object 實在太自由自在),所以得讓 Gradle 以某種型式知道我們有些狀態是需要管理。
在新的 Rule Based Configuration
API 裡選用了 Model
與 Rule
作為核心概念:
- Model 即為狀態,在 runtime 時它是物件。實作上它分成 Managed 與 Unmanaged,差別在於 Gradle 對它的控制權與『承諾』的高低。Managed 的 Model 必需是
interface
或abstract 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 能用呢?
- 透常你有興趣的是 Plugin 某些 Task 的狀態修改,大部分的情況你只需要認識
ModelMap<Task>
這個 Model 就好 - 除了直接操作 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 會回報地更加詳細。
在舊的實作過渡到新的 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。