Gradle 的依赖管理引擎是变体感知的

上一节中,Gradle 构建了一个解析的依赖关系图。在图解析期间,Gradle 根据构建的需求选择了每个依赖项的适当变体

dependency resolution variant selection model

变体代表组件的不同使用方式,例如用于 Java 编译、原生链接或文档。每个变体可能拥有自己的构件依赖关系

Gradle 使用属性来确定选择哪个变体。这些属性为每个变体添加了上下文,描述了何时应该使用它们。

组件

让我们回顾上一节中对组件的描述。一个组件

  • 包含变体

    • 其中包含一个或多个构件

    • 其中包含零个或多个依赖关系

    • 元数据描述

component model gradle

在上面的示例中,org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.1组件。它有一个模块 org.jetbrains.kotlinx:kotlinx-serialization-json 和一个版本 1.5.1

component model gradle example

变体和属性

变体代表组件的不同版本或方面,例如 apiimplementationjarclasses

在上面的示例中,org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.1 有四个变体jvmandroidjsnative

属性是类型安全的键值对,供消费者生产者变体选择期间使用:属性 : 值

在上面的示例中,org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.1变体有两个重要的属性

变体 属性 1 属性 2

JVM

org.gradle.usage: java-runtime

org.jetbrains.kotlin.platform.type: jvm

Android

org.gradle.usage: java-runtime

org.jetbrains.kotlin.platform.type: androidJvm

Javascript

org.gradle.usage: java-runtime

org.jetbrains.kotlin.platform.type: js

原生

org.gradle.usage: native-link

org.jetbrains.kotlin.platform.type: native

请注意,org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.1元数据展示了更多描述其变体属性,例如 org.gradle.libraryelementsorg.gradle.category。然而,Gradle 在依赖解析期间可能会使用其中的任意数量,因为有些属性比其他属性更相关。

"variants": [
...
  {
    "name": "jsLegacyRuntimeElements-published",
    "attributes": {
      "org.gradle.category": "library",
      "org.gradle.usage": "kotlin-runtime",
      "org.jetbrains.kotlin.js.compiler": "legacy",
      "org.jetbrains.kotlin.platform.type": "js"
    }
  },
  {
    "name": "jvmRuntimeElements-published",
    "attributes": {
      "org.gradle.category": "library",
      "org.gradle.libraryelements": "jar",
      "org.gradle.usage": "java-runtime",
      "org.jetbrains.kotlin.platform.type": "jvm"
    }
  },
...
]

Gradle 使用两种类型属性来将可用变体与构建所需的变体进行匹配:

  • 消费者属性:定义可解析配置所请求的变体所需的特征。

  • 生产者属性:每个变体都有一组描述其用途的属性

变体属性匹配

组件可以定义多少个变体没有任何限制。

典型的组件将至少包含一个实现变体,但也可能提供额外的变体,例如测试装置、文档或源代码。此外,组件可以根据消费者为相同的用途提供不同的变体。例如,在编译期间,组件可能为 Linux、Windows 和 macOS 提供不同的头文件。

Gradle 通过使用属性匹配算法匹配消费者指定的属性与生产者定义的属性来执行变体感知选择

dependency resolution variant selection
变体名称主要用于调试和错误消息。它在变体匹配中不起作用;匹配过程中只使用变体的属性

属性匹配算法

Gradle 的依赖解析引擎遵循以下属性匹配算法来确定最佳候选(或在找不到匹配项时失败):

dep man adv 5

步骤 1:寻找兼容候选

请求的属性候选属性进行匹配。

  • 对于每个候选

    • 对于每个候选属性

      • 如果候选属性缺失,则将候选标记为缺失。

      • 否则,如果候选属性与请求的属性完全匹配,则候选保持兼容。

      • 否则,如果候选属性通过兼容性测试,则候选保持兼容。

      • 否则,排除该候选。

步骤 2:最多匹配属性

过滤后,Gradle 检查一个候选是否是所有其他候选的严格超集

  • 如果只剩下一个候选,则该候选获胜。

  • 如果剩下多个候选,检查其中一个

    • 匹配的属性多于其他任何候选。

    • 与另一个候选相比没有缺失属性

  • 如果存在这样的严格超集,则该候选获胜。

步骤 3:使用请求属性进行消歧

如果剩下多个候选,Gradle 尝试消歧

  • 请求的属性优先顺序排序

  • 对于每个请求的属性按优先顺序

    • 如果存在消歧规则,则应用该规则。

    • 如果所有剩余候选的某个请求属性具有相同的值,则忽略该属性。

  • 如果只剩下一个候选,则该候选获胜。

