GradleのDependency locking

Kenji Abe
10 min readFeb 23, 2023
Photo by regularguy.eth on Unsplash

Gradleは依存関係のバージョン指定に動的バージョンを使用することができますが、このバージョンを固定する機能があるので、それについてまとめておきます。

他言語のライブラリ管理のロックファイル(BundlerのGemfile.lockやnpmのpackage-lock.json)と似たようなものになります。

動的なバージョン

ぼくが観測している範囲では基本的にあまりやらないことなんですが、Gradleで依存ライブラリを定義するときのバージョンを動的することができます。

例えば、以下のように +[1.0,) などを使ったりした場合などは、実行するタイミングによっては使用されるバージョンが変わってしまいます。

dependencies {
implementation("com.squareup.moshi:moshi:+")
}

こういった場合にGradleの機能を使ってバージョンをロックすることができます。

バージョンのロックを有効にする

Gradleのバージョンロックを有効にするには、Configuration毎にロックすることになり、いくつかの指定方法があります。

すべてのConfigurationでロックを有効にするには以下のように書くだけです。

dependencyLocking {
lockAllConfigurations()
}

以下の例は compileClasspath だけでロックを有効にする方法です。

configurations.compileClasspath {
resolutionStrategy.activateDependencyLocking()
}

Androidの場合は少し面倒でGroovyかKTSかで書き方が変わります。以下の例では debugRuntimeClasspath でだけロックを有効にします。

// Groovy
configurations {
debugRuntimeClasspath {
resolutionStrategy.activateDependencyLocking()
}
}

// KTS
afterEvaluate {
configurations {
"debugRuntimeClasspath" {
resolutionStrategy.activateDependencyLocking()
}
}
}

// KTSの別の書き方
androidComponents {
onVariants(selector().withBuildType("debug")) { variant ->
variant.runtimeConfiguration.resolutionStrategy.activateDependencyLocking()
}
}

ロックファイルを作成する

バージョンをロックを有効にして、ロックファイルを作成する必要があります。

今回は例として、 compileClasspathtestCompileClasspath でロックを有効にした場合にロックファイルを確認します。(通常のGradleプロジェクトで使用しています。Not Android)

configurations {
compileClasspath {
resolutionStrategy.activateDependencyLocking()
}
testCompileClasspath {
resolutionStrategy.activateDependencyLocking()
}
}

ロックファイルを作成するには以下のコマンドを実行します。

$ ./gradlew dependencies --write-locks

例えば、Androidなどの複数モジュールがある場合はappモジュール等で実行することになると思います。

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

これを実行すると以下のような gradle.lockfile という名前のファイルが作成されます。このファイルには推移的依存も含まれます。

com.squareup.moshi:moshi:1.14.0=compileClasspath,testCompileClasspath
com.squareup.okio:okio:2.10.0=compileClasspath,testCompileClasspath
org.apiguardian:apiguardian-api:1.1.0=testCompileClasspath
org.jetbrains.kotlin:kotlin-stdlib-common:1.8.0=compileClasspath,testCompileClasspath
org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.8.0=compileClasspath,testCompileClasspath
org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.8.0=compileClasspath,testCompileClasspath
org.jetbrains.kotlin:kotlin-stdlib:1.8.0=compileClasspath,testCompileClasspath
org.jetbrains.kotlin:kotlin-test-junit5:1.8.0=testCompileClasspath
org.jetbrains.kotlin:kotlin-test:1.8.0=testCompileClasspath
org.jetbrains:annotations:13.0=compileClasspath,testCompileClasspath
org.junit.jupiter:junit-jupiter-api:5.6.3=testCompileClasspath
org.junit.platform:junit-platform-commons:1.6.3=testCompileClasspath
org.junit:junit-bom:5.6.3=testCompileClasspath
org.opentest4j:opentest4j:1.2.0=testCompileClasspath
empty=annotationProcessor,compileOnlyDependenciesMetadata,intransitiveDependenciesMetadata,kotlinCompilerPluginClasspath,kotlinNativeCompilerPluginClasspath,kotlinScriptDef,kotlinScriptDefExtensions,runtimeOnlyDependenciesMetadata,testAnnotationProcessor,testApiDependenciesMetadata,testCompileOnlyDependenciesMetadata,testIntransitiveDependenciesMetadata,testKotlinScriptDef,testKotlinScriptDefExtensions,testRuntimeOnlyDependenciesMetadata

= の左側がライブラリとバージョンで、右側にあるのは対象となるConfigurationになります。複数のConfigurationの場合はカンマ区切りになります。

最後の empty は依存関係が無いConfigurationが表示されています。

このファイルによってバージョンがロックされることになります。例えば、このロックファイルの手でバージョンを変更して実行すると変更後のバージョンが使用されることになります。

ロックファイルとの不整合

新しく依存関係を追加して実行したとき、ロックファイルに追加された依存関係がないときはエラーが発生するようになります。

dependencies {
implementation("com.squareup.moshi:moshi:+")
// これを新しく追加
implementation("com.squareup.moshi:moshi-adapters:+")

testImplementation(kotlin("test"))
}

例えば、 com.squareup.moshi:moshi-adapters を新しく追加して実行すると以下のようなエラーが発生します。

FAILURE: Build failed with an exception.

* What went wrong:
Execution failed for task ':compileKotlin'.
> Could not resolve all files for configuration ':compileClasspath'.
> Resolved 'com.squareup.moshi:moshi-adapters:1.14.0' which is not part of the dependency lock state

これを解決するにはロックファイルをもう一度生成することで解決します。 ./gradlew dependencies — write-locks をもう一回実行するだけです。

Lock Mode

さらにLock Modeを指定することができます。

dependencyLocking {
lockMode.set(LockMode.STRICT)
}

Lock Modeには STRICTLENIENT があります。

STRICT

STRICT はロックファイルにロックを有効にしているConfigurationが含まれていない場合はエラーになります。

LENIENT

LENIENT は実際の依存がロックファイルに無かったり、依存に無いものがロックファイルにある場合でも、特にエラーになりません。

その他ユースケース

本来の目的である動的なバージョンを固定するのはもちろんですが、他にもロックファイルを使って推移的依存が暗黙的にバージョンアップされてないかを確認することができます。

ライブラリをアップデートすると、そのライブラリが依存しているライブラリもアップデートされ、それがアプリケーションが意図しない挙動になる可能性があります。

このロックファイルはGit管理することになると思うので、その差分による推移的依存もアップデートされることに気づけますし、Pull Reqeustでレビューすることも可能になります。

--

--

Kenji Abe

Programmer / Gamer / Google Developers Expert for Android, Kotlin / @STAR_ZERO