複合建置是一種包含其他建置的建置。

structuring builds 4

複合建置類似於 Gradle 多專案建置,不同之處在於複合建置包含的是整個 builds,而不是 subprojects

複合建置允許您

  • 組合通常獨立開發的建置,例如,當嘗試修復應用程式使用的函式庫中的錯誤時。

  • 將大型多專案建置分解為更小、更隔離的區塊,這些區塊可以根據需要獨立或協同工作。

包含在複合建置中的建置稱為包含建置。包含建置不與複合建置或其他包含建置共享任何配置。每個包含建置都是獨立配置和執行的。

定義複合建置

以下範例示範了如何將兩個通常獨立開發的 Gradle 建置組合到一個複合建置中。

my-composite
├── gradle
├── gradlew
├── settings.gradle.kts
├── build.gradle.kts
├── my-app
│   ├── settings.gradle.kts
│   └── app
│       ├── build.gradle.kts
│       └── src/main/java/org/sample/my-app/Main.java
└── my-utils
    ├── settings.gradle.kts
    ├── number-utils
    │   ├── build.gradle.kts
    │   └── src/main/java/org/sample/numberutils/Numbers.java
    └── string-utils
        ├── build.gradle.kts
        └── src/main/java/org/sample/stringutils/Strings.java

my-utils 多專案建置產生兩個 Java 函式庫,number-utilsstring-utilsmy-app 建置產生一個使用這些函式庫中函式的可執行檔。

my-app 建置不直接依賴於 my-utils。相反地,它宣告對 my-utils 產生的函式庫的二進制相依性

my-app/app/build.gradle.kts
plugins {
    id("application")
}

application {
    mainClass = "org.sample.myapp.Main"
}

dependencies {
    implementation("org.sample:number-utils:1.0")
    implementation("org.sample:string-utils:1.0")
}
my-app/app/build.gradle
plugins {
    id 'application'
}

application {
    mainClass = 'org.sample.myapp.Main'
}

dependencies {
    implementation 'org.sample:number-utils:1.0'
    implementation 'org.sample:string-utils:1.0'
}

透過 --include-build 定義複合建置

--include-build 命令列引數將執行的建置轉換為複合建置,將包含建置中的相依性取代到執行的建置中。

例如,從 my-app 執行的 ./gradlew run --include-build ../my-utils 的輸出

$ ./gradlew --include-build ../my-utils run
> Task :app:processResources NO-SOURCE
> Task :my-utils:string-utils:compileJava
> Task :my-utils:string-utils:processResources NO-SOURCE
> Task :my-utils:string-utils:classes
> Task :my-utils:string-utils:jar
> Task :my-utils:number-utils:compileJava
> Task :my-utils:number-utils:processResources NO-SOURCE
> Task :my-utils:number-utils:classes
> Task :my-utils:number-utils:jar
> Task :app:compileJava
> Task :app:classes

> Task :app:run
The answer is 42


BUILD SUCCESSFUL in 0s
6 actionable tasks: 6 executed

透過設定檔定義複合建置

可以使用 Settings.includeBuild(java.lang.Object)settings.gradle(.kts) 檔案中宣告包含建置,使上述配置持久化。

設定檔可用於同時新增子專案和包含建置。

包含建置是依位置新增的

settings.gradle.kts
includeBuild("my-utils")

在範例中,settings.gradle(.kts) 檔案組合了原本獨立的建置

settings.gradle.kts
rootProject.name = "my-composite"

includeBuild("my-app")
includeBuild("my-utils")
settings.gradle
rootProject.name = 'my-composite'

includeBuild 'my-app'
includeBuild 'my-utils'

若要從 my-composite 執行 my-app 建置中的 run 任務,請執行 ./gradlew my-app:app:run

您可以選擇在 my-composite 中定義一個依賴於 my-app:app:runrun 任務,以便您可以執行 ./gradlew run

build.gradle.kts
tasks.register("run") {
    dependsOn(gradle.includedBuild("my-app").task(":app:run"))
}
build.gradle
tasks.register('run') {
    dependsOn gradle.includedBuild('my-app').task(':app:run')
}

包含定義 Gradle 外掛的建置

包含建置的一個特殊情況是定義 Gradle 外掛的建置。

