What is a composite build?

复合构建只是包含其他构建的构建. 在许多方面,复合构建类似于Gradle多项目构建,不同之处在于,它包括完整的builds ,而不是包含单个projects .

复合版本使您能够:

  • combine builds that are usually developed independently, for instance when trying out a bug fix in a library that your application uses

  • 将大型的多项目构建分解为更小,更孤立的块,可以根据需要独立或一起工作

自然包含在复合构建中的构建被称为"包含构建". 包含的构建不与复合构建或其他包含的构建共享任何配置. 每个包含的构建都是独立配置和执行的.

包含的内部版本通过依赖替换与其他内部版本交互. 如果组合中的任何内部版本都具有包含的内部版本可以满足的依赖关系,则该依赖关系将由对包含的内部版本的项目依赖关系代替. 由于依赖于依赖关系替代,因此在构建任务执行图时,复合构建可能会强制配置被更早地解决. 这可能会对整体构建性能产生负面影响,因为这些配置无法并行解决.

默认情况下,Gradle将尝试确定可以由包含的内部版本替代的依赖项. 但是,为了获得更大的灵活性,如果Gradle确定的默认替换不适用于组合,则可以显式声明这些替换. 请参阅声明替代 .

除了通过项目依赖项消耗输出外,复合构建还可以直接在包含的构建中声明任务依赖项. 包含的构建是隔离的,并且不能声明复合构建或其他包含的构建上的任务依赖性. 请参阅根据包含的构建中的任务 .

Defining a composite build

以下示例演示了通常可以单独开发的2个Gradle构建可以组合为复合构建的各种方式. 对于这些示例, my-utils多项目构建会生成2个不同的Java库( number-utilsstring-utils ),而my-app构建会使用这些库中的函数生成可执行文件.

my-app构建不直接依赖于my-utils . 相反,它声明对my-utils生成的库的二进制依赖关系.

例子1. my-app的依赖关系
my-app/build.gradle
plugins {
    id 'java'
    id 'application'
    id 'idea'
}

group "org.sample"
version "1.0"

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

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

repositories {
    jcenter()
}
my-app/build.gradle.kts
plugins {
    java
    application
    idea
}

group = "org.sample"
version = "1.0"

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

dependencies {
    implementation("org.sample:number-utils:1.0")
    implementation("org.sample:string-utils:1.0")
}

repositories {
    jcenter()
}
该示例的代码可以在Gradle的'-all'发行版的samples/compositeBuilds/basic中找到.

Defining a composite build via --include-build

--include-build命令行参数将执行的构建转换为复合,将包含的构建的依赖项替换为执行的构建.

Example: Declaring a command-line composite

gradle --include-build ../my-utils run输出
> gradle --include-build ../my-utils run
> Task :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 :compileJava
> Task :classes

> Task :run
The answer is 42


BUILD SUCCESSFUL in 0s
2 actionable tasks: 2 executed

Defining a composite build via the settings file

这有可能使上述设置持久,使用Settings.includeBuild(java.lang.Object中)宣布在附带的构建settings.gradle (或settings.gradle.kts在科特林)文件. 设置文件可用于同时添加子项目和包含的内部版本. 包含的内部版本按位置添加. 有关更多详细信息,请参见下面的示例.

Defining a separate composite build

上述方法的一个缺点是,它需要您修改现有的内部版本,从而使其作为独立的内部版本不太有用. 避免这种情况的一种方法是定义一个单独的复合构建,其唯一目的是将原本单独的构建进行组合.

示例2.声明一个单独的组合
composite/settings.gradle
rootProject.name = 'adhoc'

includeBuild '../my-app'
includeBuild '../my-utils'
composite/settings.gradle.kts
rootProject.name = "adhoc"

includeBuild("../my-app")
includeBuild("../my-utils")

在这种情况下,执行的"主要"构建是组合,并且没有定义任何有用的任务来执行自身. 为了在" my-app"构建中执行"运行"任务,复合构建必须定义一个委托任务.

例子3.根据包含的构建中的任务
composite/build.gradle
task run {
    dependsOn gradle.includedBuild('my-app').task(':run')
}
composite/build.gradle.kts
tasks.register("run") {
    dependsOn(gradle.includedBuild("my-app").task(":run"))
}

更多详细信息任务取决于以下包含的构建任务.

Restrictions on included builds

大多数构建都可以包含在组合中,包括其他组合构建. 但是,有一些限制.

每个包含的版本:

  • 不得具有与另一个包含的内部版本相同的rootProject.name .

  • 不得具有与复合构建的顶级项目相同的rootProject.name .

  • 不能有rootProject.name一样的复合构建rootProject.name .

Interacting with a composite build

通常,与组合构建进行交互与常规多项目构建非常相似. 可以执行任务,可以运行测试,并且可以将构建导入到IDE中.

Executing tasks

可以从命令行或您的IDE执行复合构建中的任务. 执行任务将导致直接任务依赖关系被执行,以及从包含的构建中构建依赖关系工件所需的那些任务.

还没有任何方法可以通过命令行从包含的内部版本直接执行任务. 包含的构建任务会自动执行以生成所需的依赖工件,或者包含的构建可以声明包含的构建对任务的依赖 .

Importing into the IDE

集成构建最有用的功能之一就是IDE集成. 通过将ideaeclipse插件应用于构建,可以生成一个IDEA或Eclipse项目,该项目允许组合中的所有构建一起开发.

