For at least a decade, the prevailing wisdom in software development has been to check in your lock file for apps, but not for libraries. I humbly suggest that we should check in lock files for libraries as well.
If your project is open source (regardless of whether it is an application or a library), you can help contributors to your project by providing them the same dependencies you are using. This is especially true for your testing dependencies.
Why even have lock files?
One could argue that semantic versioning is sufficient. If authors signal compatibility in their versioning, and our package management tools honor SemVer, do we really need to know exactly which versions were used previously?
Of course we need to know. Sometimes, backwards-incompatible changes are introduced in dependencies without bumping the major version number. Sometimes library authors don't know the change was incompatible. Sometimes there are compatibility issues between dependencies that just have not been discovered yet.
I probably do not need to convince you "lock files are good."
Should we share the lock files?
Yes. This has been covered by others for a long time. I will not belabor the point.
If you have an application, you need a reproducible build. For a reproducible build, you need a lock file.
What about for libraries?
The perennial argument comes from Yehuda Katz: https://yehudakatz.com/2010/12/16/clarifying-the-roles-of-the-gemspec-and-gemfile/
More recently, I found the same advice in the Rust community. https://doc.rust-lang.org/cargo/faq.html#why-do-binaries-have-cargolock-in-version-control-but-not-libraries
Essentially, when you build your app, your build tool will resolve version compatibility for transitive dependencies. Library lock files will be ignored in this process.
If you are authoring a library, you need only specify a version compatibility range. To avoid signaling that these exact version dependencies will be used downstream, do not bother checking in your lock file. (So the argument goes.)
However, if you are not going to check it in …
Why even have a lock file?
Well, we already discussed this a few paragraphs ago. The same arguments apply for your local development of libraries.
Imagine you run your test suite, but first your app builds with freshly updated dependencies. Now image, one of those dependencies changes the interface (or worse, the semantics) of a method you use in your tests. Now your test suite has failed, or your app has failed to compile.
Was the failure due to changes in your code, changes in your tests, changes in your dependencies? It is not immediately clear.
Locally, using a lock file removes some possible non-determinism in your tests.
If you still need your lock file for local library development, then you should probably check it in.
In fact, I need you to check it in.
Help me help you
On more than one occasion, I have found a bug in a library, and when I went to submit a pull request with a possible fix—as a good Internet citizen—I first ran the test suite. Then, the test suite failed to run because the versions of dev dependencies were incompatible with each other or with the test suite itself.
If the lock file had been checked in, I would have run the test suite exactly as the original authors had intended. I would have had a much easier time contributing to the health of the project.
Don't take my word for it
It turns out, I am not the only one suggesting this.
In writing this article, I looked to see how other communities handle this.
Yarn suggests you should check in your lock file for all projects.
So far we’ve been talking about dependencies as if there were only one type of dependency when in fact there are several different types.
…
When working on a library, it is far more likely that a development dependency breaks just because there are more of them that could break.
Composer is less committal, but suggests you may wish to check in your file:
This can help your team to always test against the same dependency versions.
Dependency groups
It is interesting, if not ironic, that Yarn gets this right where Ruby/Bundler and Rust/Cargo miss this nuance. In Yarn and NPM, you have only two classes of dependencies dependencies
and devDependencies
.
However, Bundler provides a means of specifying arbitrary groups. The Gemfile
"is just Ruby." It is common to see a Rails application's Gemfile
with the following:
# gems required for running the application
gem 'rails', '~>6.0.2'
…
group :development do
# gems required only for local dev/debugging
gem 'my_awsome_debugging_tool', '~> 1.0.3'
…
end
group :test do
# gems required only for running the test suite
gem 'my_awesome_test_runner', '~> 6.0.1'
…
end
Clearly, they recognize that you have multiple kinds of dependencies. There's a distinction not just between your run-time dependencies and "dev." They also distinguish between testing and development. (This is useful if you have debugging tools that you would consider insecure on your CI servers, for example.)
With Rust and Cargo, you also have a way of specifying arbitrary groups of dependencies with Profiles. There are some built in:
Profiles provide a way to alter the compiler settings, influencing things like optimizations and debugging symbols.
Cargo has 4 built-in profiles:
dev
,release
,test
, andbench
Not only do they recognize release, dev, and test. They recognize the distinctions in the different types of testing you do (e.g. verification vs performance) and the variety of dependencies they require.
With all these different ways of using a library (building an application with them, testing them, bench-marking their performance), we need to share our lock files.
Thus, I now find myself in the unexpected position of asking Ruby and Rust to be more like JavaScript and PHP.
For the sake of your project community, please check in your lock file.