复合构建是指包含其他构建的构建。

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 文件定义复合构建

可以通过在 settings.gradle(.kts) 文件中使用 Settings.includeBuild(java.lang.Object) 声明被包含构建来使上述安排持久化。

Settings 文件可以同时用于添加子项目和被包含构建。

通过位置添加被包含构建

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 插件的构建是被包含构建的一种特殊情况。

这些构建应该在 Settings 文件的 pluginManagement {} 块内使用 includeBuild 语句包含进来。

使用这种机制,被包含构建还可以贡献一个可以在 Settings 文件本身中应用的 Settings 插件。

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 Module 或 Eclipse Project 包含进来。配置了源依赖项,提供了跨构建导航和重构。

声明由被包含构建替代的依赖项

默认情况下,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-library(其中 undefinedproject.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 根项目的依赖项。

为某个配置停用被包含构建替代项

如果您需要解析同时作为被包含构建的一部分提供的模块的已发布版本,您可以在解析的 Configuration 的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 调用之间共享共享复合构建的项目锁,以防止并发执行。