Pinned Build Tools with mise¶
mise is a command-line tool that installs exact versions of developer tools (CMake, Ninja, Python, Node.js and others) per project, without touching the versions installed system-wide. Pulp uses it to describe the build tools that build Pulp itself, in one reviewed place.
This page covers what Pulp pins today, how to use the pinned tools when you build Pulp from source, what it means for plugin developers who use the SDK, and what is planned but not built yet.
Pulp's adoption policy¶
Pulp treats tools/toolchain/manifest.toml as the canonical description of
its build-tool versions. Use the locked mise profile for reproducible local
builds, Windows VM probes, release investigations, and any change that edits
the toolchain manifest or generated lock files:
This is a consistency requirement for toolchain and release evidence: record
the mise version, platform, manifest revision, and lock checksum with the
receipt. A build is not evidence of the pinned toolchain merely because mise
is installed; run the command through mise exec (or activate the same
environment) and run tools/toolchain/check_env.py.
Ordinary source builds and public CI remain compatible with hosts that do not
have mise yet. The CI images and bootstrap path are being migrated in a
separate, reviewed step; they must not silently download tools or change a
working host's PATH. Until that migration lands, a non-mise build must be
reported as host-toolchain evidence, with the versions from
tools/toolchain/check_env.py, rather than as a pinned-toolchain build.
For Windows VM work, keep the image disposable and use the same locked profile
for ARM64 and x64 guests. Do not put credentials, VM-specific paths, or
mutable cache locations in .mise.toml; keep those in the VM recipe or host
configuration.
Why Pulp uses mise¶
A build machine usually gets its tools from whatever it happens to have: Homebrew on one Mac, apt on a Linux box, a version manager such as nvm on another. Two machines that read the same source can then build it with different CMake, Python or Node.js versions, and a result from one machine does not reproduce on another. The same kind of drift appears between a developer laptop and the CI virtual machines.
Pulp's answer is a single source of truth for build-tool versions:
tools/toolchain/manifest.tomlnames one exact version for every build tool. Floating selectors such asstable,latest,homebrewor a bare3.12are refused by the generator.tools/toolchain/generate.pyturns that manifest into the files mise reads, plus the tool fields of the CI virtual-machine image definitions (.shipyard/vm-image.toml,.shipyard/vm-image.intel.toml).mise.toolchain.lockrecords, for each tool and each platform, the exact download URL and its sha256 checksum.mise install --lockedinstalls only what the lock names, so a changed or substituted upstream download is refused instead of silently installed.
Prebuilt libraries (Skia, Dawn, V8) are not mise tools. They keep their own
pinned, checksummed fetch path in tools/deps/manifest.json.
What the lock pins¶
| Tool | Version | Installed by | Locked platforms |
|---|---|---|---|
| CMake | 4.3.3 | mise (aqua:Kitware/CMake) |
linux-arm64, linux-x64, macos-arm64, macos-x64, windows-arm64, windows-x64 |
| Ninja | 1.13.2 | mise (aqua:ninja-build/ninja) |
all six |
| Python | 3.14.4 | mise (core:python) |
all six |
| Node.js | 26.10.0 | mise (core:node) |
all six |
| clang-format | 21.1.8 | mise (conda:clang-format) |
all except windows-arm64 (conda-forge publishes no build for it) |
| Tart | 2.38.0 | mise (aqua:openai/tart) |
macOS only (Tart runs only on macOS hosts) |
Two tools are pinned in the manifest but are not installed by mise:
- Rust (
rust = "1.97.1"in the manifest, withrustfmtandclippy). rustup installs Rust and cargo readsrust-toolchain.toml. mise could only record a version string for Rust, not a checksum, so Rust is deliberately left out of the lock. Note thatexperimental/pulp-rs/rust-toolchain.tomlstill selectschannel = "stable"; the manifest's 1.97.1 records what the CI gate image actually runs, and generatingrust-toolchain.tomlfrom the manifest is planned (see below). - Xcode (
xcode = "26.5", build17F42). Xcode is installed into the macOS CI image by the image provisioning scripts, not by mise.
The manifest also records, per tool, whether a version is already what the CI
gate runs (observed) or a version the gate still has to reach (target).
That evidence lives in the manifest's [evidence] table.
Building Pulp from source¶
The pinned tools live in an environment-scoped mise config,
.mise.toolchain.toml, which mise loads only when MISE_ENV=toolchain is set.
Entering a Pulp checkout never changes an existing shell until you opt in.
For reproducibility-sensitive work, opting in is the project policy; the
remaining host-toolchain path is a migration boundary, not a second set of
Pulp pins. setup.sh, pulp build, and public CI still accept hosts without
mise until their provisioning is migrated and verified.
1. Install mise 2026.10.17¶
The lock is generated with mise 2026.10.17, and only that version is guaranteed to read it the same way: mise releases differ in how they resolve some download URLs, so an older mise can read the same config differently or reject rows of the lock.
Install it from the release archive and verify it before use. mise publishes a
SHASUMS256.txt for each release, signed with
minisign. Its public key is in the
mise installation docs and in the
repository's minisign.pub.
You need minisign installed (for example brew install minisign or your
Linux package manager). On macOS (on Linux use linux-x64 or linux-arm64, and
sha256sum -c in place of shasum -a 256 -c):
mise_version=2026.10.17
mise_platform=macos-arm64 # or macos-x64
base="https://github.com/jdx/mise/releases/download/v${mise_version}"
curl -fL -O "$base/SHASUMS256.txt" -O "$base/SHASUMS256.txt.minisig" \
-O "$base/mise-v${mise_version}-${mise_platform}.tar.gz"
# 1. The checksum file is signed by the mise release key.
minisign -Vm SHASUMS256.txt -P RWTC3g8W3z4RZK3V3qv7fa1QY4JEWyBtqIHW+85QlJpZc5yG+uNYNBSZ
# 2. The archive matches the signed checksum file.
grep " ./mise-v${mise_version}-${mise_platform}.tar.gz$" SHASUMS256.txt | shasum -a 256 -c
# 3. Only then install it.
tar -xzf "mise-v${mise_version}-${mise_platform}.tar.gz"
mkdir -p ~/.local/bin && install -m 755 mise/bin/mise ~/.local/bin/mise
mise --version
Stop if either check fails. As a cross-check, the 2026.10.17 archives have these sha256 digests:
| Archive | sha256 |
|---|---|
mise-v2026.10.17-macos-arm64.tar.gz |
d7c50c1683524a764260986ccc9c41c0de5e1fcd491714b5640b657b76781af6 |
mise-v2026.10.17-macos-x64.tar.gz |
e42418100e2f781f9be00b9feaa60378e77551741faececc2bf360992c5410c8 |
mise-v2026.10.17-linux-arm64.tar.gz |
cac510e51793ad45e65fe92305bdbe4309c9c60b5907badbc7eba111738ab08f |
mise-v2026.10.17-linux-x64.tar.gz |
17565e268c5542d8ce944321e17af81e0b741b65af4aa7e8e010ee77376a4aa3 |
mise-v2026.10.17-windows-x64.zip |
32bcfda5f72bf7cb189e18ba93c2798bffcb4a678fd004e3127780363f2d8c8c |
mise-v2026.10.17-windows-arm64.zip |
d9fb396fa2a1f41b7c68275b6e0e6af4126c2ce138c61b19b819d502cfbf0f43 |
On Windows, compare (Get-FileHash <archive> -Algorithm SHA256).Hash with the
table (PowerShell prints it in upper case).
The convenience installer curl https://mise.run | MISE_VERSION=v2026.10.17 sh
checks the archive's sha256, against digests embedded in the script when you
ask for the script's own current release and against the release's
SHASUMS256.txt otherwise. It does not check the minisign or GPG signature
(the script marks that as a to-do), and the script itself, including those
embedded digests, is served unpinned from mise.run. Prefer the steps above.
2. Trust the checkout and install the locked tools¶
From the root of your Pulp checkout:
--locked makes mise refuse any tool or platform that the lock does not cover,
rather than resolving a new download. Pulp's .mise.toml also sets
locked = true and auto_install = false, so mise never installs a tool
behind your back. mise applies a project's [settings] only after you trust
the checkout, so run mise trust first; until then those two settings, and
disable_update_warning, are ignored, and mise falls back to its defaults
(locked = false, auto_install = true). On a machine with many worktrees,
trust their common parent once instead of each checkout, for example by
exporting MISE_TRUSTED_CONFIG_PATHS="$HOME/Code" in your shell profile (the
fleet hosts use their checkout roots: /Volumes/Workshop/Code on m3, ~/Code
on m1, m5 and m5s). Confirm it from inside a checkout:
Automation does not rely on trust: the CI candidate lane and the golden-image
layer also set MISE_LOCKED=1 and MISE_AUTO_INSTALL=0, which apply whether
or not the checkout is trusted.
3. Use the tools¶
Either run single commands through mise:
or export MISE_ENV=toolchain and
activate mise in your shell, so
the pinned cmake, ninja, python3, node and clang-format come first on
your PATH inside the checkout.
With MISE_ENV=toolchain set, the configure of the Pulp checkout makes
find_package(Python3) take the pinned Python that mise puts first on PATH
(Python3_FIND_FRAMEWORK=LAST). Without it, CMake on macOS prefers any
Python.framework install, such as Homebrew's, over the pinned one. A build
directory configured before you opted in keeps its cached interpreter, so
configure a fresh one.
Project tasks¶
.mise.toml also defines tasks you can run with mise run <task>:
| Task | What it does |
|---|---|
npm-deps |
Installs the project's npm packages with npm ci from the committed lockfiles in tools/import-design/jsx-runtime and tools/scripts |
visual-deps |
Installs the hash-pinned Python packages for the visual-analysis tests |
visual-check |
Checks that the configured Python can run the visual-analysis tests |
visual-deps-windows, visual-check-windows |
The same two tasks using the Windows python command |
How the pins change¶
A tool version changes only through a pull request that:
- edits
tools/toolchain/manifest.toml(the version and its[evidence]entry), and - regenerates the derived files with the pinned mise:
This rewrites .mise.toolchain.toml, the generated block in .mise.toml,
the VM image fields, and mise.toolchain.lock, and records the lock's
sha256 in the manifest. It refuses to run with any mise version other than
the manifest's [mise].version, and runs mise with throwaway data, cache and
config directories so your own mise setup cannot leak into the lock.
tools/toolchain/update.py does both steps for one tool and prints a table of
every version and lock checksum that moved, for the pull request body:
python3 tools/toolchain/update.py cmake --to 4.3.4 --mise /path/to/mise-2026.10.17
python3 tools/toolchain/update.py rust --to 1.98.0 # no lock rows: rustup installs Rust
python3 tools/toolchain/update.py shipyard --to v0.270.0
python3 tools/toolchain/deps.py groups the three operations under one
command: check (the environment report), resolve (fetch what a build needs
once, before building: setup.sh --deps-only for the pinned dependency sources,
then npm ci for any lockfile directory whose node_modules differs, then the
report; it never installs a host tool), and update (the above, finding the
installed pinned mise itself).
update also marks the tool's [evidence] row as a target (the gate has not been
seen running it yet). It never commits, pushes or installs. If any step fails
it restores every file it touched. Moving mise itself is refused: that takes a
reviewed source build of the new release.
The toolchain-test_generate-selftest ctest checks that the committed files
equal what the manifest generates (python3 tools/toolchain/generate.py
--check runs the same comparison), that the lock hashes to the manifest's
lock_sha256, that every locked URL comes from the tool's own release
location, and that every tool has a checksummed entry, or a stated reason for
not having one, on every platform. Never edit the generated files by hand.
Checking your environment¶
tools/toolchain/check_env.py compares the tools on your PATH with the
manifest and prints one line per difference: the tool, the pinned version, the
version it found, where it found it, and the command that fixes it.
toolchain-check: cmake: expected 4.3.3 (tools/toolchain/manifest.toml), found 4.4.3 at /opt/homebrew/bin/cmake; fix: MISE_ENV=toolchain mise install --locked && eval "$(MISE_ENV=toolchain mise activate bash)"
toolchain-check: 1 difference(s) from the manifest; continuing (warn mode)
It checks python3, cmake, ninja, node, rustc (asked from
experimental/pulp-rs, where cargo builds), clang-format, the mise binary
(version, and its sha256 against [mise.binary] in the manifest), and that each
npm lockfile directory's node_modules is exactly its package-lock.json. It
only reads; it never installs anything.
It runs on any Python 3.9 or newer with no packages, because it is what checks
the interpreter. It reads tools/toolchain/expected.json, which generate.py
writes from the manifest, rather than the TOML manifest itself.
Exit status: 0 when everything matches or the manifest is in warn mode (it
is today), 1 for a difference once the manifest is in enforce mode, and 3
when it cannot read the expected versions. --format github prints GitHub
Actions ::warning:: annotations and --format json a machine-readable
report.
python3 tools/toolchain/fleet_report.py --host m1 --host m3=local --host m5 --host m5s
runs the same check on each host over ssh, under the non-login PATH lanes and
agents get, and prints one table of the version every host runs against the
pin. It copies the check into a temporary directory, removes it afterwards, and
installs nothing; a host it cannot reach is shown as unreachable.
For a host rollout step (a fleet host or a CI image moving to the pinned toolchain), run the check the way the step judges it:
--fleet-path also looks in the pinned mise's directory and the rustup proxies
(~/.cargo/bin), which a non-login ssh or launchd PATH lacks; without it an
installed Rust is reported missing. --require-tools exits 1 on any host-tool
difference whatever the manifest's mode, while still only reporting the npm
trees each checkout installs for itself (pulp deps resolve).
The pinned mise never asks whether a newer release exists:
.mise.toml sets disable_update_warning = true, and every version probe the
toolchain scripts run sets MISE_DISABLE_UPDATE_WARNING=1, since mise
--version is the one command that would otherwise query GitHub. Automatic
self-update is off by default.
A platform the manifest lists as an exception (Linux and Windows still take CMake from the system package manager) is still reported, labelled as the declared exception, so the difference stays visible.
PULP_TOOLCHAIN_UNPINNED=1 is the opt-out for contributors who manage their
own tools: once enforcement is on, it lets the check pass with a banner naming
every difference, on every run. It is refused on a fleet host, one with
~/.config/pulp/fleet-host or PULP_FLEET_HOST=1.
It also runs on its own, once, before a build:
- every configure of the Pulp checkout runs it (
tools/cmake/PulpToolchainCheck.cmake), which coverspulp build,pulp dev,pulp loopand a rawcmake -S . -B buildalike, and prints its lines as configure status. It does not run when Pulp is a subproject of another build, so SDK and plugin projects never see it; setup.sh, including thesetup.sh --deps-onlythatpulp buildruns on a cold checkout, runs it before fetching any dependency;- every CI build leg (macOS, Linux, Windows) runs it as its own step before
configuring and shows the differences as warnings on the run (the configure and
setup.sh --ciskip it there, so it is reported once).
In warn mode none of these fails. Once the manifest is in enforce mode a
difference stops the configure and setup.sh with the same lines.
Building plugins with the Pulp SDK¶
If you build plugins with the installed SDK (pulp create, pulp build in a
project with a pulp.toml), mise is optional and nothing in the SDK
workflow reads it. The SDK ships compiled libraries, so you need the
prerequisites listed in Getting Started,
not Pulp's own build toolchain. You are free to use mise in your own project to
pin your own tools.
Opt-in pulp tool installs are separate¶
Tools installed with pulp tool install (for example the Audio Quality Lab)
are not governed by the toolchain manifest or the mise lock. They keep
their own contract, described in
Extending Pulp:
each has a registry pinned_version, pulp tool update <id> [--version <v>]
re-installs it, and you can override its version with
PULP_TOOL_<ID>_VERSION or $PULP_HOME/tool-overrides.json. They install
under ~/.pulp/tools/, and mise neither installs anything there nor reads
those overrides.
Planned¶
The following is designed but not built yet. None of it runs today; this section is here so the direction is visible, not as instructions.
- A published mise binary. The macOS arm64 build the fleet runs is pinned by digest today; a reproducible build from source, published with checksums for every platform, is still to come.
- The check before generators of committed files. The generators that write committed files run the environment check first, and refuse outside the pinned environment.
- Rust from the manifest. Generate
rust-toolchain.tomlfrom the manifest so cargo uses the pinned Rust instead of the currentstable. - CI images built from the lock.
.shipyard/vm-image.mise.tomlandtools/ci/mise-golden-layer.shbake a candidate golden with the pinned toolchain installed (mise install --locked, Rust by name) and a receipt of what the guest holds. Gate lanes do not use it yet; one lane moves to it behind a flag after a one-host trial, and an image is only promoted when its receipt matches the manifest. - Enforcement on the required CI gate, after a warn-only period.
- A
toolchain.misesetting for SDK projects. The SDK installer would default to setting up mise for a new plugin project, with a fully supported way to opt out. SDK projects would never be subject to Pulp's toolchain enforcement; their pin stayssdk_versioninpulp.toml.