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

structuring builds 4

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

複合建置讓您可以

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

  • 將大型多專案建置分解成較小、更孤立的區塊,可視需要獨立或一起處理。

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

定義複合建置

下列範例示範如何將兩個通常個別開發的 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 會檢查包含建置中專案的群組和名稱,並將任何符合 ${project.group}:${project.name} 的外部相依性取代為專案相依性。

預設情況下,主要建置沒有註冊取代。

若要讓 ${project.group}:${project.name} 可以處理主要建置的(子)專案,您可以透過自包含主要建置來告訴 Gradle 將主要建置視為包含的建置:includeBuild(".")

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

例如,稱為 anonymous-library 的單一專案建置會產生 Java 公用程式函式庫,但不會宣告群組屬性的值

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 複合建置會將任何對 org.sample:number-utils 的相依性取代為對 anonymous-library 根專案的相依性。

停用設定的包含建置取代

如果您需要解析已發佈的模組版本,而該版本也可用於包含建置的一部分,您可以在已解析的設定的ResolutionStrategy上停用包含建置取代規則。這是必要的,因為這些規則會在建置中全球套用,而 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 呼叫之間共用共用複合建置的專案鎖定,以防止同時執行。