除了这些Gradle插件之外, IntelliJ IDEAEclipse Buildship的最新版本支持直接导入复合构建.

导入复合构建可以使来自单独的Gradle构建的资源易于一起开发. 对于每个包含的构建,每个子项目都作为IDEA模块或Eclipse项目包含在内. 配置了源依赖关系,提供了跨版本的导航和重构.

Declaring the dependencies substituted by an included build

默认情况下,Gradle将配置每个包含的内部版本,以确定其可以提供的依赖项. 这样做的算法非常简单:Gradle将检查包含的构建中项目的组和名称,并将项目依赖项替换为与${project.group}:${project.name}匹配的任何外部依赖项.

在某些情况下,Gradle确定的默认替换不够用,或者它们不适用于特定组合. 对于这些情况,可以显式声明所包含构建的替换. 以一个单项目构建" anonymous-library"为例,它生成一个Java实用程序库,但未声明group属性的值:

Example 4. Build that does not declare group attribute
build.gradle
plugins {
    id 'java'
}
build.gradle.kts
plugins {
    java
}

当此构建包含在组合中时,它将尝试替换依赖项模块" undefined:anonymous-library"(" undefined"是project.group的默认值,而" anonymous-library"是项目根目录) . 显然,这在复合构建中不会很有用. 要在复合构建中使用未修改的未发布库,组合构建可以显式​​声明其提供的替换:

例子5.声明一个包含构建的替换
settings.gradle
rootProject.name = 'app'

includeBuild('../anonymous-library') {
    dependencySubstitution {
        substitute module('org.sample:number-utils') with project(':')
    }
}
settings.gradle.kts
rootProject.name = "app"

includeBuild("../anonymous-library") {
    dependencySubstitution {
        substitute(module("org.sample:number-utils")).with(project(":"))
    }
}

使用此配置," my-app"复合构建将用对" anonymous-library"根项目的依赖替换对org.sample:number-utils任何依赖.

Cases where included build substitutions must be declared

许多使用uploadArchives任务发布工件的内部版本将自动作为包含的内部版本运行,而无需声明替换. 以下是一些需要声明替换的常见情况:

  • 使用archivesBaseName属性设置发布的工件的名称时.

  • 发布default配置以外的配置时:这通常意味着使用了不同于uploadArchives的任务.

  • MavenPom.addFilter()用于发布与项目名称不匹配的工件时.

  • 当使用maven-publishivy-publish插件进行发布时,发布坐标与${project.group}:${project.name}不匹配.

Cases where composite build substitutions won’t work

某些组合即使包含在显式声明的依赖项替换中,也无法正常运行. 该限制是由于以下事实:被替换的项目依赖项将始终指向目标项目的default配置. 每当为项目的默认配置指定的工件和依赖项与实际发布到存储库的内容不匹配时,组合构建就可能表现出不同的行为.

在某些情况下,发布模块元数据可能与项目默认配置不同:

  • 当发布非default配置时.

  • 使用maven-publishivy-publish插件时.

  • POMivy.xml文件作为发布的一部分进行调整时.

当包含在复合版本中时,使用这些功能的版本将无法正常运行. 我们计划在将来对此进行改进.

Depending on tasks in an included build

虽然包含的内部版本相互隔离并且不能声明直接依赖关系,但是复合内部版本可以对其包含的内部版本声明任务依赖关系. 可以使用Gradle.getIncludedBuilds()Gradle.includedBuild(java.lang.String)访问包含的内部版本,并通过IncludedBuild.task(java.lang.String)方法获取任务参考.

使用这些API,可以声明对特定包含的构建中的任务或所有包含的某些构建中具有一定路径的任务的依赖性.

示例6.取决于包含的构建中的单个任务
composite/build.gradle
task run {
    dependsOn gradle.includedBuild('my-app').task(':run')
}
composite/build.gradle.kts
tasks.register("run") {
    dependsOn(gradle.includedBuild("my-app").task(":run"))
}
示例7.取决于所有包含的构建中具有路径的任务
build.gradle
task publishDeps {
    dependsOn gradle.includedBuilds*.task(':publishIvyPublicationToIvyRepository')
}
build.gradle.kts
tasks.register("publishDeps") {
    dependsOn(gradle.includedBuilds.map { it.task(":publishIvyPublicationToIvyRepository") })
}

Current limitations and future plans for composite builds

我们认为复合构建已经非常有用. 但是,有些事情并没有按照我们希望的方式工作,还有一些我们认为可以使事情做得更好的改进.

当前实施的局限性包括:

  • 不支持包含发布的不反映项目默认配置的内部版本. 请参阅复合建筑无法使用的案例 .

  • 不支持基于软件模型的本机版本. (本机版本尚不支持二进制依赖性).

  • 如果多个复合构建并行运行,则多个复合构建可能会发生冲突. Gradle不会在Gradle调用之间共享共享复合构建的项目锁,以防止并发执行.

我们计划在即将发布的版本中进行的改进包括:

  • 更好地检测依赖项替换,以使用自定义坐标发布的生成,生成多个组件的生成,等等.这将减少需要为包含的生成显式声明依赖项替换的情况.

  • 可以直接从命令行定位包含的内部版本中的一个或多个任务的功能. 我们目前正在探索允许使用此功能的语法选项,这将消除许多在组合中需要委派任务的情况.

  • 使隐式buildSrc项目成为包含的构建.