Gradle 依赖解析引擎底层的依赖图与版本冲突仲裁策略
Gradle 的依赖解析不是“把 implementation("group:name:version") 下载成 jar”这么简单,而是一套面向图的约束求解引擎。
在 Android 工程里,依赖问题往往不是缺一个库,而是几个模型同时发生作用:
implementation、api、runtimeOnly这类配置决定依赖进入哪条 classpath。- 直接依赖与传递依赖共同组成一张依赖图。
- 同一个模块的多个版本会触发版本冲突仲裁。
- 不同坐标但提供同一能力的库会触发 capability 冲突。
- 一个组件可能发布多个 variant,Gradle 需要根据 consumer attributes 选择正确产物。
- Maven POM、Gradle Module Metadata、Ivy metadata 的信息完整度不同,直接影响解析结果。
可以把依赖解析想象成一座大型物流分拨中心。构建脚本只是提交采购清单,真正送到编译器和打包器手里的货物,要经过供应商识别、仓库路由、版本仲裁、兼容性检查和最终装箱。一个 Android 应用最后拿到的 compileClasspath、runtimeClasspath、debugRuntimeClasspath,不是采购清单的原样展开,而是 Gradle 对整张图求解后的结果。
本文聚焦依赖管理目录下最底层的一层:Gradle 如何把声明式依赖转换成可执行、可诊断、可缓存的解析结果。
依赖声明不是解析结果
在 build.gradle.kts 里写:
dependencies {
implementation("com.squareup.okhttp3:okhttp:4.12.0")
debugImplementation("com.squareup.leakcanary:leakcanary-android:2.14")
}
这两行代码只是在 Project 模型中登记依赖声明。它们不会立刻下载 jar,也不会立刻决定最终版本。真正解析通常发生在某个可解析配置被消费时,例如 Kotlin 编译任务读取 debugCompileClasspath,D8/R8 读取运行时 classpath,或者你执行 dependencies、dependencyInsight 这类诊断任务。
Gradle 的 configuration 可以粗略分成三类:
| 类型 | 典型例子 | 作用 |
|---|---|---|
| 声明桶 | implementation、api、debugImplementation |
只收集依赖声明,不能直接解析 |
| 可消费配置 | apiElements、runtimeElements |
模块对外暴露的变体出口 |
| 可解析配置 | compileClasspath、runtimeClasspath、debugRuntimeClasspath |
消费端真正触发图解析并拿到结果 |
这就是为什么同一个依赖放在 implementation 和 api 里行为不同。它们并不是“下载范围”的别名,而是参与不同配置图的输入。
一个 Java/Android Library 的典型依赖流向可以简化成这样:
dependencies {
api(...) ─┐
├─> apiElements ──> 下游 compileClasspath 可见
implementation(...) ─┼─> runtimeElements ──> 下游 runtimeClasspath 可见
└─> 自身 compile/runtime classpath 可见
}
api 暴露到下游编译期,implementation 只进入自身编译期和下游运行期。这一层边界本质上是 variant metadata 的一部分,不是 IDE 做的表面限制。
解析引擎的两段式模型
Gradle 官方把依赖解析拆成两个大阶段:
可解析配置
|
| 1. Graph Resolution
| 构建 resolved dependency graph,解决版本冲突,选择组件变体
v
已解析的组件/变体图
|
| 2. Artifact Resolution
| 把图中的变体映射到真实文件,下载 jar/aar/module/pom
v
文件集合:jar、aar、classes、resources、metadata
这两个阶段分开非常关键。
图解析阶段关心的是“谁应该出现在图里”。它处理组件坐标、版本选择、传递依赖、约束、能力冲突和变体选择。解析结果可以通过 ResolutionResult 读取,其中核心对象是 ResolvedComponentResult、ResolvedVariantResult 和它们之间的边。
artifact 解析阶段才关心“文件在哪里”。同一个组件变体可能有主 jar、源码 jar、文档 jar、AAR、native 库、classes 目录等不同 artifact。Gradle 可以在已经解析好的图上再创建 ArtifactView,选择其中一部分文件,而不是重新求整张依赖图。
这个分层设计解决了两个工程问题:
- 诊断依赖图时,不必下载所有 artifact。
- 同一张 resolved graph 可以被多个 artifact view 复用,例如编译需要 classpath,打包需要 runtime artifacts,IDE 可能只需要源码或文档。
依赖图的节点不是 jar,而是变体
很多依赖问题难排,是因为直觉里把图节点想成了“jar 文件”。Gradle 的真实模型更细:
Module: com.fasterxml.jackson.core:jackson-databind
|
`-- Component: com.fasterxml.jackson.core:jackson-databind:2.17.2
|
|-- Variant: apiElements
| |-- attributes: org.gradle.usage = java-api
| |-- dependencies: jackson-annotations, jackson-core, jackson-bom
| `-- artifact: jackson-databind-2.17.2.jar
|
`-- Variant: runtimeElements
|-- attributes: org.gradle.usage = java-runtime
|-- dependencies: jackson-annotations, jackson-core
`-- artifact: jackson-databind-2.17.2.jar
几个名词必须区分清楚:
| 名词 | 含义 | 例子 |
|---|---|---|
| Module | 逻辑模块坐标,不含具体版本 | com.google.guava:guava |
| Component | 某个模块的具体版本 | com.google.guava:guava:33.4.8-jre |
| Variant | 组件面向某种使用场景发布的形态 | apiElements、runtimeElements、debugRuntimeElements |
| Artifact | 真实文件 | .jar、.aar、.module、.pom |
| Edge | 一个变体依赖另一个变体的关系 | runtimeElements -> transitive runtime dependency |
这解释了一个 Android 工程常见现象:同一个模块可以同时有 debug/release、api/runtime、jvm/android/native 等多个出口。Gradle 不是根据 variant 名称猜,而是根据 attributes 选择。
源码视角:DependencyGraphBuilder 如何走图
Gradle 源码中的 DependencyGraphBuilder 是理解解析循环的入口。它的 resolve(...) 方法会创建 ResolveState,然后依次执行三个动作:
ResolveState resolveState = new ResolveState(...);
traverseGraph(resolveState);
validateGraph(resolveState, ...);
assembleResult(resolveState, modelVisitor);
压缩成伪代码,核心循环接近这样:
把 root variant 放入待处理队列
while 队列不空 或 存在待解决冲突:
if 队列不空:
node = 出队
if node 已被替换或不再贡献图:
移除其 outgoing edges
continue
if capability handler 发现能力冲突:
暂停处理该 node,先等待冲突仲裁
continue
edges = 收集 node 的 outgoing dependencies
对每条 edge 执行版本选择
对需要元数据的目标组件并行下载 metadata
串行把 edge attach 到选中的目标变体
else:
先解决 module version conflict
再解决 capability conflict
源码里有两个细节很能说明 Gradle 的工程取舍。
第一个细节是元数据下载可以并行。maybeDownloadMetadataInParallel(...) 会收集需要远程 metadata 的目标组件,数量大于 1 时提交到 build operation queue 并行执行。依赖解析不是纯 CPU 算法,远程仓库请求、POM/module metadata 下载是明显的 I/O 瓶颈。
第二个细节是把边接回图必须串行。attachToTargetRevisionsSerially(...) 的注释明确说明:如果在解析线程里直接把 edge 加回队列,会造成非确定性的图顺序。Gradle 宁愿把“下载”并行,把“图结构变更”串行,也要保证相同输入得到稳定结果。
这就是构建工具和普通下载器的区别。普通下载器只关心快,构建工具还必须关心可复现、可诊断和可缓存。
版本冲突:同一 Module 只能选一个 Component
最常见的冲突是同一个 module 被不同路径请求了不同版本:
:app
|-- com.google.guava:guava:20.0
|
`-- com.google.inject:guice:4.2.2
`-- com.google.guava:guava:25.1-android
Gradle 默认会在同一个依赖图里为 com.google.guava:guava 选择一个版本。官方文档给出的基本规则是:考虑图中所有请求版本,默认选择最高版本。
这个策略不是“谁离 root 近谁赢”,也不是“显式声明永远覆盖传递依赖”。直接依赖只是图中的一个 selector,传递依赖也是 selector。它们最终都会汇总到同一个 module 的候选集合里,再由冲突解析器决策。
可以把它想象成多个团队同时采购同一种零件:
团队 A 要 guava 20.0
团队 B 要 guava 25.1-android
团队 C 要 guava [23, 33)
采购系统不能给同一台机器装三套 guava。
它必须在所有要求之间找一个全局可接受的版本。
Gradle 源码中的 LatestModuleConflictResolver 进一步展示了默认“选最新”的实现不是简单字符串比较。它会:
- 使用
VersionParser把版本字符串解析为结构化版本。 - 用
VersionComparator比较 base version。 - 先找最高 base version 的候选集合。
- 如果多个候选 base version 相同,再在 qualifier、release status、Maven snapshot 等信息之间做选择。
也就是说,1.0.0、1.0.0-rc1、1.0.0-SNAPSHOT 不是按裸字符串拍脑袋排序。Gradle 有自己的版本解析与状态判断逻辑。
Rich Version:把“想要”变成“约束”
普通版本声明:
implementation("org.slf4j:slf4j-api:2.0.13")
表达的是一个 selector。复杂工程里,只说“我要这个版本”往往不够,因为你还需要表达:
- 这个版本只是偏好,其他更强约束可以覆盖。
- 这个版本必须严格满足,否则宁可失败。
- 某些坏版本必须拒绝。
- 一个版本范围内都能接受。
Gradle 用 rich version 表达这些语义:
dependencies {
implementation("org.slf4j:slf4j-api") {
version {
strictly("[2.0, 3.0[")
prefer("2.0.13")
reject("2.0.9")
}
because("保持日志 API 主版本稳定,同时避开已知问题版本")
}
}
几个词的语义边界不同:
| 规则 | 语义 | 失败条件 |
|---|---|---|
prefer |
没有更强版本要求时优先选它 | 通常不单独导致失败 |
require |
需要至少满足这个要求,可被更高兼容版本提升 | 没有可接受候选时失败 |
strictly |
必须落在严格范围或严格版本内 | 冲突结果不满足时失败 |
reject |
明确排除某些版本 | 被排除版本成为最终选择时失败 |
strictly 是大型 Android 工程里最容易误用的工具。它不是“强制升级”的优雅写法,而是在给解析器加硬约束。如果多个模块都写了互相不兼容的 strictly,Gradle 没有义务猜一个折中版本,它应该失败。
更稳健的版本治理通常分层处理:
libs.versions.toml 负责集中声明坐标和常用版本别名
platform / BOM 负责一组模块的版本对齐
dependency constraints 负责给传递依赖增加可发布的约束
component metadata rule 修补第三方 metadata 的事实错误
resolutionStrategy 最后兜底,少用,且必须有原因
下一篇会专门拆 Version Catalog 与 BOM。本文先记住一点:Version Catalog 不是解析规则,它只是声明入口;真正参与冲突仲裁的是依赖声明、platform、constraints、metadata 和 resolution strategy。
Dependency Constraints:不引入依赖,只参与仲裁
constraints 很容易被误解成“另一种 implementation”。它的关键语义是:约束本身不会把模块拉进图里,只有当该模块因为其他路径已经出现时,约束才参与版本选择。
dependencies {
implementation("com.example:feature-a:1.0")
constraints {
implementation("org.apache.commons:commons-lang3") {
version {
strictly("3.14.0")
}
because("统一传递依赖版本,避免运行期 API 差异")
}
}
}
如果 feature-a 或其他依赖根本没有引入 commons-lang3,上面的 constraint 不会凭空加入一个 jar。它像仓库里的采购红线:只有订单里真的出现这个物料时,红线才生效。
这点对 Android 多模块很重要。把约束集中放进 java-platform 或 convention plugin,可以让每个模块不必重复声明传递依赖版本,同时避免为了“控制版本”而额外引入没有使用的库。
Capability 冲突:不同坐标也可能互斥
版本冲突处理的是“同一个 module 的多个版本”。Capability 冲突处理的是“不同 module 提供同一种能力”。
典型例子是日志绑定、连接池实现、旧坐标迁移后的重复库:
runtimeClasspath
|-- ch.qos.logback:logback-classic
`-- org.slf4j:slf4j-simple
两者都可能提供 SLF4J binding。
最终运行时只能有一个真正接管日志输出。
Maven 坐标天然看不出这种冲突,因为它们的 group/name 完全不同。Gradle 引入 capability,就是为了把“功能等价或互斥”变成 metadata。
dependencies {
components {
withModule("org.slf4j:slf4j-simple") {
allVariants {
withCapabilities {
addCapability("org.slf4j", "slf4j-binding", id.version)
}
}
}
withModule("ch.qos.logback:logback-classic") {
allVariants {
withCapabilities {
addCapability("org.slf4j", "slf4j-binding", id.version)
}
}
}
}
}
configurations.configureEach {
resolutionStrategy.capabilitiesResolution
.withCapability("org.slf4j:slf4j-binding") {
select("ch.qos.logback:logback-classic:1.5.6")
because("应用运行时统一使用 logback 作为 SLF4J binding")
}
}
Gradle 检测到同一依赖图里出现多个相同 capability 时,通常会失败并报告冲突。这个失败是好事,因为它把运行时才爆炸的“两个实现抢同一个入口”提前到了构建期。
Variant-Aware Resolution:Gradle 选择的是使用场景
Gradle 的现代依赖解析是 variant-aware 的。组件可以发布多个 variant,每个 variant 用 attributes 描述自己适合什么使用场景。
一个 consumer configuration 也携带 attributes,表达它想要什么:
consumer: debugRuntimeClasspath
org.gradle.usage = java-runtime
org.gradle.category = library
org.gradle.libraryelements = jar / aar
com.android.build.api.attributes.BuildTypeAttr = debug
producer: :library
debugRuntimeElements
usage = java-runtime
buildType = debug
releaseRuntimeElements
usage = java-runtime
buildType = release
Gradle 要做的不是“找名叫 debug 的配置”,而是把 consumer attributes 和 producer variant attributes 放进 attribute matcher。
源码里的 GraphVariantSelector 展示了这条路径:
候选 variants
|
|-- 先按 requested capabilities 过滤
|
|-- AttributeMatcher.matchMultipleCandidates(...)
|
|-- 如果仍有多个候选,再尝试 strict capability 匹配
|
|-- 如果仍歧义,抛出 ambiguous variants failure
DefaultAttributeMatcher 里有一个非常关键的方法:allCommonAttributesSatisfy(...)。它只检查 consumer 与 candidate 共同拥有的 attribute 是否兼容。如果 requested 为空,或 candidate 为空,基础匹配可能认为它们不冲突。多个候选时再进入 MultipleCandidateMatcher 做消歧。
这解释了两个工程现象:
- attribute 缺失不一定等于立刻不兼容,Gradle 可能进入后续消歧。
- variant 匹配失败时,错误信息里列出的 attribute 差异比 variant 名称更重要。
官方算法大致分成几步:
1. 过滤不兼容候选
2. 如果只剩一个,选中
3. 对请求的 attributes 按优先级消歧
4. 对候选额外 attributes 消歧
5. 仍无法唯一确定则失败
Android Gradle Plugin 正是靠这套机制让 debug app 消费 debug library、让 runtime classpath 消费 runtime variant、让测试 APK 与主 APK 选择相匹配的变体。
Metadata 决定解析质量
Gradle 解析依赖时需要 metadata。不同发布格式能表达的信息不同:
| 格式 | 文件 | 能表达的信息 | 风险 |
|---|---|---|---|
| Gradle Module Metadata | .module |
variants、attributes、capabilities、constraints | 信息最完整,但消费方需要 Gradle 理解 |
| Maven POM | .pom |
依赖、scope、dependencyManagement、optional 等 | 无法完整表达 Gradle variant 模型 |
| Ivy metadata | ivy.xml |
Ivy configuration 与 artifacts | 语义依赖发布者约定 |
如果第三方库 metadata 有问题,错误会沿着依赖图传播。比如:
- POM 把只在某个 feature 下需要的 optional 依赖放得不准确。
- 一个库迁移了坐标,但旧坐标没有声明新坐标的 capability。
- 一个库发布了
-jre、-android两种形态,但 metadata 没有把它们建模为 variants。 - 传递依赖声明过宽,把运行时不需要的库带入 Android APK。
这时不要急着写全局 exclude。更精确的修复通常是 component metadata rule:
abstract class Slf4jBindingCapabilityRule : ComponentMetadataRule {
override fun execute(context: ComponentMetadataContext) {
context.details.allVariants {
withCapabilities {
addCapability("org.slf4j", "slf4j-binding", context.details.id.version)
}
}
}
}
dependencies {
components {
withModule("org.slf4j:slf4j-simple", Slf4jBindingCapabilityRule::class.java)
}
}
metadata rule 的价值在于它修补的是“组件事实”,而不是在某个 consumer configuration 上粗暴删边。修补后,Gradle 后续的版本仲裁、能力冲突和 variant selection 都能继续按模型工作。
Exclude 是切边,不是治本
Android 工程里常见这样的写法:
implementation("com.example:legacy-sdk:1.0") {
exclude(group = "commons-logging", module = "commons-logging")
}
exclude 的真实含义是从这条依赖边上移除某个传递依赖。它不是全局删除模块,也不会证明运行时代码路径真的不需要这个库。
:app
|
|-- legacy-sdk --X--> commons-logging
|
`-- other-sdk ------> commons-logging
对 legacy-sdk 的 exclude 只切断第一条边。
如果 other-sdk 仍然需要它,它依然会进入图。
什么时候可以用 exclude?
- 你确认 metadata 错了,库声明了实际不会使用的传递依赖。
- 你确认当前代码路径不会触发被排除库的类型加载。
- 有测试覆盖关键运行路径。
- 更合适的 constraints、capability、metadata rule 都不能表达这个事实。
否则 exclude 可能把构建期冲突变成运行期 ClassNotFoundException、NoSuchMethodError 或 Android 启动崩溃。
Android 依赖冲突为什么更危险
普通 JVM 应用也会遇到依赖冲突,但 Android 的风险更高。
第一,最终 APK/AAB 不是把所有 jar 原样丢给 JVM。Android 构建还要经过 D8/R8,把 class 合并成 dex,并进行 desugar、shrink、optimize、obfuscate。版本冲突造成的 API 差异,可能在编译期、dex 合并期、R8 优化期或运行期任何一层暴露。
第二,Android 运行时没有传统 Java 服务端那么宽松的 classpath 调试空间。一个传递依赖被升级后,如果旧库在运行时调用了新版本已经删除的方法,常见结果是:
java.lang.NoSuchMethodError
java.lang.NoClassDefFoundError
java.lang.IncompatibleClassChangeError
Duplicate class ... found in modules ...
第三,AGP 自己也大量依赖 variant attributes。一个本地 library 没有正确发布 debugRuntimeElements,或者一个自定义插件创建了缺 attribute 的 consumable configuration,都可能让 Gradle 选到错误 variant,或者直接报 ambiguous variant。
所以 Android 依赖治理要把“编译通过”视为最低门槛,而不是正确性的证明。更可靠的做法是让 Gradle 模型尽可能表达真实约束:版本对齐用 platform,传递依赖版本用 constraints,互斥实现用 capability,metadata 错误用 component metadata rule,最后才是 exclude 和 force。
dependencyInsight:沿着仲裁链排查
当依赖结果不符合预期时,第一工具是 dependencyInsight:
./gradlew :app:dependencyInsight \
--configuration debugRuntimeClasspath \
--dependency com.google.guava:guava
你要看的不是“有没有这个库”,而是三类信息:
1. 选中了哪个版本
2. 为什么选中:conflict resolution / constraint / forced / selected by rule
3. 哪些路径请求了它
如果看到:
com.google.guava:guava:20.0 -> 33.4.8-android
Selection reasons:
- By conflict resolution: between versions 20.0 and 33.4.8-android
说明直接声明并没有“覆盖”传递依赖,Gradle 依据全图版本仲裁选了更高版本。
如果看到 capability 冲突,优先判断这是不是模型缺失:
Cannot choose between ...
All of them match the consumer attributes
They provide the same capability ...
这类问题不要靠 exclude 硬砍。先决定业务上到底应该保留哪个实现,再用 capabilitiesResolution 或 metadata rule 把选择写清楚。
如果看到 variant 选择失败,重点看 attributes:
No matching variant of project :library was found.
The consumer was configured to find a runtime of a library ...
这类错误的根因通常是 producer 没有暴露对应 usage/buildType/flavor/libraryElements,或者 consumer 请求的 attribute 与 producer 能提供的变体不在同一个语义空间。
一个完整的冲突治理示例
假设 Android 应用出现以下依赖图:
:app
|-- com.fasterxml.jackson.core:jackson-databind:2.13.0
|
`-- com.example:network-sdk:1.8.0
|-- com.fasterxml.jackson.core:jackson-core:2.17.2
`-- com.fasterxml.jackson.core:jackson-annotations:2.17.2
如果只依赖默认最高版本仲裁,可能得到:
jackson-databind 2.13.0
jackson-core 2.17.2
jackson-annotations 2.17.2
这组版本不一定立即编译失败,但 Jackson 这类同一项目内多模块共同演进的库,混用版本容易出现运行期行为差异。正确模型不是给每个子模块手写最高版本,而是声明平台对齐:
dependencies {
implementation(platform("com.fasterxml.jackson:jackson-bom:2.17.2"))
implementation("com.fasterxml.jackson.core:jackson-databind")
implementation("com.example:network-sdk:1.8.0")
}
这样 Gradle 解析器拿到的是一组版本约束,而不是一堆散落的字符串。依赖图里只要出现 Jackson 平台下的成员模块,就按 BOM 提供的约束对齐。
如果某个第三方 POM 没有发布 BOM 关系,也可以在内部 java-platform 中集中声明:
plugins {
`java-platform`
}
dependencies {
constraints {
api("com.fasterxml.jackson.core:jackson-databind:2.17.2")
api("com.fasterxml.jackson.core:jackson-core:2.17.2")
api("com.fasterxml.jackson.core:jackson-annotations:2.17.2")
}
}
再由各 Android 模块消费这个内部 platform。这样约束可以跟随工程版本治理演进,而不是散落在几十个 module 的 build.gradle.kts 里。
解析引擎背后的设计权衡
Gradle 依赖解析的复杂性来自几个目标同时成立:
| 目标 | 设计选择 | 代价 |
|---|---|---|
| 支持传递依赖 | 图解析而非线性下载 | 需要冲突仲裁和诊断工具 |
| 支持多使用场景 | variant + attributes | metadata 不完整时错误更难读 |
| 支持生态兼容 | 兼容 Maven POM/Ivy/Gradle metadata | 不同 metadata 表达能力不一致 |
| 支持可复现 | 串行 attach 图边、锁定结果、可禁用动态版本 | 某些解析步骤不能无限并行 |
| 支持可治理 | constraints、platform、capabilities、metadata rules | API 多,容易把工具用错层 |
理解这些权衡之后,很多规则会变得自然:
- 不要用
force当版本治理主工具,因为它绕过了很多约束语义。 - 不要用全局 exclude 掩盖 metadata 问题,因为它切掉的是边,不是事实。
- 不要把 Version Catalog 当 BOM,因为 catalog 不参与传递依赖仲裁。
- 不要忽视 variant attributes,因为 Android 的 debug/release/flavor 本质上都依赖这套选择机制。
- 不要允许动态版本无约束进入核心 classpath,因为解析结果会随仓库状态变化。
工业级 Android 工程的依赖解析准则
一套稳定的 Android 依赖治理策略应当遵守下面的顺序:
- 用 Version Catalog 管理人类可读的坐标入口,但不要期待它解决冲突。
- 用 BOM 或
java-platform对齐同一技术栈的一组模块。 - 用 dependency constraints 管理传递依赖的最低版本、严格版本和拒绝版本。
- 用 component metadata rules 修补第三方发布元数据的事实错误。
- 用 capabilities 表达互斥实现,让冲突在构建期失败。
- 用
dependencyInsight和ResolutionResult诊断真实图,不凭声明文件猜结果。 - 只有在确认运行路径安全时,才使用 exclude。
- 把
failOnDynamicVersions()、failOnChangingVersions()、dependency locking 等能力用于发布构建,避免不可复现解析。
Gradle 依赖解析引擎的核心不是“自动帮你选版本”,而是给构建系统一套能表达事实与约束的语言。写得好的构建逻辑,会让冲突在模型层被清晰暴露,并让依赖变更具备可观测、可审计的排查路径;写得差的构建逻辑,会把模型层的问题拖到 D8、R8 甚至用户设备上才爆炸。
参考资料
- Gradle User Manual: Dependency Resolution: https://docs.gradle.org/current/userguide/dependency_resolution.html
- Gradle User Manual: Graph Resolution: https://docs.gradle.org/current/userguide/graph_resolution.html
- Gradle User Manual: Variant Selection and Attribute Matching: https://docs.gradle.org/current/userguide/variant_aware_resolution.html
- Gradle User Manual: Variants and Attributes: https://docs.gradle.org/current/userguide/variant_attributes.html
- Gradle User Manual: Dependency Constraints and Conflict Resolution: https://docs.gradle.org/current/userguide/dependency_constraints_conflicts.html
- Gradle User Manual: Declaring Dependency Constraints: https://docs.gradle.org/current/userguide/dependency_constraints.html
- Gradle User Manual: Declaring Versions and Ranges: https://docs.gradle.org/current/userguide/dependency_versions.html
- Gradle User Manual: Capabilities: https://docs.gradle.org/current/userguide/component_capabilities.html
- Gradle User Manual: Component Metadata Rules: https://docs.gradle.org/current/userguide/component_metadata_rules.html
- Android Developers: Gradle dependency resolution: https://developer.android.com/build/gradle-dependency-resolution
- Gradle source:
DependencyGraphBuilder: https://github.com/gradle/gradle/blob/master/platforms/software/dependency-management/src/main/java/org/gradle/api/internal/artifacts/ivyservice/resolveengine/graph/builder/DependencyGraphBuilder.java - Gradle source:
LatestModuleConflictResolver: https://github.com/gradle/gradle/blob/master/platforms/software/dependency-management/src/main/java/org/gradle/api/internal/artifacts/ivyservice/resolveengine/LatestModuleConflictResolver.java - Gradle source:
GraphVariantSelector: https://github.com/gradle/gradle/blob/master/platforms/software/dependency-management/src/main/java/org/gradle/internal/component/model/GraphVariantSelector.java - Gradle source:
DefaultAttributeMatcher: https://github.com/gradle/gradle/blob/master/subprojects/core/src/main/java/org/gradle/api/internal/attributes/matching/DefaultAttributeMatcher.java