Lockfiles
Lockfiles are a mechanism to achieve reproducible dependencies, even when new versions or revisions of those dependencies are created. Let’s see it with a practical example, start cloning the examples2 repository:
$ git clone https://github.com/conan-io/examples2.git
$ cd examples2/tutorial/versioning/lockfiles/intro
In this folder we have a small project, consisting in 3 packages: a matrix
package, emulating some mathematical library, an engine
package emulating some game engine, and a sound32
package, emulating a sound library for some 32bits systems. These packages are actually most empty, they do not build any code, but they are good to learn the concepts of lockfiles.
digraph lockfiles { node [fillcolor=”lightskyblue”, style=filled, shape=box] rankdir=”BT” “engine/1.0” -> “matrix/1.0”; “engine/1.0” -> “sound32/1.0” [label=”if arch==x86”]; }
We will start by creating the first matrix/1.0
version:
$ conan create matrix --version=1.0
Now we can check in the engine
folder its recipe:
class Engine(ConanFile):
name = "engine"
settings = "arch"
def requirements(self):
self.requires("matrix/[>=1.0 <2.0]")
if self.settings.arch == "x86":
self.requires("sound32/[>=1.0 <2.0]")
Lets move to the engine
folder and install its dependencies:
$ cd engine
$ conan install .
...
Requirements
matrix/1.0#905c3f0babc520684c84127378fefdd0 - Cache
Resolved version ranges
matrix/[>=1.0 <2.0]: matrix/1.0
As the matrix/1.0
version is in the valid range, it is resolved and used. But if someone creates a new matrix/1.1
or 1.X
version, it would also be automatically used, because it is also in the valid range. To avoid this, we will capture a “snapshot” of the current dependencies creating a conan.lock
lockfile:
$ conan lock create .
$ cat conan.lock
{
"version": "0.5",
"requires": [
"matrix/1.0#905c3f0babc520684c84127378fefdd0%1675278126.0552447"
],
"build_requires": [],
"python_requires": []
}
We can see how the created conan.lock
lockfile contains the matrix/1.0
version and its revision. But sound32/1.0
is not in the lockfile, because for the default configuration profile (not x86
), this sound32
is not a dependency.
Now, a new matrix/1.1
version is created:
$ cd ..
$ conan create matrix --version=1.1
$ cd engine
And see what happens when we issue a new conan install
command for the engine:
$ conan install .
# equivalent to conan install . --lockfile=conan.lock
...
Requirements
matrix/1.0#905c3f0babc520684c84127378fefdd0 - Cache
As we can see, the new matrix/1.1
was not used, even if it is in the valid range! This happens because by default the --lockfile=conan.lock
will be used if the conan.lock
file is found. The locked matrix/1.0
version and revision will be used to resolve the range, and the matrix/1.1
will be ignored.
Likewise, it is possible to issue other Conan commands, and if the conan.lock
is there, it will be used:
$ conan graph info . --filter=requires # --lockfile=conan.lock is implicit
# display info for matrix/1.0
$ conan create . --version=1.0 # --lockfile=conan.lock is implicit
# creates the engine/1.0 package, using matrix/1.0 as dependency
If using a lockfile is intended, like in CI, it is better that the argument --lockfile=conan.lock
explicit.
Multi-configuration lockfiles
We saw above that the engine
has a conditional dependency to the sound32
package, in case the architecture is x86
. That also means that such sound32
package version was not captured in the above lockfile.
Lets create the sound32/1.0
package first, then try to install engine
:
$ cd ..
$ conan create sound32 --version=1.0
$ cd engine
$ conan install . -s arch=x86 # FAILS!
ERROR: Requirement 'sound32/[>=1.0 <2.0]' not in lockfile
This happens because the conan.lock
lockfile doesn’t contain a locked version for sound32
. By default lockfiles are strict, if we are locking dependencies, a matching version inside the lockfile must be found. We can relax this assumption with the --lockfile-partial
argument:
$ conan install . -s arch=x86 --lockfile-partial
...
Requirements
matrix/1.0#905c3f0babc520684c84127378fefdd0 - Cache
sound32/1.0#83d4b7bf607b3b60a6546f8b58b5cdd7 - Cache
Resolved version ranges
sound32/[>=1.0 <2.0]: sound32/1.0
This will manage to partially lock to matrix/1.0
, and resolve sound32
version range as usual. But we can do better, we can extend our lockfile to also lock sound32/1.0
version, to avoid possible disruptions caused by new sound32
unexpected versions:
$ conan lock create . -s arch=x86
$ cat conan.lock
{
"version": "0.5",
"requires": [
"sound32/1.0#83d4b7bf607b3b60a6546f8b58b5cdd7%1675278904.0791488",
"matrix/1.0#905c3f0babc520684c84127378fefdd0%1675278900.0103245"
],
"build_requires": [],
"python_requires": []
}
Now, both matrix/1.0
and sound32/1.0
are locked inside our conan.lock
lockfile. It is possible to use this lockfile for both configurations (64bits, and x86 architectures), having versions in a lockfile that are not used for a given configuration is not an issue, as long as the necessary dependencies for that configuration find a matching version in it.
Important
Lockfiles contains sorted lists of requirements, ordered by versions and revisions, so latest versions and revisions are the ones that are prioritized when resolving against a lockfile. A lockfile can contain two or more different versions of the same package, just because different version ranges require them. The sorting will provide the right logic so each range resolves to each valid versions.
If a version in the lockfile doesn’t fit in a valid range, it will not be used. It is not possible for lockfiles to force a dependency that goes against what conanfile
requires define, as they are “snapshots” of an existing/realizable dependency graph, but cannot define an “impossible” dependency graph.
Evolving lockfiles
Even if lockfiles enforce and constraint the versions that can be resolved for a graph, it doesn’t mean that lockfiles cannot evolve. Actually, controlled evolution of lockfiles is paramount to important processes like Continuous Integration, when the effect of one change in the graph wants to be tested in isolation of other possible concurrent changes.
In this section we will introduce some of the basic functionality of lockfiles that allows such evolution.
First, if we would like now to introduce and test the new matrix/1.1
version in our engine
, without necessarily pulling many other dependencies that could have got new versions too, we could manually add matrix/1.1
to the lockfile:
$ Running: conan lock add --requires=matrix/1.1
$ cat conan.lock
{
"version": "0.5",
"requires": [
"sound32/1.0#83d4b7bf607b3b60a6546f8b58b5cdd7%1675278904.0791488",
"matrix/1.1",
"matrix/1.0#905c3f0babc520684c84127378fefdd0%1675278900.0103245"
],
"build_requires": [],
"python_requires": []
}
To be clear: manually adding with conan lock add
is not necessarily a recommended flow, it is possible to automate the task with other approaches, that will be explained later. This is just an introduction to the principles and concepts.
The important idea is that now we got 2 versions of matrix
in the lockfile, and matrix/1.1
is before matrix/1.0
, so for the range matrix/[>=1.0 <2.0]
, the first one (matrix/1.1
) would be prioritized. That means that when now the new lockfile is used, it will resolve to matrix/1.1
version (even if a matrix/1.2
or higher version existed in the system):
$ conan install . -s arch=x86 --lockfile-out=conan.lock
Requirements
matrix/1.1#905c3f0babc520684c84127378fefdd0 - Cache
sound32/1.0#83d4b7bf607b3b60a6546f8b58b5cdd7 - Cache
$ cat conan.lock
{
"version": "0.5",
"requires": [
"sound32/1.0#83d4b7bf607b3b60a6546f8b58b5cdd7%1675278904.0791488",
"matrix/1.1#905c3f0babc520684c84127378fefdd0%1675278901.7527816",
"matrix/1.0#905c3f0babc520684c84127378fefdd0%1675278900.0103245"
],
"build_requires": [],
"python_requires": []
}
Note that now matrix/1.1
was resolved, and it also got its revision
stored in the lockfile (because --lockfile-out=conan.lock
was passed as argument).
It is true that the former matrix/1.0
version was not used. As said above, having old versions in the lockfile that are not used is not harmful. However, if we want to prune the unused versions and revisions, we could use the --lockfile-clean
for that purpose:
$ conan install . -s arch=x86 --lockfile-out=conan.lock --lockfile-clean
...
Requirements
matrix/1.1#905c3f0babc520684c84127378fefdd0 - Cache
sound32/1.0#83d4b7bf607b3b60a6546f8b58b5cdd7 - Cache
...
$ cat conan.lock
{
"version": "0.5",
"requires": [
"sound32/1.0#83d4b7bf607b3b60a6546f8b58b5cdd7%1675278904.0791488",
"matrix/1.1#905c3f0babc520684c84127378fefdd0%1675278901.7527816"
],
"build_requires": [],
"python_requires": []
}
It is relevant to note that the -lockfile-clean
could remove locked versions in given configurations. For example, if instead of the above, the x86_64
architecture is used, the --lockfile-clean
will prune the “unused” sound32
, because in that configuration is not used. It is possible to evaluate new lockfiles for every different configuration, and then merge them:
$ conan lock create . --lockfile-out=64.lock --lockfile-clean
$ conan lock create . -s arch=x86 --lockfile-out=32.lock --lockfile-clean
$ cat 64.lock
{
"version": "0.5",
"requires": [
"matrix/1.1#905c3f0babc520684c84127378fefdd0%1675294635.6049662"
],
"build_requires": [],
"python_requires": []
}
$ cat 32.lock
{
"version": "0.5",
"requires": [
"sound32/1.0#83d4b7bf607b3b60a6546f8b58b5cdd7%1675294637.9775107",
"matrix/1.1#905c3f0babc520684c84127378fefdd0%1675294635.6049662"
],
"build_requires": [],
"python_requires": []
}
$ conan lock merge --lockfile=32.lock --lockfile=64.lock --lockfile-out=conan.lock
$ cat conan.lock
{
"version": "0.5",
"requires": [
"sound32/1.0#83d4b7bf607b3b60a6546f8b58b5cdd7%1675294637.9775107",
"matrix/1.1#905c3f0babc520684c84127378fefdd0%1675294635.6049662"
],
"build_requires": [],
"python_requires": []
}
This multiple-clean + merge operation is not something that developers should do, only CI scripts, and for some advanced CI flows that will be explained later.
See also
- Continuous Integrations links.