先日、言語の塩漬けビリティの話がネットで盛り上がっていた。
言語とそれをとりまくエコシステムの塩漬けビリティみたいな概念が、†生産性†みたいな概念とは独立した軸に存在しているような気がしており、JavaとかCは異常にこのへんが手堅く、だから選ばれてるのだと思う(Scalaはメジャーバージョン上げたときに色々壊したが、反省して3以降はちゃんとしている) https://t.co/TpWrDw3nGY
— Windymelt💀(めるくん)🚀❤️🔥 (@windymelt) December 25, 2024
Scalaの塩漬けビリティについてはまた後ほど記事にする予定だが、一般に塩漬けしやすいといったとき、最後にビルドに成功したときと同じライブラリが降ってくるという要素が大事になる。当たり前のように思えるが、ライブラリをホストしてくれているリポジトリが何らかの理由で侵害されたり、そこまで大仰な理由がなくとも、ビルドツールのライブラリ指定方法が甘くて「3.0.0以上」みたいに指定するスタイルの場合に、最新バージョンが変わって別のライブラリが降ってきてしまう、ということもありうる。しかも運悪くライブラリの行儀が悪いと、マイナーバージョンの変更でbreaking changesが起こったりする。
こうした事故を防ぐため、npmは勝手にpackage-lock.json
を作ってくれるが、sbtはそのような機能を標準で提供しないため、Discordで相談していたところsbt-dependency-lock
というツールの存在を教えてもらった。このツールを使うとbuild.sbt.lock
が生成され、ライブラリのJARファイルのダイジェストがロックファイルに記録されるようになる。
この記事では、このstringbean/sbt-dependency-lock
の概要とその使い方について紹介する。
また、この記事の例が収録されたサンプルプロジェクトは以下で利用できる:
stringbean/sbt-dependency-lock
とは
stringbean/sbt-dependency-lock
は、build.sbt
に対するロックファイルを生成・検証してくれるsbtプラグインである。
ちなみにsbt-dependency-lock
と似たsbt-lock
というプラグインもあるが、こちらはpublic archive扱いとなっており、もうメンテされていないので選ばなくてよい。
sbt-dependency-lock
のほうは後発で、ユーザビリティが改善されている。
インストール
sbt-dependency-lock
をインストールするには、プロジェクトのproject/plugins.sbt
に以下の記述を追加する:
addSbtPlugin("software.purpledragon" % "sbt-dependency-lock" % "1.5.1")
バージョンは執筆時点での最新なので、利用する際はScaladexなどで確認すること:
利用方法
このプラグインはゼロコンフィグで以下のコマンドをsbt上で利用できる:
dependencyLockWrite
build.sbt.lock
にロック情報を書き込む
dependencyLockCheck
build.sbt.lock
からロック情報を読み取り、これを現在手元にあるライブラリと突合して検証する
最初にdependencyLockCheck
を実行すると、ロックファイルが存在しないのでエラーを返す:
sbt:core> dependencyLockCheck [error] stack trace is suppressed; run last dependencyLockCheck for the full output [error] (dependencyLockCheck) Dependency lock check failed - missing lockfile [error] Total time: 0 s, completed Jan 1, 2025 6:28:14 PM
dependencyLockWrite
を実行してみる:
sbt:core> dependencyLockWrite [success] Total time: 0 s, completed Jan 1, 2025 6:30:33 PM
すると、build.sbt.lock
に以下のようなJSONファイルが出力される:
{ "lockVersion" : 1, "timestamp" : "2025-01-01T09:31:38.719Z", "configurations" : [ "compile", "optional", "provided", "runtime", "test" ], "dependencies" : [ { "org" : "org.scala-lang", "name" : "scala-library", "version" : "2.13.15", "artifacts" : [ { "name" : "scala-library.jar", "hash" : "sha1:ed6f1d58968b16c5f9067d5cac032d952552de58" } ], "configurations" : [ "compile", "runtime", "test" ] }, { "org" : "org.scala-lang", "name" : "scala3-library_3", "version" : "3.6.2", "artifacts" : [ { "name" : "scala3-library_3.jar", "hash" : "sha1:0c3f9f3da15f8c0cdea22ced65f6cf3745454e9f" } ], "configurations" : [ "compile", "runtime", "test" ] } ] }
この状態でふたたびdependencyLockCheck
してみよう:
sbt:core> dependencyLockCheck [info] Dependency lock check passed [success] Total time: 0 s, completed Jan 1, 2025 6:37:04 PM
チェックが行われ、問題ないことが表示された。
build.sbt.lock
とは
このbuild.sbt.lock
ファイルにはプロジェクトの依存性に指定されたライブラリ(標準ライブラリを含む)とその推移的依存関係にあるライブラリが格納されたJARファイルに対するダイジェストが記録されている。
また、このライブラリがどこで利用されているか(コンパイルタイミングか、ランタイムか、テストタイミングか)も記載される。
このファイルはバージョン管理システムにコミットする。
普段の開発環境での利用
sbt-dependency-lock
は、ライブラリの解決フェーズ(具体的にはupdate
が実行された後)で自動的にロックファイルと手元にあるライブラリとの整合性を確認してくれる。
デフォルトの設定では、ライブラリの整合性チェックに失敗したとき(例えばロックファイルが存在しなかったり、ダイジェストが合致しなかったり、新たにライブラリが増えたとき)に警告が表示される。この設定は変更可能だ(後述)。
CIでの利用
CIでは、明示的にsbt dependencyLockCheck
を追加しておくことでロックファイルのチェックが行われるようにできる。チェックに失敗した場合はそのままコマンドも失敗する。明示的にdependencyLockCheck
を実行する場合は、整合性確認できなかったときは常に失敗してくれるので便利だ。この振舞いはnpm ci
に近い。
設定項目
正確な内容については以下のページを参照すること。
dependencyLockFile
ロックファイルのパスとファイル名を設定できる。
dependencyLockAutoCheck
ライブラリ解決フェーズで実行される自動チェックの厳しさを設定できる。
DependencyLockUpdateMode.CheckDisabled
- 自動チェックを行わない。
DependencyLockUpdateMode.WarnOnError
- 差分が検出された場合は警告を表示する。デフォルト。
DependencyLockUpdateMode.FailOnError
- 差分が検出された場合は処理を失敗させる。より厳しい。
DependencyLockUpdateMode.AutoUpdate
- 差分が検出された場合はロックファイルの方を更新する。
npm install
に近い挙動。
- 差分が検出された場合はロックファイルの方を更新する。
dependencyLockModuleFilter
特定のモジュールをチェック対象外にできる。詳細は公式ページを参照。
dependencyLockConfigurationFilter
特定のコンフィギュレーション(コンパイルとかテストとかランタイムとかの区分)をチェック対象外にできる。詳細は公式ページを参照。
まとめ
sbt-dependency-lock
を使ってプロジェクトの依存性のチェックを行う方法について解説した。このプラグインは他のsbtプラグインそのもののチェックまではやらないので、そこまで確認するためにはproject
ディレクトリにさらにプラグインを置いて二段階でチェックする必要があるかもしれない。