如果仍然剩下多个候选,Gradle 处理无序属性(不在优先顺序列表中的属性)

  • 对于每个请求的属性没有优先顺序

    • 如果存在消歧规则,则应用该规则。

    • 如果所有剩余候选的某个请求属性具有相同的值,则忽略该属性。

  • 如果只剩下一个候选,则该候选获胜。

  • 如果没有候选剩下,Gradle 回退到最兼容的候选(步骤 2)。

步骤 4:使用额外属性进行消歧

如果仍然剩下多个候选,Gradle 考虑额外属性

  • 这些属性仅存在于某些候选中。

  • 缺乏额外属性的候选优先。

  • 考虑属性优先顺序

首先,Gradle 处理优先级额外属性

  • 对于每个额外属性按优先顺序

    • 如果某些候选具有该属性而其他候选没有

      • 缺乏该额外属性的候选优先。

      • 具有额外属性的候选被排除。

  • 如果只剩下一个候选,则该候选获胜。

  • 如果没有候选剩下,Gradle 回退到原始兼容候选(步骤 1)。

然后,Gradle 处理无序额外属性

  • 对于每个剩余的额外属性没有定义优先顺序

    • 如果该属性仅存在于部分候选中

      • 没有该属性的候选优先。

      • 具有额外属性的候选被排除。

  • 如果只剩下一个候选,则该候选获胜。

  • 如果没有候选剩下,Gradle 回退到原始兼容候选(步骤 1)。

步骤 5:使用额外属性存在性进行消歧

如果仍然剩下候选,Gradle 根据请求属性排除候选

  • 对于每个额外属性

    • 如果所有候选都提供相同的值,则忽略它。

    • 否则,移除提供该值的候选

  • 如果只剩下一个候选,则该候选获胜。

步骤 6:失败条件

除非另有说明,如果在任何时候没有候选剩下,则解析失败

此外,Gradle 输出步骤 1 中所有兼容候选的列表,以帮助调试属性匹配失败。

插件和生态系统可以通过实现兼容性规则、消歧规则和定义属性的优先顺序来影响选择算法。优先级较高的属性将按顺序用于排除候选。

例如,在 Java 生态系统中,org.gradle.usage 属性的优先级高于 org.gradle.libraryelements。这意味着如果两个候选在 org.gradle.usageorg.gradle.libraryelements 都具有兼容值,Gradle 将选择通过 org.gradle.usage 消歧规则的候选。

变体感知解析过程有两个例外:

  • 当生产者没有变体时,选择默认的构件

  • 当消费者通过名称显式选择配置时,使用与该配置相关的构件

一个简单示例

让我们来看一个示例,其中消费者正在尝试使用库进行编译。

首先,消费者详细说明它将如何使用依赖解析的结果。这通过在消费者的可解析配置上设置属性来实现。

在这种情况下,消费者希望解析匹配 org.gradle.usage=java-api变体

接下来,生产者暴露了其组件的不同变体

  • API 变体(命名为 apiElements),具有属性 org.gradle.usage=java-api

  • 运行时变体(命名为 runtimeElements),具有属性 org.gradle.usage=java-runtime

最后,Gradle 评估变体并选择正确的变体:

  • 消费者请求具有属性 org.gradle.usage=java-api变体

  • 生产者apiElements 变体匹配此请求。

  • 生产者runtimeElements 变体不匹配。

消费者请求属性 生产者可用属性 生产者变体 匹配?

org.gradle.usage=java-api

org.gradle.usage=java-api

apiElements

✅ 是

org.gradle.usage=java-api

org.gradle.usage=java-runtime

runtimeElements

❌ 否

因此,Gradle 选择 apiElements 变体并将其构件依赖关系提供给消费者

一个复杂示例

在实际场景中,消费者生产者通常都使用多个属性

例如,Gradle 中的 Java 库项目将涉及多个属性

属性 描述

org.gradle.usage

描述变体的用途。

org.gradle.dependency.bundling

描述变体如何处理依赖关系(例如,shadow jar、fat jar、普通 jar)。

org.gradle.libraryelements

描述变体的打包方式(例如,classes 或 jar)。

org.gradle.jvm.version

描述变体目标的 最低版本 Java。

org.gradle.jvm.environment

描述变体目标的 JVM 类型。

让我们考虑一个场景,其中消费者希望在 Java 8 上使用库运行测试,而生产者支持两个版本:Java 8 和 Java 11。

步骤 1:消费者指定需求。

