Skip to content

依赖锁定机制

源:Gradle 官方文档 - Dependency Locking

依赖锁定确保每次构建使用完全相同的依赖版本,即使依赖声明中使用了动态版本或范围,实现可重现的构建。

依赖锁定作用

为什么需要锁定

问题场景

kotlin
dependencies {
    implementation("com.example:library:1.+")  // 动态版本
}

风险

  • 不同时间构建可能使用不同版本
  • CI 和本地构建结果不一致
  • 难以复现问题
  • 意外引入破坏性变更

解决方案:依赖锁定

锁定的优势

可重现构建

  • 确保版本一致
  • CI/CD 稳定
  • 问题可复现

安全性

  • 防止供应链攻击
  • 审计依赖变更
  • 控制升级时机

团队协作

  • 统一依赖版本
  • 避免"在我机器上能跑"
  • 便于 Code Review

启用依赖锁定

基本启用

kotlin
// build.gradle.kts
dependencyLocking {
    lockAllConfigurations()
}

锁定特定配置

kotlin
configurations {
    runtimeClasspath {
        resolutionStrategy.activateDependencyLocking()
    }
    compileClasspath {
        resolutionStrategy.activateDependencyLocking()
    }
}

锁定模式

严格模式

kotlin
dependencyLocking {
    lockMode.set(LockMode.STRICT)
}

宽松模式(默认):

kotlin
dependencyLocking {
    lockMode.set(LockMode.LENIENT)
}

区别

  • STRICT:必须锁定所有配置
  • LENIENT:仅锁定激活的配置

生成锁文件

首次生成

bash
# 生成所有配置的锁文件
./gradlew dependencies --write-locks

# 仅生成特定模块
./gradlew :app:dependencies --write-locks

生成位置

gradle/
└── dependency-locks/
    ├── compileClasspath.lockfile
    ├── runtimeClasspath.lockfile
    ├── debugCompileClasspath.lockfile
    └── debugRuntimeClasspath.lockfile

锁文件格式

# compileClasspath.lockfile
androidx.core:core-ktx:1.15.0=compileClasspath
com.squareup.retrofit2:retrofit:2.11.0=compileClasspath
com.squareup.okhttp3:okhttp:4.12.0=compileClasspath,runtimeClasspath
empty=annotationProcessor

更新锁文件

更新所有依赖

bash
# 更新所有锁文件
./gradlew dependencies --write-locks

# 更新特定模块
./gradlew :app:dependencies --write-locks

更新特定依赖

bash
# 更新单个依赖
./gradlew dependencies --update-locks com.example:library

# 更新多个依赖
./gradlew dependencies --update-locks com.example:lib-a,com.example:lib-b

更新特定组

bash
# 更新整个 group
./gradlew dependencies --update-locks androidx.compose:*

验证锁文件

构建时验证

bash
# 普通构建会自动验证
./gradlew build

验证失败

Dependency lock state for configuration 'compileClasspath' is out of date:
  - Did not resolve 'com.example:library:1.1.0' which is part of the lock state

严格验证

kotlin
dependencyLocking {
    lockMode.set(LockMode.STRICT)
}

CI/CD 集成

GitHub Actions

yaml
name: Build

on: [push, pull_request]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      
      - name: Setup JDK
        uses: actions/setup-java@v3
        with:
          java-version: '17'
      
      - name: Verify Dependency Locks
        run: ./gradlew build --no-daemon
      
      - name: Check for lock changes
        run: |
          if ! git diff --exit-code gradle/dependency-locks/; then
            echo "Lock files have changed!"
            exit 1
          fi

更新策略

PR 中更新

yaml
- name: Update Dependencies
  if: github.event_name == 'pull_request'
  run: ./gradlew dependencies --write-locks
  
- name: Commit Lock Changes
  uses: stefanzweifel/git-auto-commit-action@v4
  with:
    commit_message: "Update dependency locks"
    file_pattern: gradle/dependency-locks/*.lockfile

实战案例

案例1:多模块项目

根项目配置

kotlin
// build.gradle.kts (project)
subprojects {
    apply(plugin = "base")
    
    dependencyLocking {
        lockAllConfigurations()
    }
}

生成所有模块的锁

bash
./gradlew allDependencies --write-locks

案例2:Android 项目

kotlin
// app/build.gradle.kts
dependencyLocking {
    lockAllConfigurations()
    lockMode.set(LockMode.STRICT)
}

生成 Android 配置锁

bash
./gradlew :app:dependencies --write-locks

锁文件

gradle/dependency-locks/
├── debugCompileClasspath.lockfile
├── debugRuntimeClasspath.lockfile
├── releaseCompileClasspath.lockfile
└── releaseRuntimeClasspath.lockfile

案例3:定期更新依赖

创建更新脚本

bash
#!/bin/bash
# update-dependencies.sh

echo "Updating all dependencies..."
./gradlew dependencies --write-locks

echo "Running tests..."
./gradlew test

if [ $? -eq 0 ]; then
    echo "Tests passed. Committing lock files..."
    git add gradle/dependency-locks/
    git commit -m "Update dependency locks"
else
    echo "Tests failed. Rolling back..."
    git checkout gradle/dependency-locks/
fi

最佳实践

提交锁文件到版本控制

gitignore
# .gitignore
# 不要忽略锁文件!
# gradle/dependency-locks/

定期更新

  • 每周或每月更新一次
  • 测试后再提交
  • Code Review 时检查变更

CI 验证

  • 确保锁文件最新
  • 失败时阻止合并
  • 自动化检查

文档化更新原因

bash
git commit -m "Update dependency locks

- Retrofit 2.10.0 -> 2.11.0 (bug fixes)
- OkHttp 4.11.0 -> 4.12.0 (security patch)
"

使用严格模式

kotlin
dependencyLocking {
    lockMode.set(LockMode.STRICT)
}

忽略不需要锁定的配置

kotlin
configurations.matching {
    it.name.endsWith("Metadata") || it.name.contains("Test")
}.configureEach {
    resolutionStrategy.deactivateDependencyLocking()
}

常见问题

锁文件冲突

问题:多人修改依赖导致锁文件冲突

解决

bash
# 重新生成锁文件
./gradlew dependencies --write-locks

# 测试
./gradlew build

# 提交
git add gradle/dependency-locks/
git commit -m "Resolve lock file conflicts"

版本不匹配

问题:锁文件中的版本与依赖声明不一致

原因

  • 依赖已更新但未更新锁
  • 手动修改了锁文件

解决

bash
./gradlew dependencies --write-locks

构建失败

问题:锁定后构建失败

原因:锁文件损坏或不完整

解决

bash
# 删除旧锁文件
rm -rf gradle/dependency-locks/

# 重新生成
./gradlew dependencies --write-locks

与其他机制配合

与 Version Catalog

toml
# gradle/libs.versions.toml
[versions]
retrofit = "2.11.0"

[libraries]
retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" }
kotlin
dependencies {
    implementation(libs.retrofit)
}

dependencyLocking {
    lockAllConfigurations()
}

优势

  • Version Catalog 管理声明版本
  • 锁文件锁定实际解析版本

与依赖约束

kotlin
dependencies {
    constraints {
        implementation("com.example:library:2.0")
    }
}

dependencyLocking {
    lockAllConfigurations()
}

配合使用

  • 约束推荐版本
  • 锁文件固定版本

工具支持

查看锁文件差异

bash
# 查看锁文件变更
git diff gradle/dependency-locks/

验证锁文件

bash
# 验证所有锁文件
./gradlew buildEnvironment

生成报告

bash
# 生成依赖报告
./gradlew htmlDependencyReport