Multi-repository configuration management: just-lock
May 23, 2025 ยท View on GitHub
The multi-repository build configuration used as input by just-mr acts for
all intents and purposes as a lockfile for justbuild projects, containing
the pinned versions of all content-fixed repositories. This is expected to be
stored and shipped together with the source code, allowing a consistent
development environment for all users.
With dependencies of projects coming in many shapes and forms, just-lock is
a tool for generating and maintaining the multi-repositories configuration
(lock)file of justbuild projects. This tool performs several functionalities,
such as importing dependencies with repository composition (extending the
functionality available in the just-import-git tool), automatic deduplication
of identical transitive dependencies (a functionality available also standalone
in the just-deduplicate-repos tool), or setup of repository clones for local
development. For the purposes of this tutorial, we will focus only on the
dependency import aspects.
Basic use of just-lock
In order to produce the multi-repository build configuration file, just-lock
expects an input configuration file. The format of this file is an extension of
the one of just-mr and it is read as a JSON object. The file defines four
elements:
- the description of local repositories, as the value for mandatory field
"repositories". This usually defines local checkouts, patches, overlays. - the description of remote dependencies, as the optional value for field
"imports". For justbuild dependencies, their multi-repository configuration (lock)file is taken as the ground truth for importing repository descriptions into the current project. Non-justbuild dependencies can also be described here, but can only be imported as-is, with overlays defining how such source code should be integrated (as it will be shown below). - the repository to consider as the main entry point for the build, as the
value of the optional field
"main". - a list of repository names, as the value of the field
"keep". These names will be kept in the final configuration during the automatic process of deduplicating any repeated repositories brought in as transitive dependencies from different imports. For the purposes of this tutorial, the use of this field is not exemplified.
The input file
For the following, we return to the hello_world example from the section on Building Third-party dependencies, which depends on the open-source project fmtlib, in the setup that enables high-level target caching.
We define the following repos.in.json input configuration file for
just-lock:
{ "main": "tutorial"
, "repositories":
{ "rules-cc":
{ "repository": "rules-cc-rules-sources"
, "target_root": "tutorial-defaults"
, "rule_root": "rules-cc"
}
, "tutorial":
{ "repository": {"type": "file", "path": "."}
, "bindings": {"rules": "rules-cc", "format": "fmtlib"}
}
, "tutorial-defaults":
{ "repository":
{ "type": "file"
, "path": "./tutorial-defaults"
, "pragma": {"to_git": true}
}
}
, "fmt-targets-layer":
{ "repository":
{ "type": "file"
, "path": "./fmt-layer"
, "pragma": {"to_git": true}
}
}
, "fmtlib":
{ "repository": "fmtlib-sources"
, "target_root": "fmt-targets-layer"
, "bindings": {"rules": "rules-cc"}
}
}
, "imports":
[ { "source": "git"
, "branch": "master"
, "commit": "7a2fb9f639a61cf7b7d7e45c7c4cea845e7528c6"
, "url": "https://github.com/just-buildsystem/rules-cc.git"
, "repos": [{"alias": "rules-cc-rules-sources", "repo": "rules"}]
}
, { "source": "git"
, "branch": "8.1.1"
, "url": "https://github.com/fmtlib/fmt.git"
, "repos": [{"alias": "fmtlib-sources"}]
, "as plain": true
}
]
}
As already mentioned, the "imports" field provides a description of the
external (usually third-party) dependencies of a project. In our case, we
use the rules-cc and fmtlib libraries, described as git sources.
The first thing to note is the use of overlays. In our simple project, as we
do not want to import anything other than the source trees of the needed
dependencies, we create the "rules-cc-rules-sources" and "fmtlib-sources"
names, which will bind the workspace roots of our "rules-cc" and "fmtlib"
repositories, respectively, to the descriptions of the remote repositories
providing the necessary source trees.
The first import description object is for rules-cc, which is a justbuild
project hosted as a Git repository, from which we would like to import its
"rules" subdirectory. Thankfully, that project offers a useful shorthand by
defining in its own locked repositories configuration file the "rules" overlay
repository pointing to the respective subdirectory. just-lock will read that
configuration file in order to produce the resulting configuration, caching any
fetched source trees. It is thus highly recommended that the same build root as
the one subsequently used by just-mr is provided to just-lock (via the
--local-build-root option, with same default behaviour as in just-mr).
In the case of fmtib, which is also hosted as a Git repository but does not
provide a justbuild configuration file, we can only import it as-is, as a
complete repository, signaled by setting the "as plain" flag. Do note that in
this case we can limit ourselves to also just providing the "branch" field,
and not also a specific commit. This is because just-lock automatically
interrogates the remote in order to retrieve the top commit associated to a
certain reference (in this case, a release tag) and pin it to a hard reference
(in this case, the commit identifier) into the output configuration. This is a
useful feature, as it is often the case that information about dependencies
comes in the form of "loose" references, such as release tags or even simply the
remote location of a distfile, but which just-lock then will pin down to a
content-defined reference, such as a commit, blob, or tree identifier.
Generating the configuration
We generate an output configuration file repos.out.json by running just-lock
with the appropriate arguments, then we build the helloworld target with this
configuration:
$ just-lock -C repos.in.json -o repos.out.json
[...]
$
$ just-mr -C repos.out.json build helloworld
INFO: Performing repositories setup
INFO: Found 5 repositories involved
INFO: Setup finished, exec ["just","build","-C","...","helloworld"]
INFO: Requested target is [["@","tutorial","","helloworld"],{}]
INFO: Analysed target [["@","tutorial","","helloworld"],{}]
INFO: Export targets found: 1 cached, 0 uncached, 0 not eligible for caching
INFO: Discovered 4 actions, 0 tree overlays, 2 trees, 0 blobs
INFO: Building [["@","tutorial","","helloworld"],{}].
INFO: Processed 4 actions, 4 cache hits.
INFO: Artifacts built, logical paths are:
helloworld [18d25e828a0176cef6fb029bfd83e1862712ec87:132736:x]
$
As it can be seen, everything comes from cache and the new configuration behaves
identical to the manually written one. If available, one can inspect the
difference in content between the two files using the jq command-line tool:
$ diff -y <(jq --sort-keys . repos.out.json) <(jq --sort-keys . repos.json)
{ {
"main": "tutorial", "main": "tutorial",
"repositories": { "repositories": {
"fmt-targets-layer": { "fmt-targets-layer": {
"repository": { "repository": {
"path": "./fmt-layer", "path": "./fmt-layer",
"pragma": { "pragma": {
"to_git": true "to_git": true
}, },
"type": "file" "type": "file"
} }
}, },
"fmtlib": { "fmtlib": {
"bindings": { "bindings": {
"rules": "rules-cc" "rules": "rules-cc"
}, },
"repository": "fmtlib-sources", <
"target_root": "fmt-targets-layer" <
}, <
"fmtlib-sources": { <
"repository": { "repository": {
"branch": "8.1.1", "branch": "8.1.1",
"commit": "b6f4ceaed0a0a24ccf575fab6c56dd50ccf6f1a9", "commit": "b6f4ceaed0a0a24ccf575fab6c56dd50ccf6f1a9",
"repository": "https://github.com/fmtlib/fmt.git", "repository": "https://github.com/fmtlib/fmt.git",
"type": "git" "type": "git"
} | },
> "target_root": "fmt-targets-layer"
}, },
"rules-cc": { "rules-cc": {
"repository": "rules-cc-rules-sources", <
"rule_root": "rules-cc", <
"target_root": "tutorial-defaults" <
}, <
"rules-cc-rules-sources": { <
"repository": { "repository": {
"branch": "master", "branch": "master",
"commit": "7a2fb9f639a61cf7b7d7e45c7c4cea845e7528c6", "commit": "7a2fb9f639a61cf7b7d7e45c7c4cea845e7528c6",
"repository": "https://github.com/just-buildsystem/ru "repository": "https://github.com/just-buildsystem/ru
"subdir": "rules", "subdir": "rules",
"type": "git" "type": "git"
} | },
> "rule_root": "rules-cc",
> "target_root": "tutorial-defaults"
}, },
"tutorial": { "tutorial": {
"bindings": { "bindings": {
"format": "fmtlib", "format": "fmtlib",
"rules": "rules-cc" "rules": "rules-cc"
}, },
"repository": { "repository": {
"path": ".", "path": ".",
"type": "file" "type": "file"
} }
}, },
"tutorial-defaults": { "tutorial-defaults": {
"repository": { "repository": {
"path": "./tutorial-defaults", "path": "./tutorial-defaults",
"pragma": { "pragma": {
"to_git": true "to_git": true
}, },
"type": "file" "type": "file"
} }
} }
} }
} }
Except for the two overlays, kept from the just-lock input file configuration,
the two configuration files have the same content.