GitHubでRustのリポジトリを作成したとき、自動生成された .gitignoreを見て気が付いた。
デフォルトでパッケージ管理におけるロックファイルであるCargo.lockがignoreされている。また、アプリケーション(executable)を開発するときはこの行は削除し、ライブラリ開発のときはgitignoreに残すように指示がある。
# Generated by Cargo
# will have compiled files and executables
debug/
target/
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
Cargo.lock
# These are backup files generated by rustfmt
**/*.rs.bk
# MSVC Windows builds of rustc generate these, which store debugging information
*.pdb
# RustRover
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
なぜライブラリの場合はlockファイルをバージョン管理しないのか
参考として提示されているリンクを参照すると、なぜlockファイルが必要かの説明はあったが、なぜライブラリとexecutableではlockファイルをバージョン管理に含めるか否かが異なるかの説明は無かった。
ライブラリにおいてlockファイルをバージョン管理に含めるべきでないのは、依存モジュールのバージョンがロックされていると他のライブラリと組合せることができなくなるためだと理解している。
例えばmyappが外部モジュールのfooとbarに、fooとbarはそれぞれhogeに依存しているとする。myappはexecutableなのでlockファイルをバージョン管理に含めるべきで、これにより常に同一の依存モジュールでビルドされる。一方で、fooとbarがlockファイルを含んでおり、それぞれhoge-1.0.0とhoge-2.0.0を指定していた場合、myappのビルド時にhoge-1.0.0とhoge-2.0.0の両方が必要になる。
多くのパッケージ管理ツールおよびプログラミング言語において同一名称(namespace)で異なるバージョンのモジュールを同時に利用することはできない。このため、myappビルド時に依存するfooとbarがそれぞれ異なるバージョンのhogeに依存しているのでmyappのビルドに失敗する。
このような状態を避けるため、ライブラリ用途の場合はlockファイルを含めず、かつ依存するhogeモジュールには幅広いバージョンに対応できるようにしておく。これによりmyappビルド時に利用するhogeモジュールのバージョンはfooとbarの制約から自由に選択できる。
当然ながら、ライブラリにおけるCargo.tomlなどのマニフェストファイルにて依存モジュールのバージョン制約が厳しいとlockファイルが含まれていなくても同様の問題が起きるので、ライブラリには幅広い依存モジュールバージョンに対応できることが望ましい。
他のパッケージ管理
上記事情はRust特有のものではなく、他のプログラミング言語やパッケージ管理においても同様。ただし、プログラミング言語によって事情は多少異なる。
node.js (npm)
npmにおいては上記問題は発生しない。npmはパッケージ依存が木構造で管理され、異なるバージョンのモジュールを同時に利用できる。すなわち、hoge-1.0.0とhoge-2.0.0を同時に利用できる。ただし、hogeが必要になる度に重複してモジュールをインストールする必要がある。このような仕組みをnpmのDuplicationと呼ぶらしい。
このような特徴を持つ言語およびパッケージ管理の仕組みは他には無さそうなので、npm特有の特性みたい。
Python
PythonもRust同様、executableの場合はlockファイルをバージョン管理に含めて、ライブラリの場合は含めないことを推奨している。また、.python-versionについてもライブラリの場合はignoreしろとある。
Rustと違うのはデフォルトではgitignoreでコメントアウトされており、デフォルトでcommit対象としているところ。
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock
# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
#pdm.lock
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
# in version control.
# https://pdm.fming.dev/latest/usage/project/#working-with-version-control
.pdm.toml
.pdm-python
.pdm-build
またPoetryの公式ドキュメントには、アプリケーション開発の場合はlockファイルをコミットすること、ライブラリ開発の場合はコミットすべきか検討することが記載さている。特にライブラリ開発であってもlockファイルをコミットしておけばテストエラー時の原因調査が楽になること、lockファイルが無いとlockファイルを新規に生成するため処理に時間が掛かることが懸念点として挙げられている。
Java
Javaも単一バージョンのモジュールしか利用できないという基本方針ではあるが、回避策として classloader isolationという方法があるらしい。
パッケージ管理で対処するのではなく、アプリケーションコード側で特定バージョンのクラスファイルをロードして利用する。
ただし、だいぶ力技なのでどの程度実用的かは不明。少なくとも自分は使った事は無い。