這些建置應使用設定檔的 pluginManagement {} 區塊內的 includeBuild 語句包含。

使用此機制,包含建置也可以貢獻一個設定外掛,該外掛可以應用於設定檔本身

settings.gradle.kts
pluginManagement {
    includeBuild("../url-verifier-plugin")
}
settings.gradle
pluginManagement {
    includeBuild '../url-verifier-plugin'
}

包含建置的限制

大多數建置都可以包含在複合建置中,包括其他複合建置。但有一些限制。

在常規建置中,Gradle 確保每個專案都有唯一的專案路徑。這使得專案可識別和可定址,而不會發生衝突。

在複合建置中,Gradle 會為包含建置中的每個專案新增額外的限定條件,以避免專案路徑衝突。用於識別複合建置中專案的完整路徑稱為建置樹狀路徑。它由包含建置的建置路徑和專案的專案路徑組成。

預設情況下,建置路徑和專案路徑是從磁碟上的目錄名稱和結構派生的。由於包含建置可以位於磁碟上的任何位置,因此它們的建置路徑由包含目錄的名稱決定。這有時可能導致衝突。

總之,包含建置必須滿足以下要求

  • 每個包含建置都必須具有唯一的建置路徑。

  • 每個包含建置路徑都不得與主建置的任何專案路徑衝突。

這些條件保證即使在複合建置中,每個專案也可以被唯一識別。

如果發生衝突,解決衝突的方法是變更包含建置的建置名稱

settings.gradle.kts
includeBuild("some-included-build") {
    name = "other-name"
}

當複合建置包含在另一個複合建置中時,兩個建置都具有相同的父級。換句話說,巢狀複合建置結構會被展平。

與複合建置互動

與複合建置互動通常與常規多專案建置類似。可以執行任務、執行測試,並且可以將建置匯入到 IDE 中。

執行任務

可以從命令列或 IDE 執行包含建置中的任務,方式與常規多專案建置中的任務相同。執行任務將導致任務相依性被執行,以及從其他包含建置建置相依性成品所需的任務。

您可以使用完整路徑呼叫包含建置中的任務,例如,:included-build-name:project-name:taskName。專案和任務名稱可以縮寫

$ ./gradlew :included-build:subproject-a:compileJava
> Task :included-build:subproject-a:compileJava

$ ./gradlew :i-b:sA:cJ
> Task :included-build:subproject-a:compileJava

若要從命令列排除任務,您需要提供任務的完整路徑。

包含建置任務會自動執行以產生所需的相依性成品,或者包含建置可以宣告對包含建置中任務的相依性

匯入到 IDE

複合建置最有用的功能之一是 IDE 整合。

匯入複合建置允許輕鬆地將來自不同 Gradle 建置的來源一起開發。對於每個包含建置,每個子專案都作為 IntelliJ IDEA 模組或 Eclipse 專案包含。相依性來源已配置,提供跨建置導航和重構。

宣告由包含建置取代的相依性

預設情況下,Gradle 將配置每個包含建置以確定它可以提供的相依性。執行此操作的演算法很簡單。Gradle 將檢查包含建置中專案的 group 和 name,並將專案相依性替換為任何與 ${project.group}:${project.name} 匹配的外部相依性。

預設情況下,不會為主建置註冊取代。

若要使主建置的(子)專案可透過 ${project.group}:${project.name} 定址,您可以告知 Gradle 將主建置視為包含建置,方法是自我包含它:includeBuild(".")

在某些情況下,Gradle 確定的預設取代不足或必須針對特定的複合建置進行更正。在這些情況下,可以明確宣告包含建置的取代。

例如,一個名為 anonymous-library 的單專案建置產生一個 Java 实用程式函式庫,但未宣告 group 屬性的值

build.gradle.kts
plugins {
    java
}
build.gradle
plugins {
    id 'java'
}

當此建置包含在複合建置中時,它將嘗試取代相依性模組 undefined:anonymous-libraryundefinedproject.group 的預設值,而 anonymous-library 是根專案名稱)。顯然,這在複合建置中沒有用。

若要在複合建置中使用未發布的函式庫,您可以明確宣告其提供的取代

