Sharing outputs between projects
在多项目构建中,一个常见的模式是一个项目消耗另一个项目的工件. 通常,Java生态系统中最简单的消耗形式是,当A
依赖于B
,则A
将依赖于项目B
产生的jar
. 如本章前面所述,此模型由A
根据B
的变体建模,其中根据A
的需求选择变体. 为了进行编译,我们需要apiElements
变体提供的B
的API依赖关系. 对于运行时,我们需要runtimeElements
变体提供的B
的运行时依赖项.
但是,如果您需要与主要工件不同的工件怎么办? 例如,Gradle为依赖于另一个项目的测试装置提供了内置支持,但是有时您根本不需要依赖的工件就不会作为变体出现.
为了在项目之间安全共享并允许最大性能(并行度),必须通过传出配置公开此类工件.
⚠
|
不要直接引用其他项目任务
声明跨项目依赖关系的常见反模式是:
此发布模型不安全,并且可能导致不可复制且难以并行化内部版本. 本节说明如何通过使用变体在项目之间定义"交换"来正确创建跨项目边界 . |
Simple sharing of artifacts between projects
首先,生产者需要声明将要暴露给消费者的配置. 如配置章节中所述,这对应于消耗性配置 .
让我们想象一下,消费者需要生产者提供的检测类 ,但是该工件不是主要工件. 生产者可以通过创建将"承载"此工件的配置来公开其检测的类:
configurations {
instrumentedJars {
canBeConsumed = true
canBeResolved = false
// If you want this configuration to share the same dependencies, otherwise omit this line
extendsFrom implementation, runtimeOnly
}
}
val instrumentedJars by configurations.creating {
isCanBeConsumed = true
isCanBeResolved = false
// If you want this configuration to share the same dependencies, otherwise omit this line
extendsFrom(configurations["implementation"], configurations["runtimeOnly"])
}
这种配置是可消耗的 ,这意味着它是供消费者使用的"交换产品". 现在,我们要向此配置添加工件,消费者在使用它时会得到:
artifacts {
instrumentedJars(instrumentedJar)
}
artifacts {
add("instrumentedJars", instrumentedJar)
}
在这里,我们要附加的"工件"实际上是一个生成Jar的任务 . 这样做,Gradle可以自动跟踪此任务的依赖关系并根据需要构建它们. 这是可能的,因为Jar
任务扩展了AbstractArchiveTask
. 如果不是这种情况,则需要显式声明工件的生成方式.
artifacts {
instrumentedJars(someTask.outputFile) {
builtBy(someTask)
}
}
artifacts {
add("instrumentedJars", someTask.outputFile) {
builtBy(someTask)
}
}
现在, 消费者需要依赖于此配置以获得正确的工件:
dependencies {
instrumentedClasspath(project(path: ":producer", configuration: 'instrumentedJars'))
}
dependencies {
instrumentedClasspath(project(mapOf(
"path" to ":producer",
"configuration" to "instrumentedJars")))
}
⚠
|
如果计划发布具有此依赖关系的组件, 则不建议声明对显式目标配置的依赖关系:这可能会导致元数据损坏. 如果需要在远程存储库上发布组件,请遵循可识别变体的交叉发布文档中的说明 . |
在这种情况下,我们将依赖项添加到instrumentedClasspath配置中,该配置是消费者特定的配置 . 在Gradle术语中,这称为可解析配置 ,其定义方式如下:
configurations {
instrumentedClasspath {
canBeConsumed = false
canBeResolved = true
}
}
val instrumentedClasspath by configurations.creating {
isCanBeConsumed = false
isCanBeResolved = true
}
Variant-aware sharing of artifacts between projects
在简单的共享解决方案中 ,我们在生产者端定义了一个配置,用作生产者和消费者之间的工件交换. 但是,使用者必须明确指出它所依赖的配置,这是我们在变体感知分辨率下要避免的配置. 实际上,我们还解释了 ,消费者可以使用属性来表达需求,而生产者也应该使用属性来提供适当的输出变体. 这样可以进行更明智的选择,因为使用单个依赖项声明,而无需任何明确的目标配置,使用者可以解决不同的问题. 典型的例子是使用单一依赖性声明中project(":myLib")
我们要么选择arm64
或i386
版本的myLib
取决于架构.
为此,我们将向消费者和生产者添加属性.
⚠
|
重要的是要理解,一旦配置具有属性,它们便会参与变体感知的解析 ,这意味着只要使用诸如 实际上,这意味着在您创建的配置上使用的属性集可能取决于所使用的生态系统 (Java,C ++等),因为这些生态系统的相关插件通常使用不同的属性. |
让我们增强前面的示例,它恰好是一个Java库项目. Java库向它们的使用者公开了两个变体apiElements
和runtimeElements
. 现在,我们要添加第三个,是instrumentedJars
.
因此,我们需要了解新变体的用途,以便在其上设置适当的属性. 让我们看一下在runtimeElements
配置中找到的属性:
Attributes
- org.gradle.category = library
- org.gradle.dependency.bundling = external
- org.gradle.jvm.version = 11
- org.gradle.libraryelements = jar
- org.gradle.usage = java-runtime
它告诉我们的是Java库插件产生具有5个属性的变体:
-
org.gradle.category
告诉我们,此变体表示一个库 -
org.gradle.dependency.bundling
tells us that the dependencies of this variant are found as jars (they are not, for example, repackaged inside the jar) -
org.gradle.jvm.version
告诉我们该库支持的最低Java版本是Java 11 -
org.gradle.libraryelements
告诉我们此变体包含在jar中找到的所有元素(类和资源) -
org.gradle.usage
表示此变体是Java运行时,因此既适用于Java编译器,也适用于运行时
因此,如果我们希望在执行测试时使用我们的工具化类代替此变量,则需要将类似的属性附加到变量中. 实际上,我们关心的属性是org.gradle.libraryelements
,它解释了变量包含的内容 ,因此我们可以通过以下方式设置变量:
configurations {
instrumentedJars {
canBeConsumed = true
canBeResolved = false
attributes {
attribute(Category.CATEGORY_ATTRIBUTE, objects.named(Category, Category.LIBRARY))
attribute(Usage.USAGE_ATTRIBUTE, objects.named(Usage, Usage.JAVA_RUNTIME))
attribute(Bundling.BUNDLING_ATTRIBUTE, objects.named(Bundling, Bundling.EXTERNAL))
attribute(TargetJvmVersion.TARGET_JVM_VERSION_ATTRIBUTE, JavaVersion.current().majorVersion.toInteger())
attribute(LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE, objects.named(LibraryElements, 'instrumented-jar'))
}
}
}
val instrumentedJars by configurations.creating {
isCanBeConsumed = true
isCanBeResolved = false
attributes {
attribute(Category.CATEGORY_ATTRIBUTE, namedAttribute(Category.LIBRARY))
attribute(Usage.USAGE_ATTRIBUTE, namedAttribute(Usage.JAVA_RUNTIME))
attribute(Bundling.BUNDLING_ATTRIBUTE, namedAttribute(Bundling.EXTERNAL))
attribute(TargetJvmVersion.TARGET_JVM_VERSION_ATTRIBUTE, JavaVersion.current().majorVersion.toInt())
attribute(LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE, namedAttribute("instrumented-jar"))
}
}
inline fun <reified T: Named> Project.namedAttribute(value: String) = objects.named(T::class.java, value)
✨
|
在此过程中,最困难的事情是选择要设置的正确属性,因为它们具有变体的语义. 因此,在添加新属性之前,您应该始终询问自己是否没有一个可以承载所需语义的属性. 如果没有,则可以添加一个新属性. 添加新属性时,还必须小心,因为在选择过程中可能会产生歧义. 通常,添加属性意味着将其添加到所有现有变体中. |
我们在这里所做的是,我们添加了一个新的变体,可以在运行时使用它,但它包含检测类而不是普通类. 但是,现在这意味着对于运行时,使用者必须在两个变体之间进行选择:
-
runtimeElements
,java-library
插件提供的常规变体 -
instrumentedJars
,我们创建的变体
特别要说的是,我们要在测试运行时类路径上插入检测后的类. 现在,我们可以在使用者上将我们的依赖项声明为常规项目依赖项:
dependencies {
testImplementation 'junit:junit:4.12'
testImplementation project(':producer')
}
dependencies {
testImplementation("junit:junit:4.12")
testImplementation(project(":producer"))
}
如果我们在这里停止,Gradle仍将选择runtimeElements
变体代替我们的instrumentedJars
变体. 这是因为testRuntimeClasspath
配置要求一个libraryelements
属性为jar
的配置,而我们新的instrumented-jars
值不兼容 .
因此,我们需要更改请求的属性,以便现在查找已检测的罐子:
configurations {
testRuntimeClasspath {
attributes {
attribute(LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE, objects.named(LibraryElements, 'instrumented-jar'))
}
}
}
configurations {
testRuntimeClasspath {
attributes {
attribute(LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE, objects.named(LibraryElements::class.java, "instrumented-jar"))
}
}
}
现在,我们告诉我们,每当要解析测试运行时类路径时,我们正在寻找的是插补类 . 但是,有一个问题:在我们的依赖项列表中,我们有JUnit,很明显,它没有进行检测. 因此,如果我们在这里停下来,Gradle将会失败,并说明没有提供已检测类的JUnit变体. 这是因为我们没有解释说,如果没有可用的检测版本,则使用常规jar是可以的. 为此,我们需要编写一个兼容性规则 :
class InstrumentedJarsRule implements AttributeCompatibilityRule<LibraryElements> {
@Override
void execute(CompatibilityCheckDetails<LibraryElements> details) {
if (details.consumerValue.name == 'instrumented-jar' && details.producerValue.name == 'jar') {
details.compatible()
}
}
}
open class InstrumentedJarsRule: AttributeCompatibilityRule<LibraryElements> {
override fun execute(details: CompatibilityCheckDetails<LibraryElements>) = details.run {
if (consumerValue?.name == "instrumented-jar" && producerValue?.name == "jar") {
compatible()
}
}
}
我们需要在属性模式上声明:
dependencies {
attributesSchema {
attribute(LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE) {
compatibilityRules.add(InstrumentedJarsRule)
}
}
}
dependencies {
attributesSchema {
attribute(LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE) {
compatibilityRules.add(InstrumentedJarsRule::class.java)
}
}
}
就是这样! 现在我们有:
-
添加了一个提供仪器罐子的变体
-
解释说,此变体可以替代运行时
-
解释说,使用者仅在测试运行时需要此变体
因此,Gradle提供了一种强大的机制,可以根据偏好和兼容性选择正确的变体. 可以在文档的"了解变体的插件"部分中找到更多详细信息.
⚠
|
通过像我们一样向现有属性添加值,或通过定义新属性,我们可以扩展模型. 这意味着所有消费者都必须了解此扩展模型. 对于本地使用者,这通常不是问题,因为所有项目都理解并共享相同的模式,但是如果您必须将此新变体发布到外部存储库,则意味着外部使用者将必须为其构建添加相同的规则他们通过. 对于生态系统插件 (例如Kotlin插件)而言,这通常不是问题,在任何情况下,如果不应用插件就无法消费,但是如果添加自定义值或属性,则是一个问题. 因此, 避免发布仅用于内部使用的自定义变体 . |
Targeting different platforms
库针对不同平台的情况很常见. 在Java生态系统中,我们经常看到同一库的不同工件,以不同的分类器进行区分. 一个典型的例子是番石榴,其发布方式如下:
-
适用于JDK 8及更高版本的
guava-jre
-
适用于JDK 7的
guava-android
这种方法的问题在于没有与分类器相关的语义. 特别是,依赖项解析引擎无法根据用户需求自动确定要使用哪个版本. 例如,最好表示您对Guava有依赖性,然后让引擎根据兼容情况在jre
和android
之间进行选择.
Gradle为此提供了一种改进的模型,它没有分类器的弱点:属性.
特别是在Java生态系统中,Gradle提供了内置属性,库作者可以使用该属性来表达与Java生态系统的兼容性: org.gradle.jvm.version
. 此属性表示使用者必须具备的最低版本才能正常工作 .
当您应用java
或java-library
插件时,Gradle会自动将此属性与传出变体相关联. 这意味着,所有使用Gradle发布的库都会自动告知它们使用的目标平台.
默认情况下, org.gradle.jvm.version
设置为源集的目标兼容性 .
自动设置此属性后,默认情况下,Gradle 不会允许您为不同的JVM构建项目. 如果需要执行此操作,则需要按照有关变体感知匹配的说明创建其他变体.
✨
|
Gradle的未来版本将提供自动为不同Java平台构建的方法. |