This guide describes how the pinned command-line tools are provided and maintained. For normal usage, see Pinned command-line tools.
The approach complements the general direction in DR-001 Infrastructure Design Decision.
One catalog is delivered through two execution paths:
| Component | Responsibility |
|---|---|
tools/lockfiles/*.lock.json and tools/lockfiles/python_tools.bzl |
Define versions and delivery metadata. |
MODULE.bazel |
Makes native tool lockfiles available to rules_multitool. |
tools/BUILD.bazel |
Exposes public Bazel targets for each command. |
| Feature installers | Install the commands exposed on the DevContainer's PATH. |
tools/run-tool |
Uses the command on PATH in a container when available; otherwise uses the public Bazel alias. |
tools/README.md |
Documents every command and its version from the catalog. |
A command is available when its catalog metadata, Bazel target, and applicable DevContainer installer are registered together. The runner selects between those delivery paths; the catalogs remain the command registries.
The public interface is .devcontainer/run-tool in each consumer
repository. Inside a container, it uses the command installed on PATH when
available. Otherwise, it invokes the matching public Bazel target. Developers
use the same command line without choosing the execution path.
Native tools with upstream release artifacts use
rules_multitool.
The multitool_aliases helper exposes the rules_multitool cwd target as
the public command. That wrapper restores BUILD_WORKING_DIRECTORY before
starting a tool, so configuration discovery and repository-relative paths
behave like a direct invocation.
Python packages do not provide the platform-specific, checksum-addressed
release binaries expected by rules_multitool. Their public Bazel targets
therefore use the uvx binary from rules_multitool to create an isolated
environment for the exact catalogued package version. A shell launcher restores
the caller's working directory before invoking uvx; using shell here is
intentional so Python-based tools do not require a system Python installation
on the host.
The DevContainer takes the other delivery path: it installs each Python tool
once with the catalogued uv binary and exposes the resulting entrypoint on
PATH. Python is already part of the image before feature tools are installed,
so the installer can use its standard library without adding a container or
host dependency.
The maintained runner source is run-tool. It remains under
tools/ in this implementation repository; consumer repositories copy it to
.devcontainer/run-tool. This repository invokes that source directly from
its pre-commit configuration. Consumer documentation only presents the copied
.devcontainer/run-tool path.
Native tool metadata lives in
tools/lockfiles/*.lock.json. Each lockfile records versions,
supported platforms, download URLs, checksums, and archive layouts.
rules_multitool consumes the files for Bazel;
devcontainer/install.py consumes them while
building the DevContainer.
Python command-line tool metadata lives in
tools/lockfiles/python_tools.bzl. The file is
a data-only Starlark dictionary because Bazel must read the pin during analysis,
before the command can run. Its restricted literal form is also valid Python
syntax. The privileged DevContainer installer parses it with
ast.literal_eval, preserving a data-only privilege boundary. This shared
format keeps the package, console entrypoint, version, and description in one
source of truth while Bazel execution remains independent of host Python.
The catalogs have distinct ownership: devcontainer-lock.json records external
DevContainer features, python_tools.bzl records Python package releases, and
uv.lock.json records the installer runtime. Each version is therefore owned
by the component that resolves it.
The published score_devcontainer Bazel module and DevContainer image share a
release version. Consumers using both must pin the same release.
For a tool distributed as a native release artifact, make the following changes together:
-
Add or update the multitool-compatible
tools/lockfiles/<command>.lock.json, including adescriptionfield for each tool entry (used to generate the README table). -
Register the lockfile with
multitool.hubin the rootMODULE.bazel. -
Add
multitool_aliases("<command>")totools/BUILD.bazel. -
Add the command to the appropriate feature installer:
s-core-local/install.shfor general tools orbazel-feature/install.shfor Bazel-specific tools. -
Read the lockfile version and test the installed command in the matching feature test:
s-core-local/tests/test_default.shorbazel-feature/tests/test_default.sh. -
Regenerate the documented command table:
$ python3 tools/internal/sync_readme.py -
If this repository's own DevContainer needs the command, add it to
.devcontainer/post_create_command.sh.
For a Python command-line tool, add an entry to
tools/lockfiles/python_tools.bzl, add python_tool("<command>") to
tools/BUILD.bazel, and install it through install.py install-python in
each applicable DevContainer installer. Keep the catalog as a single literal
PYTHON_TOOLS assignment: this is what makes it safe for the installer and
directly loadable by Bazel. The package selects the distribution passed to
uv, while entrypoint names the console command because those names need not
be identical.
Bazel invokes Python tools on demand with the pinned uvx; DevContainers
install the same package with the pinned uv. Add launcher and installer
coverage to python_tool_runner_test when either delivery contract changes.
Keep related commands such as uv and uvx in one lockfile when upstream
publishes them together. The installer can locate a command in a differently
named lockfile, but an explicit --lockfile remains available for ambiguous
cases.
To remove a command, remove it from the same integration points, including
its description field. Then run sync_readme.py (see below); it rejects
lockfile entries without a description.
Regenerate the user-facing command table after changing a lockfile or a description:
$ python3 tools/internal/sync_readme.pyThe pre-commit hook runs the same command; pre-commit rejects the commit if
running it changes tools/README.md, so a stale table cannot be committed.
Run the feature test for every installer changed. The feature tests read their expected versions from the catalog, which verifies that the DevContainer and lockfiles remain aligned. Publish the Bazel module and the DevContainer image with the same release version so consumer repositories can pin one version for both delivery paths.
python_tool_runner_test uses a fake uv executable to verify arguments,
environment variables, and working-directory behavior without network access.
CI additionally runs the real //tools:pre-commit target as a smoke test
because only that target exercises rules_multitool, Bazel runfiles, and uvx
together.
multitool_aliases exposes two native-tool targets:
<command>uses the caller's working directory and is intended forbazel run.<command>_binaryis the raw executable for use as a tool dependency in other Bazel rules.
Python tools expose only the runnable <command> target. They are intended for
interactive and CI use, not as executable dependencies in Bazel actions. The
target embeds the catalog metadata as arguments to a shell launcher and carries
only the pinned uvx executable in its runfiles, which keeps execution
independent of host Python and avoids runtime catalog path resolution.
rules_shell supplies the sh_binary and sh_test APIs consistently across
the supported Bazel generations, so one BUILD definition serves every
supported consumer.
The runner supports mixed workflows without exposing two user interfaces. A container-only implementation would not cover developers who do not run the DevContainer at all, while arbitrary system-installed tools would lose version alignment with those who do.
Full Bazel toolchains remain appropriate for tools that participate in build
actions or platform transitions. They add unnecessary complexity for the
standalone CLIs covered here. Bazel targets, exported lockfiles, and
internal/devcontainer/install.py are implementation details; the runner is
the only documented consumer invocation method.