构建依赖图后,Gradle 执行构件解析,将解析后的图映射到一组将在构建期间下载的构件。

构件

构件是在构建过程中生成或消耗的文件。构件通常是编译后的库、JAR 文件、AAR 文件、DLLs 或 ZIP 文件等文件。

我们来看一下 org.jetbrains.kotlin:kotlin-stdlib:1.8.10元数据,其中展示了几个变体构件

kotlin-stdlib-1.8.10.module
{
  "variants": [
    {
      "name": "apiElements",
      "attributes": {
        "org.gradle.category": "library",
        "org.gradle.dependency.bundling": "external",
        "org.gradle.jvm.version": "8",
        "org.gradle.libraryelements": "jar",
        "org.gradle.usage": "java-api"
      },
      "files": [
        {
          "name": "kotlin-stdlib-1.8.10-public.jar"
        },
        {
          "name": "kotlin-stdlib-1.8.10-private.jar"
        }
      ]
    },
    {
      "name": "runtimeElements",
      "attributes": {
        "org.gradle.category": "library",
        "org.gradle.dependency.bundling": "external",
        "org.gradle.jvm.version": "8",
        "org.gradle.libraryelements": "jar",
        "org.gradle.usage": "java-runtime"
      },
      "files": [
        {
          "name": "kotlin-stdlib-1.8.10.jar"
        }
      ]
    },
    {
      "name": "jdk7ApiElements",
      "attributes": {
        "org.gradle.usage": "java-api",
        "org.gradle.jvm.version": "7"
      },
      "files": [
        {
          "name": "kotlin-stdlib-jdk7-1.8.10.jar"
        }
      ]
    },
    {
      "name": "jdk8ApiElements",
      "attributes": {
        "org.gradle.usage": "java-api",
        "org.gradle.jvm.version": "8"
      },
      "files": [
        {
          "name": "kotlin-stdlib-jdk8-1.8.10.jar"
        }
      ]
    }
  ]
}

我们可以看到元数据中提供了许多构件

变体 构件 用途

apiElements

kotlin-stdlib-1.8.10-public.jar, kotlin-stdlib-1.8.10-private.jar

标准 Kotlin 运行时(默认)。

runtimeElements

kotlin-stdlib-jdk7-1.8.10.jar

为 Java 7 兼容性提供额外的 API。

jdk7ApiElements

kotlin-stdlib-jdk8-1.8.10.jar

为 Java 8 兼容性提供额外的 API。

jdk8ApiElements

kotlin-stdlib-common-1.8.10.jar

Kotlin Multiplatform 的共享标准库。

通常,一旦选择了变体,其关联的构件就可以自动解析。然而,在变体选择之后仍然进行构件选择有一些特定的原因。

例如,如果 org.jetbrains.kotlin:kotlin-stdlib:1.8.10 的元数据看起来像这样

kotlin-stdlib-1.8.10.module
{
  "variants": [
    {
      "name": "runtimeElements",
      "attributes": {
        "org.gradle.category": "library",
        "org.gradle.dependency.bundling": "external",
        "org.gradle.jvm.version": "8",
        "org.gradle.libraryelements": "jar",
        "org.gradle.usage": "java-runtime"
      },
      "files": [
        {
          "name": "kotlin-stdlib-1.8.10.jar"
        }
      ]
    },
    {
      "name": "runtimeElements",
      "attributes": {
        "org.gradle.category": "library",
        "org.gradle.dependency.bundling": "external",
        "org.gradle.jvm.version": "8",
        "org.gradle.libraryelements": "classes",
        "org.gradle.usage": "java-runtime"
      },
      "files": [
        {
          "name": "build/classes/java/main/"
        }
      ]
    },
    {
      "name": "runtimeElements",
      "attributes": {
        "org.gradle.category": "library",
        "org.gradle.dependency.bundling": "external",
        "org.gradle.jvm.version": "8",
        "org.gradle.libraryelements": "resources",
        "org.gradle.usage": "java-runtime"
      },
      "files": [
        {
          "name": "build/resources/main/"
        }
      ]
    }
  ]
}

Gradle 如何知道要下载哪个文件?

构件集

构件集是一组属于模块的单个变体构件

单个变体可以包含多个构件集,每个构件集服务于不同的目的。

我们来看一下 org.jetbrains.kotlin:kotlin-stdlib:1.8.10 的示例

变体 集中的构件

