マルチプロジェクトビルドのための Gradle lockfile
Gradle は、依存ライブラリのバージョンを範囲で指定して管理できます。この仕組みは動的バージョン(dynamic version)と呼ばれています。
// build.gradle.kts
dependencies {
// Use spring-boot-starter with its version prefixed with "3."
implementation("org.springframework.boot:spring-boot-starter:3.+")
}
このときビルドを再現可能にするには、範囲指定されたバージョンを特定のバージョンに固定(ロック)する必要があります。ロックの仕組みとして、npm の package-lock.json や Rust の Cargo.lockなどが知られていますが、Gradle も同様にロックファイル gradle.lockfile を生成するための依存ロック(dependency locking)という機能を提供しています。
依存ロックは Gradle 4.8 からサポートされていますが、まだ広く使われているとは言えません。理由の一つとして、依存ロックをマルチプロジェクトビルドや buildSrc、バージョンカタログ(libs.versions.toml)などと組み合わせて使う方法が明確でない点が挙げられます。
One does not simply generate a Gradle.lockfile for multi-project builds
本記事では、Gradle の依存ロックをマルチプロジェクトビルドや buildSrc で構成されたプロジェクトに導入する方法を紹介します。
準備
検証には Gradle のバージョン 8.14.2を使用しました。
検証用のプロジェクト ajalab/gradle-lockfile-for-multi-projects (初期コミット:5e67156)は、2つの Kotlin サブプロジェクト app1、app2 およびビルドスクリプト buildSrc で構成されています。buildSrc は、app1 と app2がビルドロジックを共有するためのコンベンションプラグイン kotlin-jvm.gradle.kts を提供します。設定ファイルは Gradle Kotlin DSL(*.kts)で記述されており、そのほとんどは IntelliJ IDEA の "Generate multi-module build" オプションによって生成されたものを流用しています。
プロジェクトで最初に指定した Kotlin のバージョンは 2.1.0 です。後ほど動的バージョンで記述し直し、アップグレードされることを検証します。
依存ライブラリの導入
コミット:43ae29f
最初に、依存ライブラリとして Spring Boot Gradle Plugin(org.springframework.boot)および Spring Boot Starter(org.springframework.boot:spring-boot-starter)を app1 と app2 それぞれに導入します。
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 3ff2f2c..320b016 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -4,6 +4,11 @@
[versions]
kotlin = "2.1.0"
+springBoot = "3.3.13"
[libraries]
kotlinGradlePlugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" }
+springBootStarter = { module = "org.springframework.boot:spring-boot-starter", version.ref = "springBoot" }
+
+[plugins]
+springBootGradlePlugin = { id = "org.springframework.boot", version.ref = "springBoot" }
diff --git a/app1/build.gradle.kts b/app1/build.gradle.kts
index b0fd2d0..c0ace6f 100644
--- a/app1/build.gradle.kts
+++ b/app1/build.gradle.kts
@@ -1,6 +1,8 @@
plugins {
id("buildsrc.convention.kotlin-jvm")
+ alias(libs.plugins.springBootGradlePlugin)
}
dependencies {
+ implementation(libs.springBootStarter)
}
diff --git a/app2/build.gradle.kts b/app2/build.gradle.kts
index b0fd2d0..c0ace6f 100644
--- a/app2/build.gradle.kts
+++ b/app2/build.gradle.kts
@@ -1,6 +1,8 @@
plugins {
id("buildsrc.convention.kotlin-jvm")
+ alias(libs.plugins.springBootGradlePlugin)
}
dependencies {
+ implementation(libs.springBootStarter)
}
ここでは、Spring Boot のバージョンとして 3.3.13 を指定しています。後ほど動的バージョンで記述し直し、アップグレードされることを検証します。
依存ロックの有効化
コミット:8ff1725
依存ロックを有効化するには、ビルドスクリプトの dependencyLocking セクション中で lockAllConfigurations [2]を呼び出します。
dependencyLocking {
lockAllConfigurations()
}
重要なポイントとして、依存ロックの設定は各サブプロジェクトおよび buildSrc に対して記述する必要があります。そのため、設定は以下の2箇所に記述します。
- サブプロジェクトがビルドロジックを共有するコンベンションプラグインの中(例:
kotlin-jvm.gradle.kts) -
buildSrcそのものに対するビルドスクリプト(例:buildSrc/build.gradle.kts)
ロックファイルの生成
コミット:4aff070
ロックファイル gradle.lockfile には、固定された依存ライブラリ(推移的に依存するものを含む)のバージョンが記述されます。
# This is a Gradle generated file for dependency locking.
# Manual edits can break the build and are not advised.
# This file is expected to be part of source control.
ch.qos.logback:logback-classic:1.5.18=compileClasspath,implementationDependenciesMetadata,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
ch.qos.logback:logback-core:1.5.18=compileClasspath,implementationDependenciesMetadata,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
io.github.java-diff-utils:java-diff-utils:4.12=kotlinInternalAbiValidation
...
ロックファイルを生成するには、Gradle タスクの実行時に --write-locks オプションを指定します。
マルチプロジェクト構成の Gradle プロジェクトでは、ロックファイルも各サブプロジェクトおよび buildSrc に対して生成されます。そして陥りやすい点として、 公式ドキュメントではロックファイルの生成に gradle dependencies --write-locks コマンドを実行していますが、このように dependencies タスクをルートプロジェクトに対して実行しても、サブプロジェクトのロックファイルは生成されません[3]。
In a multi-project setup, note that
dependenciesis executed only on one project, typically the root project.
代替手段として、各サブプロジェクトに対して個別に dependencies タスクを実行する方法が考えられます。
gradle :buildSrc:dependencies --write-locks
gradle :app1:dependencies --write-locks
gradle :app2:dependencies --write-locks
しかし、個別にサブプロジェクトを指定して実行するのは煩雑です。回避する手段として、以下の2つが挙げられます。
build タスクを使う
プロジェクトパスを指定せずに dependencies タスクを実行すると、ルートプロジェクトのみが実行対象になるためロックファイルが生成されません。代わりに build タスクのような、すべてのサブプロジェクトを対象とするタスクを利用することで、一度のコマンド実行ですべてのサブプロジェクトのロックファイルを生成できます。
gradle build --write-locks
すべてのサブプロジェクトについて dependencies タスクを実行するタスクを定義する
コミット:3b290af
build タスクの利用には、ロックファイルの生成には本来不要なビルドを走らせてしまう問題があります。
別の手段として、すべてのサブプロジェクトの dependencies タスクに依存する新しいカスタムタスクを定義する方法があげられます。以下はそのようなタスクを allDependencies として定義する例です[4]。
tasks.register("allDependencies") {
dependsOn(
subprojects.flatMap { subproject ->
subproject.tasks.matching { it.name == "dependencies" }
}
)
}
この allDependencies タスクを --write-locks オプションとともに実行することで、すべてのサブプロジェクトのロックファイルを一度に生成することができます。
gradle allDependencies --write-locks
バージョンの範囲指定
コミット:d8df924
ロックファイルが生成されたので、Kotlin と Spring Boot のバージョンを動的バージョンを用いて範囲指定してみます。範囲指定はバージョンカタログの versions セクションに直接書くことができます。
[versions]
kotlin = "2.+"
springBoot = "3.+"
+ はバージョンを接頭辞(prefix)で指定するための記述子です。
この時点ではまだロックファイルが更新されていないので、ビルドを実行しても古いバージョン(kotlin = "2.1.0"、springBoot = "3.3.13")が利用されます。
ロックファイルの更新
ロックファイルを更新するには、生成したときと同様に --write-locks オプションを指定して dependencies タスクなどを実行します。
実行後、ロックファイルに記述された依存ライブラリのバージョンが更新された(kotlin = "2.2.20"、springBoot = "3.5.4")ことが確認できます。
diff --git i/buildSrc/gradle.lockfile w/buildSrc/gradle.lockfile
index fc38189..1f0a988 100644
--- i/buildSrc/gradle.lockfile
+++ w/buildSrc/gradle.lockfile
@@ -1,29 +1,32 @@
# This is a Gradle generated file for dependency locking.
# Manual edits can break the build and are not advised.
# This file is expected to be part of source control.
...
+org.jetbrains.kotlin:kotlin-compiler-runner:2.2.20-Beta2=buildScriptClasspath,runtimeClasspath
org.jetbrains.kotlin:kotlin-daemon-client:2.0.21=kotlinBuildToolsApiClasspath
-org.jetbrains.kotlin:kotlin-daemon-client:2.1.0=buildScriptClasspath,runtimeClasspath
+org.jetbrains.kotlin:kotlin-daemon-client:2.2.20-Beta2=buildScriptClasspath,runtimeClasspath
org.jetbrains.kotlin:kotlin-daemon-embeddable:2.0.21=kotlinBuildToolsApiClasspath,kotlinCompilerClasspath
-org.jetbrains.kotlin:kotlin-gradle-plugin-annotations:2.1.0=buildScriptClasspath,compileClasspath,runtimeClasspath
-org.jetbrains.kotlin:kotlin-gradle-plugin-api:2.1.0=buildScriptClasspath,compileClasspath,runtimeClasspath
-org.jetbrains.kotlin:kotlin-gradle-plugin-idea-proto:2.1.0=buildScriptClasspath,runtimeClasspath
-org.jetbrains.kotlin:kotlin-gradle-plugin-idea:2.1.0=buildScriptClasspath,runtimeClasspath
-org.jetbrains.kotlin:kotlin-gradle-plugin-model:2.1.0=buildScriptClasspath,compileClasspath,runtimeClasspath
-org.jetbrains.kotlin:kotlin-gradle-plugin:2.1.0=buildScriptClasspath,compileClasspath,runtimeClasspath
-org.jetbrains.kotlin:kotlin-gradle-plugins-bom:2.1.0=buildScriptClasspath,compileClasspath,runtimeClasspath
-org.jetbrains.kotlin:kotlin-klib-commonizer-api:2.1.0=buildScriptClasspath,runtimeClasspath
-org.jetbrains.kotlin:kotlin-native-utils:2.1.0=buildScriptClasspath,compileClasspath,runtimeClasspath
+org.jetbrains.kotlin:kotlin-gradle-plugin-annotations:2.2.20-Beta2=buildScriptClasspath,compileClasspath,runtimeClasspath
+org.jetbrains.kotlin:kotlin-gradle-plugin-api:2.2.20-Beta2=buildScriptClasspath,compileClasspath,runtimeClasspath
+org.jetbrains.kotlin:kotlin-gradle-plugin-idea-proto:2.2.20-Beta2=buildScriptClasspath,runtimeClasspath
+org.jetbrains.kotlin:kotlin-gradle-plugin-idea:2.2.20-Beta2=buildScriptClasspath,runtimeClasspath
+org.jetbrains.kotlin:kotlin-gradle-plugin-model:2.2.20-Beta2=buildScriptClasspath,compileClasspath,runtimeClasspath
+org.jetbrains.kotlin:kotlin-gradle-plugin:2.2.20-Beta2=buildScriptClasspath,compileClasspath,runtimeClasspath
+org.jetbrains.kotlin:kotlin-gradle-plugins-bom:2.2.20-Beta2=buildScriptClasspath,compileClasspath,runtimeClasspath
+org.jetbrains.kotlin:kotlin-klib-commonizer-api:2.2.20-Beta2=buildScriptClasspath,runtimeClasspath
+org.jetbrains.kotlin:kotlin-native-utils:2.2.20-Beta2=buildScriptClasspath,compileClasspath,runtimeClasspath
...
diff --git i/app1/gradle.lockfile w/app1/gradle.lockfile
index 1e2875b..81d8baa 100644
--- i/app1/gradle.lockfile
+++ w/app1/gradle.lockfile
@@ -3,49 +3,55 @@
# This file is expected to be part of source control.
...
org.slf4j:slf4j-api:2.0.17=compileClasspath,implementationDependenciesMetadata,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
-org.springframework.boot:spring-boot-autoconfigure:3.3.13=compileClasspath,implementationDependenciesMetadata,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
-org.springframework.boot:spring-boot-starter-logging:3.3.13=compileClasspath,implementationDependenciesMetadata,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
-org.springframework.boot:spring-boot-starter:3.3.13=compileClasspath,implementationDependenciesMetadata,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
-org.springframework.boot:spring-boot:3.3.13=compileClasspath,implementationDependenciesMetadata,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
-org.springframework:spring-aop:6.1.21=compileClasspath,implementationDependenciesMetadata,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
-org.springframework:spring-beans:6.1.21=compileClasspath,implementationDependenciesMetadata,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
-org.springframework:spring-context:6.1.21=compileClasspath,implementationDependenciesMetadata,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
-org.springframework:spring-core:6.1.21=compileClasspath,implementationDependenciesMetadata,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
-org.springframework:spring-expression:6.1.21=compileClasspath,implementationDependenciesMetadata,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
-org.springframework:spring-jcl:6.1.21=compileClasspath,implementationDependenciesMetadata,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
-org.yaml:snakeyaml:2.2=compileClasspath,implementationDependenciesMetadata,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
+org.springframework.boot:spring-boot-autoconfigure:3.5.4=compileClasspath,implementationDependenciesMetadata,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
+org.springframework.boot:spring-boot-starter-logging:3.5.4=compileClasspath,implementationDependenciesMetadata,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
+org.springframework.boot:spring-boot-starter:3.5.4=compileClasspath,implementationDependenciesMetadata,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
+org.springframework.boot:spring-boot:3.5.4=compileClasspath,implementationDependenciesMetadata,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
+org.springframework:spring-aop:6.2.9=compileClasspath,implementationDependenciesMetadata,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
+org.springframework:spring-beans:6.2.9=compileClasspath,implementationDependenciesMetadata,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
+org.springframework:spring-context:6.2.9=compileClasspath,implementationDependenciesMetadata,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
+org.springframework:spring-core:6.2.9=compileClasspath,implementationDependenciesMetadata,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
+org.springframework:spring-expression:6.2.9=compileClasspath,implementationDependenciesMetadata,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
+org.springframework:spring-jcl:6.2.9=compileClasspath,implementationDependenciesMetadata,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
+org.yaml:snakeyaml:2.4=compileClasspath,implementationDependenciesMetadata,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,test
ImplementationDependenciesMetadata,testRuntimeClasspath
まとめ
本記事では、マルチプロジェクトビルドで構成された Gradle プロジェクトに依存ロックを導入する方法を紹介しました。
余談:Dependabot によるロックファイルの自動更新
2025年6月に、Dependabot によって gradle.lockfile を自動的に更新する機能が GA になりました(参考:Dependabot support for Gradle lockfiles is now generally available - GitHub Changelog)。
検証用のリポジトリでも試してみましたが、まだうまく動作しない点があるようです。
- バージョンカタログが使われていると動作しない(参考:dependabot/dependabot-core#12557)
-
dependenciesタスクがルートプロジェクトに対してのみ実行されている
-
Elasticsearch(elastic/elasticsearch)に Gradle 依存ロックを導入した事例が紹介されています。 ↩︎
-
compileClasspathのような特定の依存コンフィギュレーション(dependency configuration)に対して依存ロックを有効化する場合は、configurationsセクションの中で対象のコンフィギュレーションに対してactivateDependencyLockingを呼び出します(参考:Activate locking for specific configurations)。 ↩︎ -
dependenciesタスクがサブプロジェクトのロックファイルを生成しない問題は gradle/gradle#9373 で議論されていますが、残念ながら修正は not planned になっています。 ↩︎ -
アイディアの元は gradle/gradle#9373 の コメントです。 ↩︎
Discussion