settings.gradle.kts
includeBuild("anonymous-library") {
    dependencySubstitution {
        substitute(module("org.sample:number-utils")).using(project(":"))
    }
}
settings.gradle
includeBuild('anonymous-library') {
    dependencySubstitution {
        substitute module('org.sample:number-utils') using project(':')
    }
}

透過此配置,my-app 複合建置將以對 anonymous-library 根專案的相依性取代對 org.sample:number-utils 的任何相依性。

停用配置的包含建置取代

如果您需要解析也作為包含建置一部分提供的模組的已發布版本,則可以在ResolutionStrategy 已解析的 Configuration 上停用包含建置取代規則。這是必要的,因為這些規則是全域應用於建置的,並且 Gradle 預設情況下在解析期間不考慮已發布的版本。

例如,我們建立一個單獨的 publishedRuntimeClasspath 配置,該配置解析為也存在於其中一個本地建置中的模組的已發布版本。這是透過停用全域相依性取代規則來完成的

build.gradle.kts
configurations.create("publishedRuntimeClasspath") {
    resolutionStrategy.useGlobalDependencySubstitutionRules = false

    extendsFrom(configurations.runtimeClasspath.get())
    isCanBeConsumed = false
    attributes.attribute(Usage.USAGE_ATTRIBUTE, objects.named(Usage.JAVA_RUNTIME))
}
build.gradle
configurations.create('publishedRuntimeClasspath') {
    resolutionStrategy.useGlobalDependencySubstitutionRules = false

    extendsFrom(configurations.runtimeClasspath)
    canBeConsumed = false
    attributes.attribute(Usage.USAGE_ATTRIBUTE, objects.named(Usage, Usage.JAVA_RUNTIME))
}

一個用例是比較已發布和本地建置的 JAR 檔案。

必須宣告包含建置取代的情況

許多建置將自動作為包含建置運作,而無需宣告取代。以下是一些需要宣告取代的常見情況

  • archivesBaseName 屬性用於設定已發布成品的名稱時。

  • 當發布了 default 以外的配置時。

  • MavenPom.addFilter() 用於發布與專案名稱不符的成品時。

  • maven-publishivy-publish 外掛用於發布,且發布坐標與 ${project.group}:${project.name} 不符時。

複合建置取代無法運作的情況

即使明確宣告了相依性取代,某些建置在包含在複合建置中時也無法正確運作。此限制是因為取代的專案相依性將始終指向目標專案的 default 配置。任何時候專案的預設配置指定的成品和相依性與發布到儲存庫的內容不符時,複合建置都可能表現出不同的行為。

以下是一些已發布模組元數據可能與專案預設配置不同的情況

  • 當發布了 default 以外的配置時。

  • 當使用 maven-publishivy-publish 外掛時。

  • POMivy.xml 檔案在發布時被調整時。

使用這些功能的建置在包含在複合建置中時無法正常運作。

依賴於包含建置中的任務

雖然包含建置彼此隔離且無法宣告直接相依性,但複合建置可以宣告對其包含建置的任務相依性。包含建置是使用 Gradle.getIncludedBuilds()Gradle.includedBuild(java.lang.String) 存取的,並且任務參考是透過 IncludedBuild.task(java.lang.String) 方法取得的。

使用這些 API,可以宣告對特定包含建置中任務的相依性

build.gradle.kts
tasks.register("run") {
    dependsOn(gradle.includedBuild("my-app").task(":app:run"))
}
build.gradle
tasks.register('run') {
    dependsOn gradle.includedBuild('my-app').task(':app:run')
}

或者,您可以宣告對某些或所有包含建置中具有特定路徑的任務的相依性

build.gradle.kts
tasks.register("publishDeps") {
    dependsOn(gradle.includedBuilds.map { it.task(":publishMavenPublicationToMavenRepository") })
}
build.gradle
tasks.register('publishDeps') {
    dependsOn gradle.includedBuilds*.task(':publishMavenPublicationToMavenRepository')
}

複合建置的限制

目前實作的限制包括

  • 不支援發布內容與專案預設配置不符的包含建置。
    請參閱 複合建置無法運作的情況

  • 如果多個複合建置包含相同的建置,則在平行執行時可能會發生衝突。
    Gradle 不會在 Gradle 調用之間共享共享複合建置的專案鎖定,以防止並行執行。