apiElements

1

jar → 打包的库 JAR (kotlin-stdlib-1.8.10.jar)

2

tar → 打包的库 TAR (kotlin-stdlib-1.8.10.tar)

runtimeElements

1

jar → 打包的库 JAR (kotlin-stdlib-1.8.10.jar)

2

classes → 编译后的 .class 文件 (build/classes/java/main/)

3

resources → 关联的资源文件 (build/resources/main/)

jdk8ApiElements

1

jar → 打包的库 JAR (kotlin-stdlib-jdk8-1.8.10.jar)

jdk7ApiElements

1

jar → 打包的库 JAR (kotlin-stdlib-jdk7-1.8.10.jar)

org.jetbrains.kotlin:kotlin-stdlib:1.8.10apiElements 变体提供了两个构件集——jartar——每个都以不同的形式表示相同的可分发内容。org.jetbrains.kotlin:kotlin-stdlib:1.8.10runtimeElements 变体提供了三个构件集——jarclassesresources——每个都以不同的形式表示相同的可分发内容。

Gradle 现在必须遵循特定流程来确定最适合构建的构件集

构件解析流程

构件选择在依赖图上以节点对节点的方式进行操作。

图中的每个节点(即变体)都可能暴露多个构件集,但只能选择其中一个集。

dep man adv 4

对于图中的每个节点变体,Gradle 会对该节点暴露的每个构件集执行属性匹配,以确定最佳的构件集

如果没有构件集与请求的属性匹配,Gradle 将尝试构建一个构件转换链来满足请求。

有关属性匹配过程的更多详细信息,请参阅上一

可用 API

可以使用 Gradle API 来影响构件选择过程。

然后 Gradle 可以将构件选择结果作为 ArtifactCollection 暴露。更常见的是,结果作为 FileCollection 暴露,这是一个平面文件列表。

隐式构件选择

默认情况下,用于构件选择的属性与图解析期间用于变体选择的属性相同。这些属性Configuration.getAttributes() 属性指定。

要使用这些默认属性执行构件选择(以及隐式的图解析),请使用 FileCollectionArtifactCollection API。

文件也可以从配置的 ResolvedConfigurationLenientConfigurationResolvedArtifactResolvedDependency API 中访问。然而,这些 API 处于维护模式,不鼓励在新开发中使用。这些 API 使用默认属性执行构件选择。

解析文件

要解析文件,我们首先定义一个接受 ConfigurableFileCollection 作为输入的任务

build.gradle.kts
abstract class ResolveFiles : DefaultTask() {

    @get:InputFiles
    abstract val files: ConfigurableFileCollection

    @TaskAction
    fun print() {
        files.forEach {
            println(it.name)
        }
    }
}
build.gradle
abstract class ResolveFiles extends DefaultTask {

    @InputFiles
    abstract ConfigurableFileCollection getFiles()

    @TaskAction
    void print() {
        files.each {
            println(it.name)
        }
    }
}

然后,我们可以将可解析配置的文件连接到任务的输入。Configuration 直接实现 FileCollection,并且可以直接连接

build.gradle.kts
tasks.register<ResolveFiles>("resolveConfiguration") {
    files.from(configurations.runtimeClasspath)
}
build.gradle
tasks.register("resolveConfiguration", ResolveFiles) {
    files.from(configurations.runtimeClasspath)
}

运行 resolveConfiguration 任务会产生

> Task :resolveConfiguration
junit-platform-commons-1.11.0.jar
junit-jupiter-api-5.11.0.jar
opentest4j-1.3.0.jar

解析构件

我们可以不直接从隐式构件选择过程中消费文件,而是消费包含文件和元数据的构件

这个过程稍微复杂一些,为了保持配置缓存的兼容性,我们需要将 ResolvedArtifactResult 的字段拆分为两个任务输入

build.gradle.kts
data class ArtifactDetails(
    val id: ComponentArtifactIdentifier,
    val variant: ResolvedVariantResult
)

abstract class ResolveArtifacts : DefaultTask() {

    @get:Input
    abstract val details: ListProperty<ArtifactDetails>

    @get:InputFiles
    abstract val files: ListProperty<File>

    fun from(artifacts: Provider<Set<ResolvedArtifactResult>>) {
        details.set(artifacts.map {
            it.map { artifact -> ArtifactDetails(artifact.id, artifact.variant) }
        })
        files.set(artifacts.map {
            it.map { artifact -> artifact.file }
        })
    }