消费者希望解析一个变体,该变体

  • 可在运行时使用(org.gradle.usage=java-runtime)。

  • 可在 至少 Java 8 上运行(org.gradle.jvm.version=8)。

步骤 2:生产者暴露多个变体。

生产者为 API 和运行时使用提供了 Java 8 和 Java 11 的变体

变体名称 属性

apiJava8Elements

org.gradle.usage=java-api, org.gradle.jvm.version=8

runtime8Elements

org.gradle.usage=java-runtime, org.gradle.jvm.version=8

apiJava11Elements

org.gradle.usage=java-api, org.gradle.jvm.version=11

runtime11Elements

org.gradle.usage=java-runtime, org.gradle.jvm.version=11

步骤 3:Gradle 匹配属性。

Gradle 将消费者请求的属性生产者变体进行比较:

  • 消费者请求具有 org.gradle.usage=java-runtimeorg.gradle.jvm.version=8 属性的变体

  • runtime8Elementsruntime11Elements 都匹配 org.gradle.usage=java-runtime 属性

  • API 变体apiJava8ElementsapiJava11Elements)因不匹配 org.gradle.usage=java-runtime 而被丢弃。

  • 选择 runtime8Elements 变体,因为它与 Java 8 兼容。

  • runtime11Elements 变体不兼容,因为它需要 Java 11。

消费者请求属性 生产者可用属性 生产者变体 匹配?

org.gradle.usage=java-runtime, org.gradle.jvm.version=8

org.gradle.usage=java-runtime, org.gradle.jvm.version=8

runtime8Elements

✅ 已选择

org.gradle.usage=java-runtime, org.gradle.jvm.version=8

org.gradle.usage=java-runtime, org.gradle.jvm.version=11

runtime11Elements

❌ 不兼容

org.gradle.usage=java-runtime, org.gradle.jvm.version=8

org.gradle.usage=java-api, org.gradle.jvm.version=8

apiJava8Elements

❌ 已丢弃

org.gradle.usage=java-runtime, org.gradle.jvm.version=8

org.gradle.usage=java-api, org.gradle.jvm.version=11

apiJava11Elements

❌ 已丢弃

Gradle 选择 runtime8Elements 并将其构件依赖关系提供给消费者

但是,如果消费者设置 org.gradle.jvm.version=7 会发生什么?

在这种情况下,依赖解析将失败,并出现错误解释没有合适的变体。Gradle 知道消费者需要与 Java 7 兼容的库,但生产者的最低版本是 8。

如果消费者请求 org.gradle.jvm.version=15,Gradle 可以选择 Java 8 或 Java 11 变体。然后,Gradle 会选择兼容的最高版本——Java 11。

可视化变体信息

Gradle 提供内置任务来可视化变体选择过程,并显示涉及的生产者消费者属性

Outgoing variants report

报告任务 outgoingVariants 显示可供项目消费者选择的变体列表。它显示每个变体能力属性构件

此任务类似于 dependencyInsight 报告任务

默认情况下,outgoingVariants 会打印所有变体的信息。它提供了可选参数 --variant <variantName> 来选择单个变体进行显示。它还接受 --all 标志以包含关于传统和已弃用配置的信息,或接受 --no-all 以排除此信息。

以下是在新生成的 java-library 项目上运行 outgoingVariants 任务的输出:

> Task :outgoingVariants
--------------------------------------------------
Variant apiElements
--------------------------------------------------
API elements for the 'main' feature.

Capabilities
    - new-java-library:lib:unspecified (default capability)
Attributes
    - org.gradle.category            = library
    - org.gradle.dependency.bundling = external
    - org.gradle.jvm.version         = 11
    - org.gradle.libraryelements     = jar
    - org.gradle.usage               = java-api
Artifacts
    - build/libs/lib.jar (artifactType = jar)

Secondary Variants (*)

    --------------------------------------------------
    Secondary Variant classes
    --------------------------------------------------
        Description = Directories containing compiled class files for main.

        Attributes
            - org.gradle.category            = library
            - org.gradle.dependency.bundling = external
            - org.gradle.jvm.version         = 11
            - org.gradle.libraryelements     = classes
            - org.gradle.usage               = java-api
        Artifacts
            - build/classes/java/main (artifactType = java-classes-directory)

--------------------------------------------------
Variant mainSourceElements (i)
--------------------------------------------------
Description = List of source directories contained in the Main SourceSet.

Capabilities
    - new-java-library:lib:unspecified (default capability)
Attributes
    - org.gradle.category            = verification
    - org.gradle.dependency.bundling = external
    - org.gradle.verificationtype    = main-sources