    @TaskAction
    fun print() {
        assert(details.get().size == files.get().size)
        details.get().zip(files.get()).forEach { (details, file) ->
            println("${details.variant.displayName}:${file.name}")
        }
    }
}
build.gradle
class ArtifactDetails {
    ComponentArtifactIdentifier id
    ResolvedVariantResult variant

    ArtifactDetails(ComponentArtifactIdentifier id, ResolvedVariantResult variant) {
        this.id = id
        this.variant = variant
    }
}

abstract class ResolveArtifacts extends DefaultTask {

    @Input
    abstract ListProperty<ArtifactDetails> getDetails()

    @InputFiles
    abstract ListProperty<File> getFiles()

    void from(Provider<Set<ResolvedArtifactResult>> artifacts) {
        details.set(artifacts.map {
            it.collect { artifact -> new ArtifactDetails(artifact.id, artifact.variant) }
        })
        files.set(artifacts.map {
            it.collect { artifact -> artifact.file }
        })
    }

    @TaskAction
    void print() {
        List<ArtifactDetails> allDetails = details.get()
        List<File> allFiles = files.get()

        assert allDetails.size() == allFiles.size()
        for (int i = 0; i < allDetails.size(); i++) {
            def details = allDetails.get(i)
            def file = allFiles.get(i)
            println("${details.variant.displayName}:${file.name}")
        }
    }
}

此任务的初始化方式与文件解析任务类似

build.gradle.kts
tasks.register<ResolveArtifacts>("resolveIncomingArtifacts") {
    from(configurations.runtimeClasspath.flatMap { it.incoming.artifacts.resolvedArtifacts })
}
build.gradle
tasks.register("resolveIncomingArtifacts", ResolveArtifacts) {
    from(configurations.runtimeClasspath.incoming.artifacts.resolvedArtifacts)
}

运行此任务,我们可以看到文件元数据包含在输出中

org.junit.platform:junit-platform-commons:1.11.0 variant runtimeElements:junit-platform-commons-1.11.0.jar
org.junit.jupiter:junit-jupiter-api:5.11.0 variant runtimeElements:junit-jupiter-api-5.11.0.jar
org.opentest4j:opentest4j:1.3.0 variant runtimeElements:opentest4j-1.3.0.jar

自定义构件选择

ArtifactView API 在解析后的图之上操作,该图由 ../javadoc/org/gradle/api/artifacts/result/ResolutionResult.html[ResolutionResult] 定义。

该 API 提供了灵活的方式来访问解析的构件

  • FileCollection - 文件平面列表,这是处理解析构件最常用的方式。

  • ArtifactCollection - 提供对解析构件的元数据和文件的访问,支持更高级的构件处理。

当调用配置的 getFiles() 时,Gradle 根据图解析期间使用的属性选择构件。然而,ArtifactView API 更灵活。它允许你使用自定义属性从图解析构件

构件视图

ArtifactView 允许你

  1. 查询具有不同属性的构件:

    • 假设图解析了依赖项的 runtime 变体。你可以使用 ArtifactView 从其 api 变体中提取构件,即使它们最初不是解析图的一部分。

  2. 提取特定类型的构件:

    • 通过指定 artifactType 等属性,你只能请求 .jar 文件或特定的构件类型(例如,源文件、Javadoc)。

  3. 避免副作用:

    • 使用 ArtifactView 允许你提取构件,而无需更改底层的依赖项解析逻辑或配置状态。

在以下示例中,一个生产者项目创建了一个包含典型 Java 库变体runtimeElementsapiElements)的库。我们还创建了一个名为 apiProductionElements 的自定义变体,其构件为 production.jar,属性为 org.gradle.category:production

producer/build.gradle.kts
plugins {
    `java-library`
}

repositories {
    mavenCentral()
}

dependencies {
    // Define some dependencies here
}

// Define a task that produces a custom artifact
tasks.register<Jar>("createProductionArtifact") {
    archiveBaseName.set("production")
    from(sourceSets["main"].output)
    destinationDirectory.set(file("build/libs"))
}

configurations {
    // Define a custom configuration and extend from apiElements
    create("apiProductionElements") {
        extendsFrom(configurations.apiElements.get())
        outgoing.artifacts.clear()
        attributes {
            attribute(Category.CATEGORY_ATTRIBUTE, objects.named("production"))
        }
        artifacts {
            add("apiProductionElements", tasks.named("createProductionArtifact"))
        }
    }
}
producer/build.gradle
plugins {
    id 'java-library'
}

repositories {
    mavenCentral()
}

dependencies {
    // Define some dependencies here
}

// Define a task that produces a custom artifact
tasks.register('createProductionArtifact', Jar) {
    archiveBaseName.set('production')
    from(sourceSets.main.output)
    destinationDirectory.set(file('build/libs'))
}

configurations {
    // Define a custom configuration and extend from apiElements
    apiProductionElements {
        extendsFrom(configurations.apiElements)
        attributes {
            attribute(Category.CATEGORY_ATTRIBUTE, objects.named(Category, 'production'))
        }
        artifacts {
            add('apiProductionElements', tasks.named('createProductionArtifact'))
        }
    }
}

我们可以使用名为 checkProducerAttributes 的自定义任务查看该库可用的变体及其对应的构件属性。以下是显示该库相关变体及其对应构件属性的缩略输出:

Configuration: apiElements
Attributes:
  - org.gradle.category -> library
  - org.gradle.usage -> java-api
  - org.gradle.libraryelements -> jar
  - org.gradle.dependency.bundling -> external
Artifacts:
  - producer.jar

Configuration: apiProductionElements
Attributes:
  - org.gradle.category -> production
Artifacts:
  - production.jar

Configuration: mainSourceElements
Attributes:
  - org.gradle.dependency.bundling -> external
  - org.gradle.category -> verification
  - org.gradle.verificationtype -> main-sources
Artifacts:
  - /src/main/resources
  - /src/main/java

Configuration: runtimeElements
Attributes:
  - org.gradle.category -> library
  - org.gradle.usage -> java-runtime
  - org.gradle.libraryelements -> jar
  - org.gradle.dependency.bundling -> external
Artifacts:
  - producer.jar

一个 Java 应用是此 Java 库的消费者

consumer/build.gradle.kts
plugins {
    application
}

repositories {
    mavenCentral()
}

// Declare the dependency on the producer project
dependencies {
    implementation(project(":producer")) // This references another subproject in the same build
}
consumer/build.gradle
plugins {
    id 'application'
}

repositories {
    mavenCentral()
}

// Declare the dependency on the producer project
dependencies {
    implementation project(':producer')
}

默认情况下,作为消费者的应用在运行时将消费预期的变体。我们可以使用另一个名为 checkResolvedVariant 的自定义任务来验证这一点,该任务会打印以下内容:

RuntimeClasspath Configuration:
- Component: project :producer
    - Variant: runtimeElements
       - org.gradle.category -> library
       - org.gradle.usage -> java-runtime
       - org.gradle.libraryelements -> jar
       - org.gradle.dependency.bundling -> external
       - org.gradle.jvm.version -> 11
- Artifact: producer.jar

正如预期的那样,对于 runtimeClasspath,应用消费该库的 runtimeElements 变体,该变体作为构件 producer.jar 可用。它使用属性 org.gradle.category:libraryorg.gradle.usage:java-runtime 来选择此变体

现在,我们创建一个 ArtifactView 来选择该库提供的其他构件之一。我们通过使用带有属性 org.gradle.category:classesArtifactView 来实现这一点,以便我们获得源文件而不是 jar 文件。

consumer/build.gradle.kts
tasks.register("artifactWithAttributeAndView") {
    val configuration = configurations.runtimeClasspath
    println("ArtifactView with attribute 'libraryelements = classes' for ${configuration.name}:")
    val artifactView = configuration.get().incoming.artifactView {
        attributes {
            attribute(LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE, objects.named(LibraryElements::class, "classes"))
        }
    }
    println("- Attributes:")
    artifactView.attributes.keySet().forEach { attribute ->
        val value = artifactView.attributes.getAttribute(attribute)
        println("  - ${attribute.name} = ${value}")
    }
    artifactView.artifacts.artifactFiles.files.forEach { file ->
        println("- Artifact: ${file.name}")
    }
}