Artifacts
    - src/main/java (artifactType = directory)
    - src/main/resources (artifactType = directory)

--------------------------------------------------
Variant runtimeElements
--------------------------------------------------
Runtime elements for the 'main' feature.

Capabilities
    - new-java-library:lib:unspecified (default capability)
Attributes
    - org.gradle.category            = library
    - org.gradle.dependency.bundling = external
    - org.gradle.jvm.version         = 11
    - org.gradle.libraryelements     = jar
    - org.gradle.usage               = java-runtime
Artifacts
    - build/libs/lib.jar (artifactType = jar)

Secondary Variants (*)

    --------------------------------------------------
    Secondary Variant classes
    --------------------------------------------------
        Description = Directories containing compiled class files for main.

        Attributes
            - org.gradle.category            = library
            - org.gradle.dependency.bundling = external
            - org.gradle.jvm.version         = 11
            - org.gradle.libraryelements     = classes
            - org.gradle.usage               = java-runtime
        Artifacts
            - build/classes/java/main (artifactType = java-classes-directory)

    --------------------------------------------------
    Secondary Variant resources
    --------------------------------------------------
        Description = Directories containing the project's assembled resource files for use at runtime.

        Attributes
            - org.gradle.category            = library
            - org.gradle.dependency.bundling = external
            - org.gradle.jvm.version         = 11
            - org.gradle.libraryelements     = resources
            - org.gradle.usage               = java-runtime
        Artifacts
            - build/resources/main (artifactType = java-resources-directory)

从中可以看到 java 库暴露的两个主要变体apiElementsruntimeElements。请注意,主要区别在于 org.gradle.usage 属性,其值分别为 java-apijava-runtime。正如它们所指示的,这区分了需要放在消费者编译类路径上的内容,以及需要放在运行时类路径上的内容。

它还显示次要变体,这些变体仅限于 Gradle 项目且不发布。例如,apiElements 中的次要变体 classes 使得 Gradle 在针对 java-library 项目进行编译时可以跳过 JAR 创建。

关于无效可消费配置的信息

一个项目不能有多个具有相同属性能力的配置。在这种情况下,项目将构建失败。

为了能够可视化此类问题,outgoing 变体报告以宽松的方式处理这些错误。这允许报告显示关于该问题的信息。

Resolvable configurations report

Gradle 还提供了互补的报告任务 resolvableConfigurations,该任务显示项目的可解析配置,即可以添加依赖并解析的配置。

报告将列出它们的属性以及它们继承的任何配置。它还将列出在解析过程中受兼容性规则消歧规则影响的任何属性的摘要。

默认情况下,resolvableConfigurations 打印所有纯粹可解析配置的信息。这些配置被标记为可解析,但标记为可消费。尽管一些可解析配置也标记为可消费,但这些是传统配置,不应在构建脚本中添加依赖项。

此报告提供:

  • 一个可选参数

    • --configuration <configurationName> → 选择要显示的单个配置。

  • 包含或排除传统和已弃用配置的标志

    • --all → 包含关于传统和已弃用配置的信息。

    • --no-all → 排除此信息。

  • 控制扩展配置部分中传递扩展的标志

    • --recursive → 列出传递而不是直接扩展的配置。

    • --no-recursive → 排除此信息。

以下是在新生成的 java-library 项目上运行 resolvableConfigurations 任务的输出:

> Task :resolvableConfigurations
--------------------------------------------------
Configuration compileClasspath
--------------------------------------------------
Description = Compile classpath for source set 'main'.

Attributes
    - org.gradle.category            = library
    - org.gradle.dependency.bundling = external
    - org.gradle.jvm.environment     = standard-jvm
    - org.gradle.jvm.version         = 11
    - org.gradle.libraryelements     = classes
    - org.gradle.usage               = java-api
Extended Configurations
    - compileOnly
    - implementation

--------------------------------------------------
Configuration runtimeClasspath
--------------------------------------------------
Description = Runtime classpath of source set 'main'.

Attributes
    - org.gradle.category            = library
    - org.gradle.dependency.bundling = external
    - org.gradle.jvm.environment     = standard-jvm
    - org.gradle.jvm.version         = 11
    - org.gradle.libraryelements     = jar
    - org.gradle.usage               = java-runtime
Extended Configurations
    - implementation
    - runtimeOnly

[...]

从中可以看到用于解析依赖关系的两个主要配置,compileClasspathruntimeClasspath,以及它们对应的测试配置(已截断)。

现在我们理解了变体选择和属性匹配,让我们继续依赖解析的构件解析阶段。这个阶段也是变体感知的。

下一步: 了解构件解析 >>