tasks.register("artifactWithAttributeAndVariantReselectionView") {
    val configuration = configurations.runtimeClasspath
    println("ArtifactView with attribute 'category = production' for ${configuration.name}:")
    val artifactView = configuration.get().incoming.artifactView {
        withVariantReselection()
        attributes {
            attribute(Category.CATEGORY_ATTRIBUTE, objects.named(Category::class, "production"))
        }
    }
    println("- Attributes:")
    artifactView.attributes.keySet().forEach { attribute ->
        val value = artifactView.attributes.getAttribute(attribute)
        println("  - ${attribute.name} = ${value}")
    }
    artifactView.artifacts.artifactFiles.files.forEach { file ->
        println("- Artifact: ${file.name}")
    }
}
consumer/build.gradle
tasks.register('artifactWithAttributeAndView') {
    def configuration = configurations.runtimeClasspath
    println "ArtifactView with attribute 'libraryelements = classes' for ${configuration.name}:"
    def artifactView = configuration.incoming.artifactView {
        attributes {
            attribute(LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE, objects.named(LibraryElements, 'classes'))
        }
    }
    println "- Attributes:"
    artifactView.attributes.keySet().each { attribute ->
        def value = artifactView.attributes.getAttribute(attribute)
        println "  - ${attribute.name} = ${value}"
    }
    artifactView.artifacts.artifactFiles.files.each { file ->
        println "- Artifact: ${file.name}"
    }
}

tasks.register('artifactWithAttributeAndVariantReselectionView') {
    def configuration = configurations.runtimeClasspath
    println "ArtifactView with attribute 'category = production' for ${configuration.name}:"
    def artifactView = configuration.incoming.artifactView {
        withVariantReselection()
        attributes {
            attribute(Category.CATEGORY_ATTRIBUTE, objects.named(Category,'production'))
        }
    }
    println "- Attributes:"
    artifactView.attributes.keySet().each { attribute ->
        def value = artifactView.attributes.getAttribute(attribute)
        println "  - ${attribute.name} = ${value}"
    }
    artifactView.artifacts.artifactFiles.files.each { file ->
        println "- Artifact: ${file.name}"
    }
}

我们运行任务 artifactWithAttributeAndView,可以看到我们获得了主构件

ArtifactView with attribute 'libraryelements = classes' for runtimeClasspath:
- Attributes:
  - org.gradle.libraryelements = classes
  - org.gradle.category = library
  - org.gradle.usage = java-runtime
  - org.gradle.dependency.bundling = external
  - org.gradle.jvm.environment = standard-jvm
- Artifact: main

现在,我们创建一个 ArtifactView,通过指定属性 org.gradle.category:production 并强制 Gradle 重新选择一个新的变体来选择我们的自定义变体 apiProductionElements

consumer/build.gradle.kts
tasks.register("artifactWithAttributeAndVariantReselectionView") {
    val configuration = configurations.runtimeClasspath
    println("ArtifactView with attribute 'category = production' for ${configuration.name}:")
    val artifactView = configuration.get().incoming.artifactView {
        withVariantReselection()
        attributes {
            attribute(Category.CATEGORY_ATTRIBUTE, objects.named(Category::class, "production"))
        }
    }
    println("- Attributes:")
    artifactView.attributes.keySet().forEach { attribute ->
        val value = artifactView.attributes.getAttribute(attribute)
        println("  - ${attribute.name} = ${value}")
    }
    artifactView.artifacts.artifactFiles.files.forEach { file ->
        println("- Artifact: ${file.name}")
    }
}
consumer/build.gradle
tasks.register('artifactWithAttributeAndVariantReselectionView') {
    def configuration = configurations.runtimeClasspath
    println "ArtifactView with attribute 'category = production' for ${configuration.name}:"
    def artifactView = configuration.incoming.artifactView {
        withVariantReselection()
        attributes {
            attribute(Category.CATEGORY_ATTRIBUTE, objects.named(Category,'production'))
        }
    }
    println "- Attributes:"
    artifactView.attributes.keySet().each { attribute ->
        def value = artifactView.attributes.getAttribute(attribute)
        println "  - ${attribute.name} = ${value}"
    }
    artifactView.artifacts.artifactFiles.files.each { file ->
        println "- Artifact: ${file.name}"
    }
}

正如预期的那样,选择了 apiProductionElements 变体以及 production.jar 构件

ArtifactView with attribute 'libraryelements = classes' for runtimeClasspath:
- Attributes:
  - org.gradle.libraryelements = classes
  - org.gradle.category = library
  - org.gradle.usage = java-runtime
  - org.gradle.dependency.bundling = external
  - org.gradle.jvm.environment = standard-jvm
- Artifact: main